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 0fa911ca91e..da8d0c6ea54 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -184,7 +184,7 @@ jobs: windows: needs: lint-commits name: test Windows - runs-on: windows-2019 + runs-on: windows-latest strategy: fail-fast: false matrix: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a4df3609e2c..60863f3b5a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ on: required: false default: prerelease push: - branches: [master, feature/*] + branches: [master, feature/*, release/*] # tags: # - v[0-9]+.[0-9]+.[0-9]+ @@ -40,12 +40,16 @@ jobs: # run: echo 'TAG_NAME=prerelease' >> $GITHUB_ENV - if: github.event_name == 'workflow_dispatch' run: echo "TAG_NAME=${{ github.event.inputs.tag_name }}" >> $GITHUB_ENV - - if: github.ref_name != 'master' + - if: startsWith(github.ref_name, 'feature/') run: | - TAG_NAME=${{ github.ref_name }} - FEAT_NAME=$(echo $TAG_NAME | sed 's/feature\///') + FEAT_NAME=$(echo ${{ github.ref_name }} | sed 's/feature\///') echo "FEAT_NAME=$FEAT_NAME" >> $GITHUB_ENV echo "TAG_NAME=pre-$FEAT_NAME" >> $GITHUB_ENV + - if: startsWith(github.ref_name, 'release/') + run: | + RC_NAME=$(echo ${{ github.ref_name }} | sed 's/release\///') + echo "FEAT_NAME=" >> $GITHUB_ENV + echo "TAG_NAME=$RC_NAME" >> $GITHUB_ENV - if: github.ref_name == 'master' run: | echo "FEAT_NAME=" >> $GITHUB_ENV @@ -105,10 +109,14 @@ jobs: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 - name: Delete existing prerelease - # "prerelease" (main branch) or "pre-" - if: "env.TAG_NAME == 'prerelease' || startsWith(env.TAG_NAME, 'pre-')" + # "prerelease" (main branch), "pre-", or "rc-" + if: env.TAG_NAME == 'prerelease' || startsWith(env.TAG_NAME, 'pre-') || startsWith(env.TAG_NAME, 'rc-') run: | - echo "SUBJECT=AWS IDE Extensions: ${FEAT_NAME:-${TAG_NAME}}" >> $GITHUB_ENV + if [[ "$TAG_NAME" == rc-* ]]; then + echo "SUBJECT=AWS IDE Extensions Release Candidate: ${TAG_NAME#rc-}" >> $GITHUB_ENV + else + echo "SUBJECT=AWS IDE Extensions: ${FEAT_NAME:-${TAG_NAME}}" >> $GITHUB_ENV + fi gh release delete "$TAG_NAME" --cleanup-tag --yes || true # git push origin :"$TAG_NAME" || true - name: Publish Prerelease diff --git a/.github/workflows/setup-release-candidate.yml b/.github/workflows/setup-release-candidate.yml new file mode 100644 index 00000000000..390669d22af --- /dev/null +++ b/.github/workflows/setup-release-candidate.yml @@ -0,0 +1,56 @@ +name: Setup Release Candidate + +on: + workflow_dispatch: + inputs: + commitId: + description: 'Commit ID to create RC from' + required: true + type: string + +jobs: + setup-rc: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.commitId }} + token: ${{ secrets.GITHUB_TOKEN }} + persist-credentials: true + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Generate Branch Name + id: branch-name + 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 }} + run: | + git config user.name "aws-toolkit-automation" + git config user.email "<>" + + # 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 596af538b2e..fb06d810f42 100644 --- a/.gitignore +++ b/.gitignore @@ -31,8 +31,9 @@ src.gen/* **/src/shared/telemetry/clienttelemetry.d.ts **/src/codewhisperer/client/codewhispererclient.d.ts **/src/codewhisperer/client/codewhispereruserclient.d.ts -**/src/amazonqFeatureDev/client/featuredevproxyclient.d.ts **/src/auth/sso/oidcclientpkce.d.ts +**/src/sagemakerunifiedstudio/shared/client/gluecatalogapi.d.ts +**/src/sagemakerunifiedstudio/shared/client/sqlworkbench.d.ts # Generated by tests **/src/testFixtures/**/bin @@ -56,3 +57,6 @@ packages/*/resources/css/icons.css # Created by `npm run webRun` when testing extension in web mode .vscode-test-web + +# License scanning output +licenses-full.json diff --git a/LICENSE-THIRD-PARTY b/LICENSE-THIRD-PARTY new file mode 100644 index 00000000000..efdb4af3017 --- /dev/null +++ b/LICENSE-THIRD-PARTY @@ -0,0 +1,10428 @@ +@aws/language-server-runtimes +0.2.128 + + 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.56 + + 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. + + +****************************** + +available-typed-arrays +1.0.5 +MIT License + +Copyright (c) 2020 Inspect JS + +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. + + +****************************** + +aws-sdk +2.1692.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 2012-2017 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. + + +****************************** + +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. + + +****************************** + +call-bind +1.0.7 +MIT License + +Copyright (c) 2020 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. + + +****************************** + +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. + + +****************************** + +define-data-property +1.1.4 +MIT License + +Copyright (c) 2023 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. + + +****************************** + +define-properties +1.1.4 +The MIT License (MIT) + +Copyright (C) 2015 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. + +****************************** + +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. + +****************************** + +es-abstract +1.20.2 +The MIT License (MIT) + +Copyright (C) 2015 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. + + +****************************** + +es-define-property +1.0.0 +MIT License + +Copyright (c) 2024 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. + + +****************************** + +es-errors +1.3.0 +MIT License + +Copyright (c) 2024 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. + + +****************************** + +es-to-primitive +1.2.1 +The MIT License (MIT) + +Copyright (c) 2015 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. + + + +****************************** + +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) + +****************************** + +events +1.1.1 +MIT + +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. + + +****************************** + +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 + +****************************** + +for-each +0.3.3 +The MIT License (MIT) + +Copyright (c) 2012 Raynos. + +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. + + + +****************************** + +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. + + +****************************** + +function-bind +1.1.2 +Copyright (c) 2013 Raynos. + +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. + + + +****************************** + +function.prototype.name +1.1.5 +The MIT License (MIT) + +Copyright (c) 2016 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. + + +****************************** + +functions-have-names +1.2.3 +MIT License + +Copyright (c) 2019 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. + + +****************************** + +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. + + +****************************** + +get-intrinsic +1.2.4 +MIT License + +Copyright (c) 2020 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. + + +****************************** + +get-symbol-description +1.0.0 +MIT License + +Copyright (c) 2021 Inspect JS + +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. + + +****************************** + +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/ + + +****************************** + +gopd +1.0.1 +MIT License + +Copyright (c) 2022 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. + + +****************************** + +has +1.0.3 +license: MIT +authors: Thiago de Arruda + +****************************** + +has-bigints +1.0.2 +MIT License + +Copyright (c) 2019 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. + + +****************************** + +has-property-descriptors +1.0.2 +MIT License + +Copyright (c) 2022 Inspect JS + +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. + + +****************************** + +has-proto +1.0.3 +MIT License + +Copyright (c) 2022 Inspect JS + +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. + + +****************************** + +has-symbols +1.0.3 +MIT License + +Copyright (c) 2016 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. + + +****************************** + +has-tostringtag +1.0.0 +MIT License + +Copyright (c) 2021 Inspect JS + +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. + + +****************************** + +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. + + + +****************************** + +hasown +2.0.2 +MIT License + +Copyright (c) Jordan Harband and 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. + + +****************************** + +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. + + +****************************** + +internal-slot +1.0.3 +MIT License + +Copyright (c) 2019 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 +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-arguments +1.1.1 +The MIT License (MIT) + +Copyright (c) 2014 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-bigint +1.0.4 +MIT License + +Copyright (c) 2018 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-boolean-object +1.1.2 +The MIT License (MIT) + +Copyright (c) 2015 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-callable +1.2.4 +The MIT License (MIT) + +Copyright (c) 2015 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-date-object +1.0.5 +The MIT License (MIT) + +Copyright (c) 2015 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. + + +****************************** + +is-generator-function +1.0.10 +The MIT License (MIT) + +Copyright (c) 2014 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-negative-zero +2.0.2 +The MIT License (MIT) + +Copyright (c) 2014 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-number-object +1.0.7 +The MIT License (MIT) + +Copyright (c) 2015 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-regex +1.1.4 +The MIT License (MIT) + +Copyright (c) 2014 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-shared-array-buffer +1.0.2 +MIT License + +Copyright (c) 2021 Inspect JS + +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-string +1.0.7 +The MIT License (MIT) + +Copyright (c) 2015 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-symbol +1.0.4 +The MIT License (MIT) + +Copyright (c) 2015 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-typed-array +1.1.9 +The MIT License (MIT) + +Copyright (c) 2015 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-weakref +1.0.2 +MIT License + +Copyright (c) 2020 Inspect JS + +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. + + + +****************************** + +jmespath +0.16.0 +Copyright 2014 James Saryerwinnie + +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. + + +****************************** + +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. + + +****************************** + +object-inspect +1.13.2 +MIT License + +Copyright (c) 2013 James Halliday + +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-keys +1.1.1 +The MIT License (MIT) + +Copyright (C) 2013 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. + +****************************** + +object.assign +4.1.4 +The MIT License (MIT) + +Copyright (c) 2014 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. + +****************************** + +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. + +****************************** + +punycode +1.3.2 +license: MIT +authors: Mathias Bynens + +****************************** + +querystring +0.2.0 + +Copyright 2012 Irakli Gozalishvili. 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. + + +****************************** + +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. +""" + + +****************************** + +regexp.prototype.flags +1.4.3 +The MIT License (MIT) + +Copyright (C) 2014 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. + + + +****************************** + +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. + + +****************************** + +set-function-length +1.2.2 +MIT License + +Copyright (c) Jordan Harband and 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. + + +****************************** + +side-channel +1.0.6 +MIT License + +Copyright (c) 2019 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. + + +****************************** + +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.prototype.trimend +1.0.5 +MIT License + +Copyright (c) 2017 Khaled Al-Ansari + +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.prototype.trimstart +1.0.5 +MIT License + +Copyright (c) 2017 Khaled Al-Ansari + +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 + + +****************************** + +unbox-primitive +1.0.2 +MIT License + +Copyright (c) 2019 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. + + +****************************** + +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. + + +****************************** + +url +0.10.3 +The MIT License (MIT) + +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. + + +****************************** + +util +0.12.5 +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. + + +****************************** + +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. + + +****************************** + +uuid +8.0.0 +The MIT License (MIT) + +Copyright (c) 2010-2020 Robert Kieffer 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. + + +****************************** + +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-boxed-primitive +1.0.2 +MIT License + +Copyright (c) 2019 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. + + +****************************** + +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. + + +****************************** + +which-typed-array +1.1.8 +The MIT License (MIT) + +Copyright (c) 2015 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. + + + +****************************** + +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.6.2 +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/linuxTests.yml b/buildspec/linuxTests.yml index 900b720e61a..241b5bb193a 100644 --- a/buildspec/linuxTests.yml +++ b/buildspec/linuxTests.yml @@ -48,7 +48,7 @@ phases: - VCS_COMMIT_ID="${CODEBUILD_RESOLVED_SOURCE_VERSION}" - CI_BUILD_URL=$(echo $CODEBUILD_BUILD_URL | sed 's/#/%23/g') # Encode `#` in the URL because otherwise the url is clipped in the Codecov.io site - CI_BUILD_ID="${CODEBUILD_BUILD_ID}" - - test -n "${CODECOV_TOKEN}" && [ "$TARGET_BRANCH" = "master" ] && ./codecov --token=${CODECOV_TOKEN} --branch=${CODEBUILD_RESOLVED_SOURCE_VERSION} --repository=${CODEBUILD_SOURCE_REPO_URL} --file=./coverage/amazonq/lcov.info --file=./coverage/toolkit/lcov.info + - test -n "${CODECOV_TOKEN}" && [ "$TARGET_BRANCH" = "master" ] && ./codecov --token=${CODECOV_TOKEN} --branch=${CODEBUILD_RESOLVED_SOURCE_VERSION} --repository=${CODEBUILD_SOURCE_REPO_URL} --file=./coverage/amazonq/lcov.info --file=./coverage/toolkit/lcov.info || true reports: unit-test: 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 42d94d334a4..49a6ad00b87 100644 --- a/docs/lsp.md +++ b/docs/lsp.md @@ -26,9 +26,9 @@ sequenceDiagram ## Language Server Debugging -1. Clone https://github.com/aws/language-servers.git and set it up in the same workspace as this project by cmd+shift+p and "add folder to workspace" and selecting the language-servers folder that you just cloned. Your VS code folder structure should look like below. +If you want to connect a local version of language-servers to aws-toolkit-vscode, follow these steps: - +1. Clone https://github.com/aws/language-servers.git and set it up in the same workspace as this project by cmd+shift+p and "add folder to workspace" and selecting the language-servers folder that you just cloned. Your VS code folder structure should look like below. ``` /aws-toolkit-vscode @@ -45,7 +45,7 @@ sequenceDiagram npm run package ``` to get the project setup -3. Enable the lsp experiment: +3. You need to open VScode user settings (Cmd+Shift+P and Search "Open User Settings (JSON)") and add the lines below at the bottom of the settings to enable the lsp experiment: ``` "aws.experiments": { "amazonqLSP": true, @@ -54,9 +54,84 @@ sequenceDiagram } ``` 4. Uncomment the `__AMAZONQLSP_PATH` and `__AMAZONQLSP_UI` variables in the `amazonq/.vscode/launch.json` extension configuration -5. Use the `Launch LSP with Debugging` configuration and set breakpoints in VSCode or the language server +5. Use the `Launch LSP with Debugging` configuration and set breakpoints in VSCode or the language server, Once you run "Launch LSP with Debugging" a new window should start, wait for the plugin to show up there. Then go to the run menu again and run "Attach to Language Server (amazonq)" after this you should be able to add breakpoints in the LSP code. 6. (Optional): Enable `"amazonq.trace.server": "on"` or `"amazonq.trace.server": "verbose"` in your VSCode settings to view detailed log messages sent to/from the language server. These log messages will show up in the "Amazon Q Language Server" output channel +### Breakpoints Work-Around + +If the breakpoints in your language-servers project remain greyed out and do not trigger when you run `Launch LSP with Debugging`, your debugger may be attaching to the language server before it has launched. You can follow the work-around below to avoid this problem. If anyone fixes this issue, please remove this section. + +1. Set your breakpoints and click `Launch LSP with Debugging` +2. Once the debugging session has started, click `Launch LSP with Debugging` again, then `Cancel` on any pop-ups that appear +3. On the debug panel, click `Attach to Language Server (amazonq)` next to the red stop button +4. Click `Launch LSP with Debugging` again, then `Cancel` on any pop-ups that appear + +## Language Server Runtimes Debugging + +If you want to connect a local version of language-server-runtimes to aws-toolkit-vscode, follow these steps: + +1. Clone https://github.com/aws/language-server-runtimes.git and set it up in the same workspace as this project by cmd+shift+p and "add folder to workspace" and selecting the language-server-runtimes folder that you just cloned. Your VS code folder structure should look like below. + + ``` + /aws-toolkit-vscode + /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 + +4b. Inside of aws-toolkit-vscode run: + + npm install + npm link @aws/language-server-runtimes @aws/language-server-runtimes-types + ## Amazon Q Inline Activation - In order to get inline completion working you must open a supported file type defined in CodewhispererInlineCompletionLanguages in `packages/amazonq/src/app/inline/completion.ts` diff --git a/package-lock.json b/package-lock.json index baf0c46764a..7ad26d71ac9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,15 +15,18 @@ "plugins/*" ], "dependencies": { + "@aws/language-server-runtimes": "^0.2.128", "@types/node": "^22.7.5", + "jaro-winkler": "^0.2.8", "vscode-nls": "^5.2.0", "vscode-nls-dev": "^4.0.4" }, "devDependencies": { - "@aws-toolkits/telemetry": "^1.0.323", + "@aws-toolkits/telemetry": "^1.0.329", "@playwright/browser-chromium": "^1.43.1", "@stylistic/eslint-plugin": "^2.11.0", "@types/he": "^1.2.3", + "@types/jaro-winkler": "^0.2.4", "@types/vscode": "^1.68.0", "@types/vscode-webview": "^1.57.1", "@types/webpack-env": "^1.18.5", @@ -68,6 +71,966 @@ "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", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-3.0.0.tgz", + "integrity": "sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==", + "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", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-crypto/sha256-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-3.0.0.tgz", + "integrity": "sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==", + "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", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-crypto/supports-web-crypto": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-3.0.0.tgz", + "integrity": "sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==", + "dependencies": { + "tslib": "^1.11.1" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-crypto/supports-web-crypto/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-crypto/util": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-3.0.0.tgz", + "integrity": "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-crypto/util/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/client-sso": { + "version": "3.363.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.363.0.tgz", + "integrity": "sha512-PZ+HfKSgS4hlMnJzG+Ev8/mgHd/b/ETlJWPSWjC/f2NwVoBQkBnqHjdyEx7QjF6nksJozcVh5Q+kkYLKc/QwBQ==", + "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" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.363.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.363.0.tgz", + "integrity": "sha512-V3Ebiq/zNtDS/O92HUWGBa7MY59RYSsqWd+E0XrXv6VYTA00RlMTbNcseivNgp2UghOgB9a20Nkz6EqAeIN+RQ==", + "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" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/client-sts": { + "version": "3.363.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.363.0.tgz", + "integrity": "sha512-0jj14WvBPJQ8xr72cL0mhlmQ90tF0O0wqXwSbtog6PsC8+KDE6Yf+WsxsumyI8E5O8u3eYijBL+KdqG07F/y/w==", + "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" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.363.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.363.0.tgz", + "integrity": "sha512-VAQ3zITT2Q0acht0HezouYnMFKZ2vIOa20X4zQA3WI0HfaP4D6ga6KaenbDcb/4VFiqfqiRHfdyXHP0ThcDRMA==", + "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/credential-provider-ini": { + "version": "3.363.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.363.0.tgz", + "integrity": "sha512-ZYN+INoqyX5FVC3rqUxB6O8nOWkr0gHRRBm1suoOlmuFJ/WSlW/uUGthRBY5x1AQQnBF8cpdlxZzGHd41lFVNw==", + "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" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.363.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.363.0.tgz", + "integrity": "sha512-C1qXFIN2yMxD6pGgug0vR1UhScOki6VqdzuBHzXZAGu7MOjvgHNdscEcb3CpWnITHaPL2ztkiw75T1sZ7oIgQg==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.363.0.tgz", + "integrity": "sha512-fOKAINU7Rtj2T8pP13GdCt+u0Ml3gYynp8ki+1jMZIQ+Ju/MdDOqZpKMFKicMn3Z1ttUOgqr+grUdus6z8ceBQ==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.363.0.tgz", + "integrity": "sha512-5RUZ5oM0lwZSo3EehT0dXggOjgtxFogpT3cZvoLGtIwrPBvm8jOQPXQUlaqCj10ThF1sYltEyukz/ovtDwYGew==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.363.0.tgz", + "integrity": "sha512-Z6w7fjgy79pAax580wdixbStQw10xfyZ+hOYLcPudoYFKjoNx0NQBejg5SwBzCF/HQL23Ksm9kDfbXDX9fkPhA==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.363.0.tgz", + "integrity": "sha512-FobpclDCf5Y1ueyJDmb9MqguAdPssNMlnqWQpujhYVABq69KHu73fSCWSauFPUrw7YOpV8kG1uagDF0POSxHzA==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.363.0.tgz", + "integrity": "sha512-SSGgthScYnFGTOw8EzbkvquqweFmvn7uJihkpFekbtBNGC/jGOGO+8ziHjTQ8t/iI/YKubEwv+LMi0f77HKSEg==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.363.0.tgz", + "integrity": "sha512-MWD/57QgI/N7fG8rtzDTUdSqNpYohQfgj9XCFAoVeI/bU4usrkOrew43L4smJG4XrDxlNT8lSJlDtd64tuiUZA==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.363.0.tgz", + "integrity": "sha512-ri8YaQvXP6odteVTMfxPqFR26Q0h9ejtqhUDv47P34FaKXedEM4nC6ix6o+5FEYj6l8syGyktftZ5O70NoEhug==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.363.0.tgz", + "integrity": "sha512-6+0aJ1zugNgsMmhTtW2LBWxOVSaXCUk2q3xyTchSXkNzallYaRiZMRkieW+pKNntnu0g5H1T0zyfCO0tbXwxEA==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.357.0.tgz", + "integrity": "sha512-/riCRaXg3p71BeWnShrai0y0QTdXcouPSM0Cn1olZbzTf7s71aLEewrc96qFrL70XhY4XvnxMpqQh+r43XIL3g==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.357.0.tgz", + "integrity": "sha512-XHKyS5JClT9su9hDif715jpZiWHQF9gKZXER8tW0gOizU3R9cyWc9EsJ2BRhFNhi7nt/JF/CLUEc5qDx3ETbUw==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.363.0.tgz", + "integrity": "sha512-fk9ymBUIYbxiGm99Cn+kAAXmvMCWTf/cHAcB79oCXV4ELXdPa9lN5xQhZRFNxLUeXG4OAMEuCAUUuZEj8Fnc1Q==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.363.0.tgz", + "integrity": "sha512-Fli/dvgGA9hdnQUrYb1//wNSFlK2jAfdJcfNXA6SeBYzSeH5pVGYF4kXF0FCdnMA3Fef+Zn1zAP/hw9v8VJHWQ==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-5imgGUlZL4dW4YWdMYAKLmal9ny/tlenM81QZY7xYyb76z9Z/QOg7oM5Ak9HQl8QfFTlGVWwcMXl+54jroRgEQ==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-1.1.0.tgz", + "integrity": "sha512-7WD9eZHp46BxAjNGHJLmxhhyeiNWkBdVStd7SUJPUZqQGeIO/REtIrcIfKUfdiHTQ9jyu2SYoqvzqqaFc6987w==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-1.1.0.tgz", + "integrity": "sha512-kUMOdEu3RP6ozH0Ga8OeMP8gSkBsK1UqZZKyPLFnpZHrtZuHSSt7M7gsHYB/bYQBZAo3o7qrGmRty3BubYtYxQ==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-1.1.0.tgz", + "integrity": "sha512-N22C9R44u5WGlcY+Wuv8EXmCAq62wWwriRAuoczMEwAIjPbvHSthyPSLqI4S7kAST1j6niWg8kwpeJ3ReAv3xg==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-1.1.0.tgz", + "integrity": "sha512-yiNKDGMzrQjnpnbLfkYKo+HwIxmBAsv0AI++QIJwvhfkLpUTBylelkv6oo78/YqZZS6h+bGfl0gILJsKE2wAKQ==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-1.1.0.tgz", + "integrity": "sha512-h2rXn68ClTwzPXYzEUNkz+0B/A0Hz8YdFNTiEwlxkwzkETGKMxmsrQGFXwYm3jd736R5vkXcClXz1ddKrsaBEQ==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-1.1.0.tgz", + "integrity": "sha512-twpQ/n+3OWZJ7Z+xu43MJErmhB/WO/mMTnqR6PwWQShvSJ/emx5d1N59LQZk6ZpTAeuRWrc+eHhkzTp9NFjNRQ==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-1.1.0.tgz", + "integrity": "sha512-iNxwhZ7Xc5+LjeDElEOi/Nh8fFsc9Dw9+5w7h7/GLFIU0RgAwBJuJtcP1vNTOwzW4B3hG+gRu8sQLqA9OEaTwA==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-1.1.0.tgz", + "integrity": "sha512-PvpazNjVpxX2ICrzoFYCpFnjB39DKCpZds8lRpAB3p6HGrx6QHBaNvOzVhJGBf0jcAbfCdc5/W0n9z8VWaSSww==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-1.1.0.tgz", + "integrity": "sha512-lINKYxIvT+W20YFOtHBKeGm7npuJg0/YCoShttU7fVpsmU+a2rdb9zrJn1MHqWfUL6DhTAWGa0tH2O7l4XrDcw==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-1.1.0.tgz", + "integrity": "sha512-RiBMxhxuO9VTjHsjJvhzViyceoLhU6gtrnJGpAXY43wE49IstXIGEQz8MT50/hOq5EumX16FCpup0r5DVyfqNQ==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-1.1.0.tgz", + "integrity": "sha512-XynYiIvXNea2BbLcppvpNK0zu8o2woJqgnmxqYTn4FWagH/Hr2QIk8LOsUz7BIJ4tooFhmx8urHKCdlPbbPDCA==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-1.1.0.tgz", + "integrity": "sha512-2G4TlzUnmTrUY26VKTonQqydwb+gtM/mcl+TqDP8CnWtJKVL8ElPpKgLGScP04bPIRY9x2/10lDdoaRXDqPuCw==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-1.1.0.tgz", + "integrity": "sha512-d3kRriEgaIiGXLziAM8bjnaLn1fthCJeTLZIwEIpzQqe6yPX0a+yQoLCTyjb2fvdLwkMoG4p7THIIB5cj5lkbg==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-1.2.0.tgz", + "integrity": "sha512-qlJd9gT751i4T0t/hJAyNGfESfi08Fek8QiLcysoKPgR05qHhG0OYhlaCJHhpXy4ECW0lHyjvFM1smrCLIXVfw==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-1.2.0.tgz", + "integrity": "sha512-GfGfruksi3nXdFok5RhgtOnWe5f6BndzYfmEXISD+5gAGdayFGpjWu5pIqIweTudMtse20bGbc+7MFZXT1Tb8Q==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-1.1.0.tgz", + "integrity": "sha512-gDEi4LxIGLbdfjrjiY45QNbuDmpkwh9DX4xzrR2AzjjXpxwGyfSpbJaYhXARw9p17VH0h9UewnNQXNwaQyYMDA==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-1.1.0.tgz", + "integrity": "sha512-Lm/FZu2qW3XX+kZ4WPwr+7aAeHf1Lm84UjNkKyBu16XbmEV7ukfhXni2aIwS2rcVf8Yv5E7wchGGpOFldj9V4Q==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-1.1.0.tgz", + "integrity": "sha512-OCTEeJ1igatd5kFrS2VDlYbainNNpf7Lj1siFOxnRWqYOP9oNvC5HOJBd3t+Z8MbrmehBtuDJ2QqeBsfeiNkww==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/shared-ini-file-loader": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-1.1.0.tgz", + "integrity": "sha512-S/v33zvCWzFyGZGlsEF0XsZtNNR281UhR7byk3nRfsgw5lGpg51rK/zjMgulM+h6NSuXaFILaYrw1I1v4kMcuA==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-1.1.0.tgz", + "integrity": "sha512-j32SGgVhv2G9nBTmel9u3OXux8KG20ssxuFakJrEeDug3kqbl1qrGzVLCe+Eib402UDtA0Sp1a4NZ2SEXDBxag==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-1.2.0.tgz", + "integrity": "sha512-z1r00TvBqF3dh4aHhya7nz1HhvCg4TRmw51fjMrh5do3h+ngSstt/yKlNbHeb9QxJmFbmN8KEVSWgb1bRvfEoA==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/url-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-1.1.0.tgz", + "integrity": "sha512-tpvi761kzboiLNGEWczuybMPCJh6WHB3cz9gWAG95mSyaKXmmX8ZcMxoV+irZfxDqLwZVJ22XTumu32S7Ow8aQ==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-1.1.0.tgz", + "integrity": "sha512-FpYmDmVbOXAxqvoVCwqehUN0zXS+lN8V7VS9O7I8MKeVHdSTsZzlwiMEvGoyTNOXWn8luF4CTDYgNHnZViR30g==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-1.1.0.tgz", + "integrity": "sha512-cep3ioRxzRZ2Jbp3Kly7gy6iNVefYXiT6ETt8W01RQr3uwi1YMkrbU1p3lMR4KhX/91Nrk6UOgX1RH+oIt48RQ==", + "dependencies": { + "tslib": "^2.5.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-body-length-node": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-1.1.0.tgz", + "integrity": "sha512-fRHRjkUuT5em4HZoshySXmB1n3HAU7IS232s+qU4TicexhyGJpXMK/2+c56ePOIa1FOK2tV1Q3J/7Mae35QVSw==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-1.1.0.tgz", + "integrity": "sha512-9m6NXE0ww+ra5HKHCHig20T+FAwxBAm7DIdwc/767uGWbRcY720ybgPacQNB96JMOI7xVr/CDa3oMzKmW4a+kw==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-1.1.0.tgz", + "integrity": "sha512-rQ47YpNmF6Is4I9GiE3T3+0xQ+r7RKRKbmHYyGSbyep/0cSf9kteKcI0ssJTvveJ1K4QvwrxXj1tEFp/G2UqxQ==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-1.1.0.tgz", + "integrity": "sha512-0bWhs1e412bfC5gwPCMe8Zbz0J8UoZ/meEQdo6MYj8Ne+c+QZ+KxVjx0a1dFYOclvM33SslL9dP0odn8kfblkg==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-1.1.0.tgz", + "integrity": "sha512-440e25TUH2b+TeK5CwsjYFrI9ShVOgA31CoxCKiv4ncSK4ZM68XW5opYxQmzMbRWARGEMu2XEUeBmOgMU2RLsw==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-1.1.0.tgz", + "integrity": "sha512-7UtIE9eH0u41zpB60Jzr0oNCQ3hMJUabMcKRUVjmyHTXiWDE4vjSqN6qlih7rCNeKGbioS7f/y2Jgym4QZcKFg==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-middleware": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-1.1.0.tgz", + "integrity": "sha512-6hhckcBqVgjWAqLy2vqlPZ3rfxLDhFWEmM7oLh2POGvsi7j0tHkbN7w4DFhuBExVJAbJ/qqxqZdRY6Fu7/OezQ==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-retry": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-1.1.0.tgz", + "integrity": "sha512-ygQW5HBqYXpR3ua09UciS0sL7UGJzGiktrKkOuEJwARoUuzz40yaEGU6xd9Gs7KBmAaFC8gMfnghHtwZ2nyBCQ==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-1.1.0.tgz", + "integrity": "sha512-w3lsdGsntaLQIrwDWJkIFKrFscgZXwU/oxsse09aSTNv5TckPhDeYea3LhsDrU5MGAG3vprhVZAKr33S45coVA==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-1.1.0.tgz", + "integrity": "sha512-/jL/V1xdVRt5XppwiaEU8Etp5WHZj609n0xMTuehmCqdoOFbId1M+aEeDWZsQ+8JbEB/BJ6ynY2SlYmOaKtt8w==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/@smithy/util-utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-1.1.0.tgz", + "integrity": "sha512-p/MYV+JmqmPyjdgyN2UxAeYDj9cBqCjp0C/NsTWnnjoZUVqoeZ6IrW915L9CAKWVECgv9lVQGc4u/yz26/bI1A==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-1.1.0.tgz", + "integrity": "sha512-S6FNIB3UJT+5Efd/0DeziO5Rs82QAMODHW4v2V3oNRrwaBigY/7Yx3SiLudZuF9WpVsV08Ih3BjIH34nzZiinQ==", + "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", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz", + "integrity": "sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==", + "funding": [ + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + }, + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@amzn/sagemaker-client/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", "license": "Apache-2.0", @@ -89,6 +1052,19 @@ "tslib": "^2.6.2" } }, + "node_modules/@aws-crypto/ie11-detection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-3.0.0.tgz", + "integrity": "sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==", + "dependencies": { + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/ie11-detection/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/@aws-crypto/sha1-browser": { "version": "5.2.0", "license": "Apache-2.0", @@ -101,49 +1077,8535 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-crypto/sha256-browser": { - "version": "5.2.0", - "license": "Apache-2.0", + "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", + "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", + "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", + "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", + "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", + "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", + "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", + "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", + "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", + "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", + "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-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", + "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/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", + "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/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", + "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/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, + "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", + "peer": true, + "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", + "peer": true, + "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", + "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", + "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, + "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", + "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/@aws-sdk/client-sso-oidc/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", + "@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", + "peer": true, + "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", + "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", + "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", + "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", + "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, + "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", + "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", + "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, + "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", + "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/@aws-sdk/client-sso-oidc/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", + "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", + "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", + "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, + "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", + "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/@aws-sdk/client-sso-oidc/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" + }, + "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, + "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, + "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", + "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/@aws-sdk/client-sso-oidc/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" + }, + "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, + "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", + "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", + "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, + "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", + "peer": true, + "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", + "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/@aws-sdk/client-sso-oidc/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", + "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, + "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", + "peer": true, + "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", + "peer": true, + "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", + "peer": true, + "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", + "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", + "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, + "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", + "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/@aws-sdk/client-sso-oidc/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", + "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", + "peer": true, + "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", + "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/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, + "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, + "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", + "peer": true, + "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, + "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", + "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", + "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, + "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", + "peer": true, + "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, + "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", + "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", + "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", + "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", + "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, + "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", + "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", + "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", + "peer": true, + "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", + "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/@aws-sdk/client-sso/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", + "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", + "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", + "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_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/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" + }, + "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", + "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/@aws-sdk/client-sso/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/@aws-sdk/client-sso/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", + "@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", + "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/@aws-sdk/client-sso/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" + }, + "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", + "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/@aws-sdk/client-sso/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", + "@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", + "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", + "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", + "peer": true, + "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", + "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/@aws-sdk/client-sso/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", + "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", + "peer": true, + "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", + "peer": true, + "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", + "peer": true, + "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", + "peer": true, + "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", + "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", + "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", + "peer": true, + "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", + "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/@aws-sdk/client-sso/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", + "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", + "peer": true, + "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", + "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" + }, + "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", + "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/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/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" + }, + "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", + "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_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", + "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", + "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", + "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/@aws-sdk/credential-provider-env/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" + }, + "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", + "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/@aws-sdk/credential-provider-env/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/@aws-sdk/credential-provider-env/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" + }, + "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", + "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/@aws-sdk/credential-provider-env/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", + "@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", + "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", + "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", + "peer": true, + "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", + "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/@aws-sdk/credential-provider-env/node_modules/@smithy/util-body-length-browser": { + "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/@aws-sdk/credential-provider-env/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" + }, + "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", + "peer": true, + "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", + "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", + "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", + "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/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/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" + }, + "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", + "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_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", + "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", + "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", + "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", + "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", + "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/@aws-sdk/credential-provider-http/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" + }, + "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", + "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/@aws-sdk/credential-provider-http/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", + "@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", + "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/@aws-sdk/credential-provider-http/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" + }, + "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", + "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/@aws-sdk/credential-provider-http/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", + "@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", + "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", + "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", + "peer": true, + "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", + "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/@aws-sdk/credential-provider-http/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", + "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", + "peer": true, + "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", + "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/@aws-sdk/credential-provider-http/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" + }, + "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", + "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", + "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", + "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/@aws-sdk/credential-provider-node/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/@aws-sdk/credential-provider-node/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/@aws-sdk/credential-provider-node/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/@aws-sdk/credential-provider-process": { + "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/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", + "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/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/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" + }, + "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", + "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_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", + "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", + "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", + "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/@aws-sdk/credential-provider-process/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" + }, + "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", + "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/@aws-sdk/credential-provider-process/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/@aws-sdk/credential-provider-process/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" + }, + "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", + "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/@aws-sdk/credential-provider-process/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", + "@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", + "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", + "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", + "peer": true, + "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", + "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/@aws-sdk/credential-provider-process/node_modules/@smithy/util-body-length-browser": { + "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/@aws-sdk/credential-provider-process/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" + }, + "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", + "peer": true, + "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", + "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", + "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", + "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/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { + "version": "3.758.0", + "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", + "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", + "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/@aws-sdk/credential-provider-sso/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", + "@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", + "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", + "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", + "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/@aws-sdk/credential-provider-sso/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" + }, + "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", + "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/@aws-sdk/credential-provider-sso/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/@aws-sdk/credential-provider-sso/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" + }, + "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", + "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/@aws-sdk/credential-provider-sso/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", + "@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", + "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", + "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" + }, + "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", + "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/@aws-sdk/credential-provider-sso/node_modules/@smithy/util-body-length-browser": { + "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/@aws-sdk/credential-provider-sso/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" + }, + "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" + }, + "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", + "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", + "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-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", + "@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", + "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", + "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", + "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", + "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", + "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/util-stream/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-stream/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", + "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", + "peer": true, + "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", + "peer": true, + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.730.0.tgz", + "integrity": "sha512-iJt2pL6RqWg7R3pja1WfcC2+oTjwaKFYndNE9oUQqyc6RN24XWUtGy9JnWqTUOy8jYzaP2eoF00fGeasSBX+Dw==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.730.0.tgz", + "integrity": "sha512-mI8kqkSuVlZklewEmN7jcbBMyVODBld3MsTjCKSl5ztduuPX69JD7nXLnWWPkw1PX4aGTO24AEoRMGNxntoXUg==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.730.0.tgz", + "integrity": "sha512-jonKyR+2GcqbZj2WDICZS0c633keLc9qwXnePu83DfAoFXMMIMyoR/7FOGf8F3OrIdGh8KzE9VvST+nZCK9EJA==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.730.0.tgz", + "integrity": "sha512-fFXgo3jBXLWqu8I07Hd96mS7RjrtpDgm3bZShm0F3lKtqDQF+hObFWq9A013SOE+RjMLVfbABhToXAYct3FcBw==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.730.0.tgz", + "integrity": "sha512-1aF3elbCzpVhWLAuV63iFElfLOqLGGTp4fkf2VAFIDO3hjshpXUQssTgIWiBwwtJYJdOSxaFrCU7u8frjr/5aQ==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.730.0.tgz", + "integrity": "sha512-zwsxkBuQuPp06o45ATAnznHzj3+ibop/EaTytNzSv0O87Q59K/jnS/bdtv1n6bhe99XCieRNTihvtS7YklzK7A==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.730.0.tgz", + "integrity": "sha512-ztRjh1edY7ut2wwrj1XqHtqPY/NXEYIk5fYf04KKsp8zBi81ScVqP7C+Cst6PFKixjgLSG6RsqMx9GSAalVv0Q==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.730.0.tgz", + "integrity": "sha512-cNKUQ81eptfZN8MlSqwUq3+5ln8u/PcY57UmLZ+npxUHanqO1akpgcpNsLpmsIkoXGbtSQrLuDUgH86lS/SWOw==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.730.0.tgz", + "integrity": "sha512-SdI2xrTbquJLMxUh5LpSwB8zfiKq3/jso53xWRgrVfeDlrSzZuyV6QghaMs3KEEjcNzwEnTfSIjGQyRXG9VrEw==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.730.0.tgz", + "integrity": "sha512-l5vdPmvF/d890pbvv5g1GZrdjaSQkyPH/Bc8dO/ZqkWxkIP8JNgl48S2zgf4DkP3ik9K2axWO828L5RsMDQzdA==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.723.0.tgz", + "integrity": "sha512-LLVzLvk299pd7v4jN9yOSaWDZDfH0SnBPb6q+FDPaOCMGBY8kuwQso7e/ozIKSmZHRMGO3IZrflasHM+rI+2YQ==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.723.0.tgz", + "integrity": "sha512-chASQfDG5NJ8s5smydOEnNK7N0gDMyuPbx7dYYcm1t/PKtnVfvWF+DHCTrRC2Ej76gLJVCVizlAJKM8v8Kg3cg==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.723.0.tgz", + "integrity": "sha512-7usZMtoynT9/jxL/rkuDOFQ0C2mhXl4yCm67Rg7GNTstl67u7w5WN1aIRImMeztaKlw8ExjoTyo6WTs1Kceh7A==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.730.0.tgz", + "integrity": "sha512-aPMZvNmf2a42B41au3bA3ODU4HfHka2nYT/SAIhhVXH1ENYfAmZo7FraFPxetKepFMCtL7j4QE6/LDucK6liIw==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.730.0.tgz", + "integrity": "sha512-vilIgf1/7kre8DdE5zAQkDOwHFb/TahMn/6j2RZwFLlK7cDk91r19deSiVYnKQkupDMtOfNceNqnorM4I3PDzw==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.723.0.tgz", + "integrity": "sha512-tGF/Cvch3uQjZIj34LY2mg8M2Dr4kYG8VU8Yd0dFnB1ybOEOveIK/9ypUo9ycZpB9oO6q01KRe5ijBaxNueUQg==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.730.0.tgz", + "integrity": "sha512-BSPssGj54B/AABWXARIPOT/1ybFahM1ldlfmXy9gRmZi/afe9geWJGlFYCCt3PmqR+1Ny5XIjSfue+kMd//drQ==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.723.0.tgz", + "integrity": "sha512-LmK3kwiMZG1y5g3LGihT9mNkeNOmwEyPk6HGcJqh0wOSV4QpWoKu2epyKE4MLQNUUlz2kOVbVbOrwmI6ZcteuA==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.730.0.tgz", + "integrity": "sha512-1KTFuVnk+YtLgWr6TwDiggcDqtPpOY2Cszt3r2lkXfaEAX6kHyOZi1vdvxXjPU5LsOBJem8HZ7KlkmrEi+xowg==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.723.0.tgz", + "integrity": "sha512-Wh9I6j2jLhNFq6fmXydIpqD1WyQLyTfSxjW9B+PXSnPyk3jtQW8AKQur7p97rO8LAUzVI0bv8kb3ZzDEVbquIg==", + "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", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.730.0.tgz", + "integrity": "sha512-yBvkOAjqsDEl1va4eHNOhnFBk0iCY/DBFNyhvtTMqPF4NO+MITWpFs3J9JtZKzJlQ6x0Yb9TLQ8NhDjEISz5Ug==", + "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", + "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-cognito-identity/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/core": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.5.3.tgz", + "integrity": "sha512-xa5byV9fEguZNofCclv6v9ra0FYh5FATQW/da7FQUVTic94DfrN/NvmKZjrMyzbpqfot9ZjBaO8U1UeTbmSLuA==", + "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", + "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-cognito-identity/node_modules/@smithy/fetch-http-handler": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.4.tgz", + "integrity": "sha512-AMtBR5pHppYMVD7z7G+OlHHAcgAN7v0kVKEpHuTO4Gb199Gowh0taYi9oDStFeUhetkeP55JLSVlTW1n9rFtUw==", + "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", + "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-cognito-identity/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/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-cognito-identity/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": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/middleware-endpoint": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.11.tgz", + "integrity": "sha512-zDogwtRLzKl58lVS8wPcARevFZNBOOqnmzWWxVe9XiaXU2CADFjvJ9XfNibgkOWs08sxLuSr81NrpY4mgp9OwQ==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.12.tgz", + "integrity": "sha512-wvIH70c4e91NtRxdaLZF+mbLZ/HcC6yg7ySKUiufL6ESp6zJUSnJucZ309AvG9nqCFHSRB5I6T3Ez1Q9wCh0Ww==", + "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", + "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/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/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", + "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/client-cognito-identity/node_modules/@smithy/node-http-handler": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz", + "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==", + "dependencies": { + "@smithy/abort-controller": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/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/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/client-cognito-identity/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-cognito-identity/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-cognito-identity/node_modules/@smithy/service-error-classification": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.5.tgz", + "integrity": "sha512-LvcfhrnCBvCmTee81pRlh1F39yTS/+kYleVeLCwNtkY8wtGg8V/ca9rbZZvYIl8OjlMtL6KIjaiL/lgVqHD2nA==", + "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", + "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/client-cognito-identity/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" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/smithy-client": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.3.tgz", + "integrity": "sha512-xxzNYgA0HD6ETCe5QJubsxP0hQH3QK3kbpJz3QrosBCuIWyEXLR/CO5hFb2OeawEKUxMNhz3a1nuJNN2np2RMA==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", + "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/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/client-cognito-identity/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": { + "@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", + "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": { + "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", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "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", + "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" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/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/client-cognito-identity/node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.19", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.19.tgz", + "integrity": "sha512-mvLMh87xSmQrV5XqnUYEPoiFFeEGYeAKIDDKdhE2ahqitm8OHM3aSvhqL6rrK6wm1brIk90JhxDf5lf2hbrLbQ==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.19.tgz", + "integrity": "sha512-8tYnx+LUfj6m+zkUUIrIQJxPM1xVxfRBvoGHua7R/i6qAxOMjqR6CpEpDwKoIs1o0+hOjGvkKE23CafKL0vJ9w==", + "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", + "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/client-cognito-identity/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" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/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": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-retry": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.5.tgz", + "integrity": "sha512-V7MSjVDTlEt/plmOFBn1762Dyu5uqMrV2Pl2X0dYk4XvWfdWJNe9Bs5Bzb56wkCuiWjSfClVMGcsuKrGj7S/yg==", + "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", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.2.tgz", + "integrity": "sha512-aI+GLi7MJoVxg24/3J1ipwLoYzgkB4kUfogZfnslcYlynj3xsQ0e7vk4TnTro9hhsS5PvX1mwmkRqqHQjwcU7w==", + "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", + "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-cognito-identity/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": { + "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": ">=18.0.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": { + "@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": ">=18.0.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": { + "@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": ">=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" + }, + "engines": { + "node": ">=18.0.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": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.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": { + "@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/@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/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-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": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "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" + }, + "engines": { + "node": ">=18.0.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": { + "@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-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": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.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": { + "@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": ">=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" + }, + "engines": { + "node": ">=18.0.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": { + "@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": ">=18.0.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": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.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": { + "@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-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": { + "@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/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": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.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": { + "tslib": "^2.6.2" + }, + "engines": { + "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" + }, + "engines": { + "node": ">=18.0.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": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.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": { + "@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-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": { + "@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": ">=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" + }, + "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" + }, + "engines": { + "node": ">=18.0.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": ">=18.0.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": { - "@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", + "@smithy/service-error-classification": "^4.0.6", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-crypto/sha256-js": { - "version": "5.2.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": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.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-crypto/supports-web-crypto": { - "version": "5.2.0", - "license": "Apache-2.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-crypto/util": { - "version": "5.2.0", - "license": "Apache-2.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": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", + "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway": { - "version": "3.693.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", @@ -155,7 +9617,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-api-gateway": "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", @@ -186,15 +9648,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", - "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-api-gateway/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": { @@ -241,7 +9705,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/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", "dependencies": { @@ -292,7 +9756,7 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-api-gateway/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", "dependencies": { @@ -341,7 +9805,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/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": { @@ -361,7 +9825,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/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": { @@ -380,7 +9844,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/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": { @@ -404,7 +9868,7 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-api-gateway/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": { @@ -425,7 +9889,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/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": { @@ -442,7 +9906,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/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": { @@ -459,7 +9923,7 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-api-gateway/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": { @@ -472,7 +9936,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/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": { @@ -484,7 +9948,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/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": { @@ -497,7 +9961,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/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": { @@ -513,7 +9977,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/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": { @@ -528,7 +9992,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/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": { @@ -545,7 +10009,7 @@ "@aws-sdk/client-sso-oidc": "^3.693.0" } }, - "node_modules/@aws-sdk/client-api-gateway/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": { @@ -558,7 +10022,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/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": { @@ -568,7 +10032,7 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/client-api-gateway/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": { @@ -590,7 +10054,7 @@ } } }, - "node_modules/@aws-sdk/client-api-gateway/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": { @@ -600,7 +10064,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/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": { @@ -611,7 +10075,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/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": { @@ -622,9 +10086,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner": { + "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", @@ -666,15 +10131,17 @@ "@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": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/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", @@ -719,9 +10186,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/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==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -770,9 +10238,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-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==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -819,9 +10288,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/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", @@ -839,9 +10309,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/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", @@ -858,9 +10329,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/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", @@ -882,9 +10354,10 @@ "@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-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", @@ -903,9 +10376,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/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", @@ -920,9 +10394,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/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", @@ -937,9 +10412,10 @@ "@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-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", @@ -950,9 +10426,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/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", @@ -962,9 +10439,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/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", @@ -975,9 +10453,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/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", @@ -988,692 +10467,1237 @@ "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-ecr/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==", + "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-ecr/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==", + "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-ecr/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==", + "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-ecr/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==", + "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-ecr/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==", + "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-ecr/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==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-ecr/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==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-ecr/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==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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.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-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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.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-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.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-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/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-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/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-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/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-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/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-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/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-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": { + "@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-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/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-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.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-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/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" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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": { + "@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-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": { + "@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-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": { + "@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-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": { + "@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-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.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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": { + "@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-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", + "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-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/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-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.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-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.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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/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-apprunner/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.693.0", - "license": "Apache-2.0", + "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": { - "@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" + "@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": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/token-providers": { - "version": "3.693.0", - "license": "Apache-2.0", + "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": { - "@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/protocol-http": "^5.1.2", + "@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-apprunner/node_modules/@aws-sdk/util-endpoints": { - "version": "3.693.0", - "license": "Apache-2.0", + "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": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", - "@smithy/util-endpoints": "^2.1.5", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.693.0", - "license": "Apache-2.0", + "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": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", - "bowser": "^2.11.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-apprunner/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-glue/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/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/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" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@smithy/is-array-buffer": { - "version": "3.0.0", - "license": "Apache-2.0", + "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/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@smithy/util-buffer-from": { - "version": "3.0.0", - "license": "Apache-2.0", + "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/is-array-buffer": "^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-apprunner/node_modules/@smithy/util-utf8": { - "version": "3.0.0", - "license": "Apache-2.0", + "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/util-buffer-from": "^3.0.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-cloudcontrol": { - "version": "3.693.0", - "license": "Apache-2.0", + "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": { - "@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" + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/client-sso": { - "version": "3.693.0", - "license": "Apache-2.0", + "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": { - "@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" + "@smithy/types": "^4.3.1" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.693.0", - "license": "Apache-2.0", + "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": { - "@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", + "@smithy/types": "^4.3.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-cloudcontrol/node_modules/@aws-sdk/client-sts": { - "version": "3.693.0", - "license": "Apache-2.0", + "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": { - "@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/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-cloudcontrol/node_modules/@aws-sdk/core": { - "version": "3.693.0", - "license": "Apache-2.0", + "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": { - "@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/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-cloudcontrol/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.693.0", - "license": "Apache-2.0", + "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": { - "@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": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.693.0", - "license": "Apache-2.0", + "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": { - "@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/querystring-parser": "^4.0.4", + "@smithy/types": "^4.3.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-cloudcontrol/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-glue/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/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/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-cloudcontrol/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.693.0", - "license": "Apache-2.0", + "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/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": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.693.0", - "license": "Apache-2.0", + "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/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-cloudcontrol/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.693.0", - "license": "Apache-2.0", + "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.692.0", - "@smithy/protocol-http": "^4.1.6", - "@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-cloudcontrol/node_modules/@aws-sdk/middleware-logger": { - "version": "3.693.0", - "license": "Apache-2.0", + "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.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-cloudcontrol/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.693.0", - "license": "Apache-2.0", + "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/types": "3.692.0", - "@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-cloudcontrol/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.693.0", - "license": "Apache-2.0", + "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/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/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-cloudcontrol/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.693.0", - "license": "Apache-2.0", + "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": { - "@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/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/token-providers": { - "version": "3.693.0", - "license": "Apache-2.0", + "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.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": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/util-endpoints": { - "version": "3.693.0", - "license": "Apache-2.0", + "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.692.0", - "@smithy/types": "^3.7.0", - "@smithy/util-endpoints": "^2.1.5", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.693.0", - "license": "Apache-2.0", + "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/types": "3.692.0", - "@smithy/types": "^3.7.0", - "bowser": "^2.11.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-cloudcontrol/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@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/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" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@smithy/is-array-buffer": { - "version": "3.0.0", - "license": "Apache-2.0", + "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": { "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@smithy/util-buffer-from": { - "version": "3.0.0", - "license": "Apache-2.0", + "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/is-array-buffer": "^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-cloudcontrol/node_modules/@smithy/util-utf8": { - "version": "3.0.0", - "license": "Apache-2.0", + "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": { - "@smithy/util-buffer-from": "^3.0.0", - "tslib": "^2.6.2" + "strnum": "^2.1.0" }, - "engines": { - "node": ">=16.0.0" + "bin": { + "fxparser": "src/cli/cli.js" } }, - "node_modules/@aws-sdk/client-cloudformation": { - "version": "3.682.0", + "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", "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", + "@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.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-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.6", - "@types/uuid": "^9.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "@smithy/util-waiter": "^3.1.8", + "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/client-sso": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-iam/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.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", + "@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.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-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" }, @@ -1681,47 +11705,47 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/client-sso-oidc": { + "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.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", + "@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.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-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" }, @@ -1729,51 +11753,51 @@ "node": ">=16.0.0" }, "peerDependencies": { - "@aws-sdk/client-sts": "^3.682.0" + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/client-sts": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/client-sts": { + "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.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", + "@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.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-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" }, @@ -1781,19 +11805,19 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/core": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/core": { + "version": "3.693.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", + "@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" }, @@ -1801,261 +11825,221 @@ "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", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.693.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", + "@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-cloudformation/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.693.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", + "@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.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" + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.693.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", + "@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-cloudformation/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.693.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", + "@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-cloudformation/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.693.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", + "@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.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" + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/middleware-logger": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.693.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/types": "^3.5.0", + "@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-cloudformation/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-logger": { + "version": "3.693.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/protocol-http": "^4.1.4", - "@smithy/types": "^3.5.0", + "@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-cloudformation/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.693.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", + "@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-cloudformation/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.693.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", + "@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-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", + "node_modules/@aws-sdk/client-iam/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" - }, - "peerDependencies": { - "@aws-sdk/client-sso-oidc": "^3.679.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/types": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/token-providers": { + "version": "3.693.0", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.5.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" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.693.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/util-endpoints": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-endpoints": { + "version": "3.693.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/types": "^3.5.0", - "@smithy/util-endpoints": "^2.1.3", + "@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-cloudformation/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.693.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/types": "^3.5.0", + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.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", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.693.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", + "@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": { @@ -2070,18 +12054,7 @@ } } }, - "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": { + "node_modules/@aws-sdk/client-iam/node_modules/@smithy/is-array-buffer": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { @@ -2091,7 +12064,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/util-buffer-from": { + "node_modules/@aws-sdk/client-iam/node_modules/@smithy/util-buffer-from": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { @@ -2102,7 +12075,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/util-utf8": { + "node_modules/@aws-sdk/client-iam/node_modules/@smithy/util-utf8": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { @@ -2113,52 +12086,51 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs": { - "version": "3.682.0", + "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", "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", + "@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.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-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", @@ -2168,46 +12140,48 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/client-sso": { - "version": "3.682.0", + "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", "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", + "@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.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-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" }, @@ -2215,47 +12189,49 @@ "node": ">=16.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-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", "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", + "@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.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-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" }, @@ -2263,51 +12239,53 @@ "node": ">=16.0.0" }, "peerDependencies": { - "@aws-sdk/client-sts": "^3.682.0" + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/client-sts": { - "version": "3.682.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", "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", + "@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.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-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" }, @@ -2315,19 +12293,21 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/core": { - "version": "3.679.0", + "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.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", + "@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" }, @@ -2335,261 +12315,249 @@ "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", + "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", "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/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-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.682.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", "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.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.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" + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.682.0", + "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", "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/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-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.679.0", + "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", "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/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" - }, - "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", + "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", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/protocol-http": "^4.1.4", - "@smithy/types": "^3.5.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": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/middleware-logger": { - "version": "3.679.0", + "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", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/types": "^3.5.0", + "@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-cloudwatch-logs/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.679.0", + "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", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/protocol-http": "^4.1.4", - "@smithy/types": "^3.5.0", + "@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-cloudwatch-logs/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.682.0", + "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", "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", + "@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-cloudwatch-logs/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.679.0", + "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", "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", + "@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-cloudwatch-logs/node_modules/@aws-sdk/token-providers": { - "version": "3.679.0", + "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", "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", + "@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" - }, - "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", + "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", "dependencies": { - "@smithy/types": "^3.5.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" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.693.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/util-endpoints": { - "version": "3.679.0", + "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", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/types": "^3.5.0", - "@smithy/util-endpoints": "^2.1.3", + "@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-cloudwatch-logs/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.679.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", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/types": "^3.5.0", + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.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", + "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", "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", + "@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": { @@ -2604,51 +12572,100 @@ } } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@smithy/fetch-http-handler": { - "version": "3.2.9", + "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", "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" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@smithy/is-array-buffer": { + "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", "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-buffer-from": { + "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", "dependencies": { - "@smithy/is-array-buffer": "^3.0.0", + "@smithy/util-buffer-from": "^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", + "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", "dependencies": { - "@smithy/util-buffer-from": "^3.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", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst": { + "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", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -2659,7 +12676,6 @@ "@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", @@ -2689,2193 +12705,3096 @@ "@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" + "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso": { - "version": "3.758.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": { "@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", + "@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-oidc": { - "version": "3.758.0", + "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": { "@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", + "@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-oidc/node_modules/@aws-sdk/core": { - "version": "3.758.0", + "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": { - "@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", + "@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/client-sso-oidc/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.734.0", + "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": { - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", - "@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-oidc/node_modules/@aws-sdk/middleware-logger": { - "version": "3.734.0", + "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": { - "@aws-sdk/types": "3.734.0", - "@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-oidc/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.734.0", + "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": { - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.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-oidc/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.758.0", + "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": { - "@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/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-oidc/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.734.0", + "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": { - "@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.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-oidc/node_modules/@aws-sdk/types": { - "version": "3.734.0", + "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/client-sso-oidc/node_modules/@aws-sdk/util-endpoints": { - "version": "3.743.0", + "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": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "@smithy/util-endpoints": "^3.0.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-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.734.0", + "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": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "bowser": "^2.11.0", + "@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/client-sso-oidc/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.758.0", + "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": { - "@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/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" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "node": ">=16.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, - "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", + "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", + "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-oidc/node_modules/@smithy/core": { - "version": "3.1.5", + "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": { - "@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/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-oidc/node_modules/@smithy/fetch-http-handler": { - "version": "5.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/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/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-oidc/node_modules/@smithy/hash-node": { - "version": "4.0.1", + "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/types": "^4.1.0", - "@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/client-sso-oidc/node_modules/@smithy/invalid-dependency": { - "version": "4.0.1", + "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": { - "@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/client-sso-oidc/node_modules/@smithy/middleware-content-length": { - "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/protocol-http": "^5.0.1", - "@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/client-sso-oidc/node_modules/@smithy/middleware-endpoint": { - "version": "4.0.6", + "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/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/client-sso-oidc/node_modules/@smithy/middleware-retry": { - "version": "4.0.7", + "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": { - "@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/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-oidc/node_modules/@smithy/middleware-serde": { - "version": "4.0.2", + "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/client-sso-oidc/node_modules/@smithy/middleware-stack": { - "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/client-sso-oidc/node_modules/@smithy/node-config-provider": { - "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/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@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/client-sso-oidc/node_modules/@smithy/node-http-handler": { - "version": "4.0.3", + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/is-array-buffer": { + "version": "3.0.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", "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-oidc/node_modules/@smithy/property-provider": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/util-utf8": { + "version": "3.0.0", "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/client-sso-oidc/node_modules/@smithy/protocol-http": { - "version": "5.0.1", + "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": { - "@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/client-sso-oidc/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.1", + "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/client-sso-oidc/node_modules/@smithy/signature-v4": { - "version": "5.0.1", + "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/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" + "@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-oidc/node_modules/@smithy/smithy-client": { - "version": "4.1.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-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-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-oidc/node_modules/@smithy/types": { - "version": "4.1.0", + "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": { + "@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-oidc/node_modules/@smithy/url-parser": { - "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/querystring-parser": "^4.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-oidc/node_modules/@smithy/util-base64": { - "version": "4.0.0", + "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/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^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/client-sso-oidc/node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", + "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": { + "@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-oidc/node_modules/@smithy/util-body-length-node": { - "version": "4.0.0", + "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": { + "@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-oidc/node_modules/@smithy/util-config-provider": { - "version": "4.0.0", + "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": { + "@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-oidc/node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.0.7", + "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/property-provider": "^4.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "bowser": "^2.11.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-oidc/node_modules/@smithy/util-defaults-mode-node": { - "version": "4.0.7", + "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/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", + "@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-oidc/node_modules/@smithy/util-endpoints": { - "version": "3.0.1", + "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": { - "@smithy/node-config-provider": "^4.0.1", - "@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-oidc/node_modules/@smithy/util-middleware": { - "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/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-oidc/node_modules/@smithy/util-retry": { - "version": "4.0.1", + "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": { - "@smithy/service-error-classification": "^4.0.1", - "@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-oidc/node_modules/@smithy/util-utf8": { - "version": "4.0.0", + "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/util-buffer-from": "^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/client-sso/node_modules/@aws-sdk/core": { - "version": "3.758.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": { - "@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/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/@aws-sdk/middleware-host-header": { - "version": "3.734.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/types": "3.734.0", - "@smithy/protocol-http": "^5.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/@aws-sdk/middleware-logger": { - "version": "3.734.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/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/client-sso/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.734.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/types": "3.734.0", - "@smithy/protocol-http": "^5.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/client-sso/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.758.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": { - "@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/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/@aws-sdk/region-config-resolver": { - "version": "3.734.0", + "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": { - "@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": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/types": { - "version": "3.734.0", + "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/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/client-sso/node_modules/@aws-sdk/util-endpoints": { - "version": "3.743.0", + "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": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-buffer-from": "^3.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/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", - "bowser": "^2.11.0", - "tslib": "^2.6.2" + "node": ">=16.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", + "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": { - "@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-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" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/config-resolver": { - "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/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.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/core": { - "version": "3.1.5", + "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/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/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/fetch-http-handler": { - "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/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/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/hash-node": { - "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", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^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/client-sso/node_modules/@smithy/invalid-dependency": { - "version": "4.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/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-content-length": { - "version": "4.0.1", + "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/protocol-http": "^5.0.1", - "@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/middleware-endpoint": { - "version": "4.0.6", + "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/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/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/middleware-retry": { - "version": "4.0.7", + "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": { - "@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" + "@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/middleware-serde": { - "version": "4.0.2", + "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": ">=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-stack": { - "version": "4.0.1", + "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", - "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/node-config-provider": { - "version": "4.0.1", + "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", - "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/client-sso/node_modules/@smithy/node-http-handler": { - "version": "4.0.3", + "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", - "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/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/property-provider": { - "version": "4.0.1", + "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", - "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/client-sso/node_modules/@smithy/protocol-http": { - "version": "5.0.1", + "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", - "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/client-sso/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.1", + "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", - "peer": true, "dependencies": { - "@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/signature-v4": { - "version": "5.0.1", + "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", - "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/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/smithy-client": { - "version": "4.1.6", + "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", - "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", + "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/types": { - "version": "4.1.0", + "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", - "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/url-parser": { - "version": "4.0.1", + "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": { - "@smithy/querystring-parser": "^4.0.1", - "@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/client-sso/node_modules/@smithy/util-base64": { - "version": "4.0.0", + "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/util-buffer-from": "^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/@aws-sdk/client-sso/node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", + "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/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-body-length-node": { - "version": "4.0.0", + "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": { + "@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-config-provider": { - "version": "4.0.0", + "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": { + "@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-defaults-mode-browser": { - "version": "4.0.7", + "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/property-provider": "^4.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "bowser": "^2.11.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/util-defaults-mode-node": { - "version": "4.0.7", + "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/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", + "@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/client-sso/node_modules/@smithy/util-endpoints": { - "version": "3.0.1", + "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": { - "@smithy/node-config-provider": "^4.0.1", - "@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/util-middleware": { - "version": "4.0.1", + "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", - "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/util-retry": { - "version": "4.0.1", + "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", - "peer": true, "dependencies": { - "@smithy/service-error-classification": "^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/util-utf8": { - "version": "4.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", - "peer": true, "dependencies": { - "@smithy/util-buffer-from": "^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/core": { + "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-sdk/core": "3.693.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": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.758.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", - "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", + "@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/@aws-sdk/core": { - "version": "3.758.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", - "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", "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-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", - "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/core": { - "version": "3.1.5", + "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", - "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/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/middleware-endpoint": { - "version": "4.0.6", + "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", - "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/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/middleware-serde": { - "version": "4.0.2", + "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", - "peer": true, "dependencies": { - "@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-env/node_modules/@smithy/middleware-stack": { - "version": "4.0.1", + "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", - "peer": true, "dependencies": { - "@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-env/node_modules/@smithy/node-config-provider": { - "version": "4.0.1", + "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", - "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", + "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/property-provider": { - "version": "4.0.1", + "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", - "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-env/node_modules/@smithy/protocol-http": { - "version": "5.0.1", + "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", - "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-env/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.1", + "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", - "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/signature-v4": { - "version": "5.0.1", + "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", - "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/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-env/node_modules/@smithy/smithy-client": { - "version": "4.1.6", + "node_modules/@aws-sdk/client-s3": { + "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-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-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/types": { - "version": "4.1.0", - "license": "Apache-2.0", - "peer": true, + "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": { - "tslib": "^2.6.2" + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.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.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", + "@types/uuid": "^9.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/credential-provider-env/node_modules/@smithy/url-parser": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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": { - "@smithy/querystring-parser": "^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.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-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "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/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-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/util-middleware": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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.1.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-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "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": { - "@smithy/util-buffer-from": "^4.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-codecatalyst/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "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/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-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" } }, - "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", - "peer": true, + "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/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/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" }, "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", - "peer": true, + "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/types": "^4.1.0", + "@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", "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", - "peer": true, + "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.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/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", "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", - "peer": true, + "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/protocol-http": "^5.0.1", - "@smithy/querystring-builder": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-base64": "^4.0.0", + "@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", "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", - "peer": true, + "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": { - "@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.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/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/middleware-serde": { - "version": "4.0.2", - "license": "Apache-2.0", - "peer": true, + "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": { - "@smithy/types": "^4.1.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/credential-provider-http/node_modules/@smithy/middleware-stack": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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": { - "@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-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/node-config-provider": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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": { - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.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/credential-provider-http/node_modules/@smithy/node-http-handler": { - "version": "4.0.3", - "license": "Apache-2.0", - "peer": true, + "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": { - "@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.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" }, "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", - "peer": true, + "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/types": "^4.1.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-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/protocol-http": { - "version": "5.0.1", - "license": "Apache-2.0", - "peer": true, + "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": { - "@smithy/types": "^4.1.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/credential-provider-http/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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/types": "^4.1.0", + "@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" }, "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", - "peer": true, + "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/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/credential-provider-http/node_modules/@smithy/smithy-client": { - "version": "4.1.6", - "license": "Apache-2.0", - "peer": true, + "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/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", - "peer": true, + "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": { + "@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/credential-provider-http/node_modules/@smithy/url-parser": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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": { - "@smithy/querystring-parser": "^4.0.1", - "@smithy/types": "^4.1.0", + "@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-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/util-base64": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "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/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/credential-provider-http/node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-s3-control/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-codecatalyst/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/util-middleware": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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.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": { + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/chunked-blob-reader-native": { "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.0.0.tgz", + "integrity": "sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig==", "dependencies": { - "@smithy/util-buffer-from": "^4.0.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-node": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "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": { - "@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/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/credential-provider-node/node_modules/@aws-sdk/types": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "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/types": "^4.1.0", + "@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-codecatalyst/node_modules/@aws-sdk/credential-provider-node/node_modules/@smithy/property-provider": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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/types": "^4.1.0", + "@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/credential-provider-node/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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/types": "^4.1.0", + "@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/credential-provider-node/node_modules/@smithy/types": { - "version": "4.1.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/hash-blob-browser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.0.4.tgz", + "integrity": "sha512-WszRiACJiQV3QG6XMV44i5YWlkrlsM5Yxgz4jvsksuu7LDXA6wAtypfPajtNTadzpJy3KyJPoWehYpmZGKUFIQ==", "dependencies": { + "@smithy/chunked-blob-reader": "^5.0.0", + "@smithy/chunked-blob-reader-native": "^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/credential-provider-process": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "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": { - "@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/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/credential-provider-process/node_modules/@aws-sdk/core": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "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": { - "@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/types": "^4.3.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/credential-provider-process/node_modules/@aws-sdk/types": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "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.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/credential-provider-process/node_modules/@smithy/core": { - "version": "3.1.5", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-s3-control/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/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", - "peer": true, + "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/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/types": "^4.3.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/credential-provider-process/node_modules/@smithy/middleware-serde": { - "version": "4.0.2", - "license": "Apache-2.0", - "peer": true, + "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/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/credential-provider-process/node_modules/@smithy/middleware-stack": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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/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/credential-provider-process/node_modules/@smithy/node-config-provider": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@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/credential-provider-process/node_modules/@smithy/property-provider": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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/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/credential-provider-process/node_modules/@smithy/protocol-http": { - "version": "5.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-s3-control/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/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/credential-provider-process/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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/types": "^4.1.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/credential-provider-process/node_modules/@smithy/signature-v4": { - "version": "5.0.1", - "license": "Apache-2.0", - "peer": true, + "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/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/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/credential-provider-process/node_modules/@smithy/smithy-client": { - "version": "4.1.6", - "license": "Apache-2.0", - "peer": true, + "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/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/types": "^4.3.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/types": { - "version": "4.1.0", - "license": "Apache-2.0", - "peer": true, + "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/types": "^4.3.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/url-parser": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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": { - "@smithy/querystring-parser": "^4.0.1", - "@smithy/types": "^4.1.0", + "@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/credential-provider-process/node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "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": { + "@smithy/types": "^4.3.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/util-middleware": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" + "@smithy/types": "^4.3.1" }, "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", - "peer": true, + "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": { - "@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-codecatalyst/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "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/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/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/credential-provider-sso/node_modules/@aws-sdk/core": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "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/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/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/credential-provider-sso/node_modules/@aws-sdk/token-providers": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "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/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", - "peer": true, + "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": { - "@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/credential-provider-sso/node_modules/@smithy/core": { - "version": "3.1.5", - "license": "Apache-2.0", - "peer": true, + "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": { - "@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-buffer-from": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, @@ -4883,161 +15802,154 @@ "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", - "peer": true, + "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": { - "@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", - "peer": true, + "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": { - "@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", - "peer": true, + "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": { - "@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/credential-provider-sso/node_modules/@smithy/node-config-provider": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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": { - "@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", - "peer": true, + "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": { - "@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/credential-provider-sso/node_modules/@smithy/protocol-http": { - "version": "5.0.1", - "license": "Apache-2.0", - "peer": true, + "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": { - "@smithy/types": "^4.1.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.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/credential-provider-sso/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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": { - "@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/credential-provider-sso/node_modules/@smithy/signature-v4": { - "version": "5.0.1", - "license": "Apache-2.0", - "peer": true, + "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": { - "@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", - "peer": true, + "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": { - "@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/types": "^4.3.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/types": { - "version": "4.1.0", - "license": "Apache-2.0", - "peer": true, + "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": { + "@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-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/url-parser": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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/querystring-parser": "^4.0.1", - "@smithy/types": "^4.1.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": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/util-body-length-browser": { + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/util-uri-escape": { "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "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" }, @@ -5045,104 +15957,244 @@ "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", - "peer": true, + "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/types": "^4.1.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/credential-provider-sso/node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "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": { - "@smithy/util-buffer-from": "^4.0.0", - "tslib": "^2.6.2" + "strnum": "^2.1.0" }, - "engines": { - "node": ">=18.0.0" + "bin": { + "fxparser": "src/cli/cli.js" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/middleware-host-header": { + "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": { + "@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-codecatalyst/node_modules/@aws-sdk/middleware-logger": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso-oidc": { "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/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-codecatalyst/node_modules/@aws-sdk/middleware-recursion-detection": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sts": { "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/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-codecatalyst/node_modules/@aws-sdk/middleware-user-agent": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/core": { "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/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/region-config-resolver": { + "node_modules/@aws-sdk/client-s3/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/node-config-provider": "^3.1.10", + "@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-config-provider": "^3.0.0", - "@smithy/util-middleware": "^3.0.9", + "@smithy/util-stream": "^3.3.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/token-providers": { + "node_modules/@aws-sdk/client-s3/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", @@ -5152,39 +16204,54 @@ "node": ">=16.0.0" }, "peerDependencies": { - "@aws-sdk/client-sso-oidc": "^3.693.0" + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/util-endpoints": { + "node_modules/@aws-sdk/client-s3/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", - "@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": { + "node_modules/@aws-sdk/client-s3/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", - "bowser": "^2.11.0", "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/util-user-agent-node": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-web-identity": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/core": "3.693.0", "@aws-sdk/types": "3.692.0", - "@smithy/node-config-provider": "^3.1.10", + "@smithy/property-provider": "^3.1.9", "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, @@ -5192,430 +16259,663 @@ "node": ">=16.0.0" }, "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/abort-controller": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-s3/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/@smithy/abort-controller/node_modules/@smithy/types": { - "version": "4.1.0", + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-logger": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { + "@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/@smithy/credential-provider-imds": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.693.0", "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", + "@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/@smithy/credential-provider-imds/node_modules/@smithy/node-config-provider": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-user-agent": { + "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/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/@smithy/credential-provider-imds/node_modules/@smithy/property-provider": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.693.0", "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/@smithy/credential-provider-imds/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/token-providers": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@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/@smithy/credential-provider-imds/node_modules/@smithy/types": { - "version": "4.1.0", + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-endpoints": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { + "@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/@smithy/credential-provider-imds/node_modules/@smithy/url-parser": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-browser": { + "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/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/@smithy/is-array-buffer": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-s3/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/@smithy/querystring-builder": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-s3/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", "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": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/querystring-builder/node_modules/@smithy/types": { - "version": "4.1.0", + "node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { + "@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/querystring-parser": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-utf8": { + "version": "3.0.0", "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/@smithy/querystring-parser/node_modules/@smithy/types": { - "version": "4.1.0", + "node_modules/@aws-sdk/client-sagemaker": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sagemaker/-/client-sagemaker-3.693.0.tgz", + "integrity": "sha512-iInrrb7V9f0CRBiVCaaxCbpoBRQ5BqxX4elRYI6gE/pSDD2tPqmRfm4reahMtTUcKg1jaSGuvqJLfOpp0HTozQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "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", + "@smithy/util-waiter": "^3.1.8", + "@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/@smithy/service-error-classification": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-sagemaker/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/@smithy/service-error-classification/node_modules/@smithy/types": { - "version": "4.1.0", + "node_modules/@aws-sdk/client-sagemaker/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": { + "@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/@smithy/util-buffer-from": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-sagemaker/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/is-array-buffer": "^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-hex-encoding": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-sagemaker/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": { + "@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-stream": { - "version": "4.1.2", + "node_modules/@aws-sdk/client-sagemaker/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/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", + "@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-stream/node_modules/@smithy/fetch-http-handler": { - "version": "5.0.1", + "node_modules/@aws-sdk/client-sagemaker/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/protocol-http": "^5.0.1", - "@smithy/querystring-builder": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-base64": "^4.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": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.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-sagemaker/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/abort-controller": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/querystring-builder": "^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/@smithy/util-stream/node_modules/@smithy/protocol-http": { - "version": "5.0.1", + "node_modules/@aws-sdk/client-sagemaker/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/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/@smithy/util-stream/node_modules/@smithy/types": { - "version": "4.1.0", + "node_modules/@aws-sdk/client-sagemaker/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": { + "@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/@smithy/util-stream/node_modules/@smithy/util-base64": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-sagemaker/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/util-buffer-from": "^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/@smithy/util-stream/node_modules/@smithy/util-utf8": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-sagemaker/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/util-buffer-from": "^4.0.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/@smithy/util-uri-escape": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-sagemaker/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/@smithy/util-utf8": { - "version": "3.0.0", + "node_modules/@aws-sdk/client-sagemaker/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": { - "@smithy/util-buffer-from": "^3.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": ">=16.0.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-sagemaker/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", + "@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/@smithy/util-utf8/node_modules/@smithy/util-buffer-from": { - "version": "3.0.0", + "node_modules/@aws-sdk/client-sagemaker/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": { - "@smithy/is-array-buffer": "^3.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": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.693.0" } }, - "node_modules/@aws-sdk/client-cognito-identity": { - "version": "3.637.0", + "node_modules/@aws-sdk/client-sagemaker/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-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/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.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-cognito-identity/node_modules/@aws-sdk/types": { - "version": "3.609.0", + "node_modules/@aws-sdk/client-sagemaker/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": { - "@smithy/types": "^3.3.0", + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "bowser": "^2.11.0", "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/fetch-http-handler": { - "version": "3.2.4", + "node_modules/@aws-sdk/client-sagemaker/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": { - "@smithy/protocol-http": "^4.1.0", - "@smithy/querystring-builder": "^3.0.3", - "@smithy/types": "^3.3.0", - "@smithy/util-base64": "^3.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": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/is-array-buffer": { + "node_modules/@aws-sdk/client-sagemaker/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": { "tslib": "^2.6.2" @@ -5624,30 +16924,36 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-utf8": { + "node_modules/@aws-sdk/client-sagemaker/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": { - "@smithy/util-buffer-from": "^3.0.0", + "@smithy/is-array-buffer": "^3.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/util-utf8/node_modules/@smithy/util-buffer-from": { + "node_modules/@aws-sdk/client-sagemaker/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": { - "@smithy/is-array-buffer": "^3.0.0", + "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-ec2": { - "version": "3.695.0", + "node_modules/@aws-sdk/client-sfn": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sfn/-/client-sfn-3.693.0.tgz", + "integrity": "sha512-B2K3aXGnP7eD1ITEIx4kO43l1N5OLqHdLW4AUbwoopwU5qzicc9jADrthXpGxymJI8AhJz9T2WtLmceBU2EpNg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -5659,7 +16965,6 @@ "@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", @@ -5691,7 +16996,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" @@ -5700,8 +17004,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/client-sso": { + "node_modules/@aws-sdk/client-sfn/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", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -5747,8 +17053,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/client-sso-oidc": { + "node_modules/@aws-sdk/client-sfn/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", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -5798,8 +17106,10 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/client-sts": { + "node_modules/@aws-sdk/client-sfn/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", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -5847,8 +17157,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/core": { + "node_modules/@aws-sdk/client-sfn/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", @@ -5867,8 +17179,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/credential-provider-http": { + "node_modules/@aws-sdk/client-sfn/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", "dependencies": { "@aws-sdk/core": "3.693.0", @@ -5886,8 +17200,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/credential-provider-ini": { + "node_modules/@aws-sdk/client-sfn/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": { "@aws-sdk/core": "3.693.0", @@ -5910,8 +17226,10 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/credential-provider-node": { + "node_modules/@aws-sdk/client-sfn/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", @@ -5931,8 +17249,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/credential-provider-sso": { + "node_modules/@aws-sdk/client-sfn/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": { "@aws-sdk/client-sso": "3.693.0", @@ -5948,8 +17268,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-ec2/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", + "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-sdk/core": "3.693.0", @@ -5965,8 +17287,10 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/middleware-host-header": { + "node_modules/@aws-sdk/client-sfn/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", @@ -5978,8 +17302,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/middleware-logger": { + "node_modules/@aws-sdk/client-sfn/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", @@ -5990,8 +17316,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/middleware-recursion-detection": { + "node_modules/@aws-sdk/client-sfn/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", @@ -6003,8 +17331,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/middleware-user-agent": { + "node_modules/@aws-sdk/client-sfn/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", @@ -6019,8 +17349,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/region-config-resolver": { + "node_modules/@aws-sdk/client-sfn/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", @@ -6034,8 +17366,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/token-providers": { + "node_modules/@aws-sdk/client-sfn/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", @@ -6051,8 +17385,10 @@ "@aws-sdk/client-sso-oidc": "^3.693.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/util-endpoints": { + "node_modules/@aws-sdk/client-sfn/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", @@ -6064,8 +17400,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-ec2/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", + "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", @@ -6074,8 +17412,10 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/client-ec2/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", + "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", @@ -6096,8 +17436,10 @@ } } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@smithy/is-array-buffer": { + "node_modules/@aws-sdk/client-sfn/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": { "tslib": "^2.6.2" @@ -6106,8 +17448,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@smithy/util-buffer-from": { + "node_modules/@aws-sdk/client-sfn/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": { "@smithy/is-array-buffer": "^3.0.0", @@ -6117,8 +17461,10 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/@smithy/util-utf8": { + "node_modules/@aws-sdk/client-sfn/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": { "@smithy/util-buffer-from": "^3.0.0", @@ -6128,7 +17474,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam": { + "node_modules/@aws-sdk/client-ssm": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -6173,13 +17519,15 @@ "@smithy/util-retry": "^3.0.9", "@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-iam/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": { @@ -6226,7 +17574,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/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", "dependencies": { @@ -6277,7 +17625,7 @@ "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-iam/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", "dependencies": { @@ -6326,7 +17674,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/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": { @@ -6346,7 +17694,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/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": { @@ -6365,7 +17713,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/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": { @@ -6389,7 +17737,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-ssm/node_modules/@aws-sdk/credential-provider-node": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -6410,7 +17758,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/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": { @@ -6427,7 +17775,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/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": { @@ -6444,7 +17792,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-ssm/node_modules/@aws-sdk/middleware-host-header": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -6457,7 +17805,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/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": { @@ -6469,7 +17817,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/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": { @@ -6482,7 +17830,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/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": { @@ -6498,7 +17846,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/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": { @@ -6513,7 +17861,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/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": { @@ -6530,7 +17878,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-ssm/node_modules/@aws-sdk/util-endpoints": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -6543,7 +17891,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/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": { @@ -6553,7 +17901,7 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/client-iam/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": { @@ -6575,7 +17923,7 @@ } } }, - "node_modules/@aws-sdk/client-iam/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": { @@ -6585,7 +17933,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/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": { @@ -6596,7 +17944,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-iam/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": { @@ -6607,16 +17955,13 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-lambda": { + "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.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", @@ -6628,9 +17973,6 @@ "@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", @@ -6653,1003 +17995,719 @@ "@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", + "node_modules/@aws-sdk/client-sso-oidc": { + "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/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/querystring-builder": "^3.0.3", + "@smithy/smithy-client": "^3.2.0", "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", "@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": { - "version": "3.0.0", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^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", "tslib": "^2.6.2" }, "engines": { "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" + "peerDependencies": { + "@aws-sdk/client-sts": "^3.637.0" } }, - "node_modules/@aws-sdk/client-s3": { - "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": { - "@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", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso": { - "version": "3.693.0", + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.4", "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/protocol-http": "^4.1.0", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", "@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-s3/node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.693.0", + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/is-array-buffer": { + "version": "3.0.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/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-s3/node_modules/@aws-sdk/client-sts": { - "version": "3.693.0", + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-utf8": { + "version": "3.0.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/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-buffer-from": "^3.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/core": { - "version": "3.693.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": { - "@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": "^3.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.693.0", + "node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/types": { + "version": "3.609.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", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.693.0", + "node_modules/@aws-sdk/client-sso/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.4", "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/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": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.693.0", + "node_modules/@aws-sdk/client-sso/node_modules/@smithy/is-array-buffer": { + "version": "3.0.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-s3/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.693.0", + "node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-utf8": { + "version": "3.0.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", + "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.693.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": { - "@aws-sdk/core": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/property-provider": "^3.1.9", - "@smithy/types": "^3.7.0", + "@smithy/is-array-buffer": "^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-s3/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.693.0", + "node_modules/@aws-sdk/client-sts": { + "version": "3.637.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/types": "^3.7.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/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", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-logger": { - "version": "3.693.0", + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/types": { + "version": "3.609.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.693.0", + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.4", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/protocol-http": "^4.1.6", - "@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" - }, - "engines": { - "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.693.0", + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/is-array-buffer": { + "version": "3.0.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-s3/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.693.0", + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-utf8": { + "version": "3.0.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", + "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/token-providers": { - "version": "3.693.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": { - "@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/is-array-buffer": "^3.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sso-oidc": "^3.693.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-endpoints": { - "version": "3.693.0", + "node_modules/@aws-sdk/core": { + "version": "3.635.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", - "@smithy/util-endpoints": "^2.1.5", + "@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/client-s3/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/credential-provider-cognito-identity": { + "version": "3.730.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.730.0.tgz", + "integrity": "sha512-Ynp67VkpaaFubqPrqGxLbg5XuS+QTjR7JVhZvjNO6Su4tQVKBFSfQpDIXTyggD9UVixXy4NB9cqg30uvebDeiw==", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", - "bowser": "^2.11.0", + "@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/client-s3/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/credential-provider-cognito-identity/node_modules/@aws-sdk/types": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.723.0.tgz", + "integrity": "sha512-LmK3kwiMZG1y5g3LGihT9mNkeNOmwEyPk6HGcJqh0wOSV4QpWoKu2epyKE4MLQNUUlz2kOVbVbOrwmI6ZcteuA==", "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/types": "^4.0.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-s3/node_modules/@smithy/is-array-buffer": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/credential-provider-cognito-identity/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": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-buffer-from": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/credential-provider-cognito-identity/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/is-array-buffer": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-utf8": { - "version": "3.0.0", + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.693.0", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^3.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": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-ssm": { + "node_modules/@aws-sdk/credential-provider-env/node_modules/@aws-sdk/core": { "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/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/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" + "fast-xml-parser": "4.4.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/credential-provider-http": { + "version": "3.635.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", + "@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/client-sso-oidc": { - "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-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", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http/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/credential-provider-ini": { + "version": "3.758.0", + "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/types": "^4.1.0", + "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": { - "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", + "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", + "@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/core": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/core": { + "version": "3.758.0", "license": "Apache-2.0", + "peer": true, "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", + "@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" + "node": ">=18.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-ini/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.758.0", "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", + "@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/credential-provider-ini": { - "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", + "peer": true, "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", + "@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" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.693.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/credential-provider-node": { - "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", + "peer": true, "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", + "@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/credential-provider-sso": { - "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", + "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", + "@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/credential-provider-web-identity": { - "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", + "peer": true, "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/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.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-ssm/node_modules/@aws-sdk/middleware-host-header": { - "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", + "peer": true, "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/protocol-http": "^4.1.6", - "@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" + "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/middleware-recursion-detection": { + "version": "3.734.0", "license": "Apache-2.0", + "peer": true, "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", + "@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/middleware-recursion-detection": { - "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", + "peer": true, "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", + "@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": ">=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/region-config-resolver": { + "version": "3.734.0", "license": "Apache-2.0", + "peer": true, "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/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" + "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/token-providers": { + "version": "3.758.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", + "@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/@aws-sdk/token-providers": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/types": { + "version": "3.734.0", "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", + "@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/util-endpoints": { + "version": "3.743.0", "license": "Apache-2.0", + "peer": true, "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/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-ssm/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.734.0", "license": "Apache-2.0", + "peer": true, "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", + "@aws-sdk/types": "3.734.0", + "@smithy/types": "^4.1.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, - "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/util-user-agent-node": { + "version": "3.758.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", + "@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" @@ -7660,576 +18718,484 @@ } } }, - "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/@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": ">=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/@smithy/config-resolver": { + "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { - "@smithy/is-array-buffer": "^3.0.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" + "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/@smithy/core": { + "version": "3.1.5", "license": "Apache-2.0", + "peer": true, "dependencies": { - "@smithy/util-buffer-from": "^3.0.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": { - "version": "3.637.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/credential-provider-imds": { + "version": "4.0.1", "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.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", + "@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": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.637.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/fetch-http-handler": { + "version": "5.0.1", "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.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/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/credential-provider-ini/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", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.637.0" + "engines": { + "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/invalid-dependency": { + "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { - "@smithy/types": "^3.3.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-oidc/node_modules/@smithy/fetch-http-handler": { - "version": "3.2.4", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "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/client-sso-oidc/node_modules/@smithy/is-array-buffer": { - "version": "3.0.0", + "node_modules/@aws-sdk/credential-provider-ini/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", "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/middleware-endpoint": { + "version": "4.0.6", "license": "Apache-2.0", + "peer": true, "dependencies": { - "@smithy/util-buffer-from": "^3.0.0", + "@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-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/middleware-retry": { + "version": "4.0.7", "license": "Apache-2.0", + "peer": true, "dependencies": { - "@smithy/is-array-buffer": "^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", + "peer": true, + "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/@aws-sdk/types": { - "version": "3.609.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/middleware-stack": { + "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { - "@smithy/types": "^3.3.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/@smithy/fetch-http-handler": { - "version": "3.2.4", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/node-config-provider": { + "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "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/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/@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", + "@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-sso/node_modules/@smithy/is-array-buffer": { - "version": "3.0.0", + "node_modules/@aws-sdk/credential-provider-ini/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": ">=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/protocol-http": { + "version": "5.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { - "@smithy/util-buffer-from": "^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/@smithy/util-utf8/node_modules/@smithy/util-buffer-from": { - "version": "3.0.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/querystring-builder": { + "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { - "@smithy/is-array-buffer": "^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": { - "version": "3.637.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/querystring-parser": { + "version": "4.0.1", "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.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/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/service-error-classification": { + "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { - "@smithy/types": "^3.3.0", - "tslib": "^2.6.2" + "@smithy/types": "^4.1.0" }, "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/shared-ini-file-loader": { + "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "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/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", + "@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": ">=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/smithy-client": { + "version": "4.1.6", "license": "Apache-2.0", + "peer": true, "dependencies": { - "@smithy/util-buffer-from": "^3.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": ">=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/types": { + "version": "4.1.0", "license": "Apache-2.0", + "peer": true, "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/core": { - "version": "3.635.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/url-parser": { + "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "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/querystring-parser": "^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/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", + "peer": true, "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", + "peer": true, "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", + "peer": true, "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", + "peer": true, "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", + "peer": true, "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", "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" }, "engines": { "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-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/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/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-retry": { + "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/shared-ini-file-loader": "^4.0.1", + "@smithy/service-error-classification": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, @@ -8237,300 +19203,313 @@ "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-stream": { + "version": "4.1.2", "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/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/credential-provider-ini/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.734.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/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/credential-provider-ini/node_modules/@aws-sdk/middleware-logger": { - "version": "3.734.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/types": "3.734.0", - "@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-ini/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.734.0", + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.637.0", "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/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.635.0", + "@aws-sdk/credential-provider-ini": "3.637.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.637.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.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-node/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.620.1", "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/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.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-node/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.637.0", "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-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.635.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.637.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.637.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/token-providers": { - "version": "3.758.0", + "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.620.1", "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.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/types": { - "version": "3.734.0", + "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.621.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.621.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/util-endpoints": { - "version": "3.743.0", + "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/types": { + "version": "3.609.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "@smithy/util-endpoints": "^3.0.1", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.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-process": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "bowser": "^2.11.0", + "@aws-sdk/core": "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/credential-provider-ini/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.758.0", + "node_modules/@aws-sdk/credential-provider-process/node_modules/@aws-sdk/core": { + "version": "3.693.0", "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/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" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/abort-controller": { - "version": "4.0.1", + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.637.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/client-sso": "3.637.0", + "@aws-sdk/token-providers": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/config-resolver": { - "version": "4.0.1", + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/types": { + "version": "3.609.0", "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": "^3.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/core": { - "version": "3.1.5", + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.758.0", "license": "Apache-2.0", "peer": true, "dependencies": { - "@smithy/middleware-serde": "^4.0.2", - "@smithy/protocol-http": "^5.0.1", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/nested-clients": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.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/credential-provider-ini/node_modules/@smithy/credential-provider-imds": { - "version": "4.0.1", + "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", "@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/url-parser": "^4.0.1", + "@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/@smithy/fetch-http-handler": { - "version": "5.0.1", + "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/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/credential-provider-ini/node_modules/@smithy/hash-node": { + "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", - "@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/credential-provider-ini/node_modules/@smithy/invalid-dependency": { - "version": "4.0.1", + "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", "@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/credential-provider-ini/node_modules/@smithy/is-array-buffer": { - "version": "4.0.0", + "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", + "@smithy/types": "^4.1.0", + "@smithy/util-base64": "^4.0.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-web-identity/node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", "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": { + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/middleware-endpoint": { "version": "4.0.6", "license": "Apache-2.0", "peer": true, @@ -8548,26 +19527,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/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_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/middleware-serde": { + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/middleware-serde": { "version": "4.0.2", "license": "Apache-2.0", "peer": true, @@ -8579,7 +19539,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/middleware-stack": { + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/middleware-stack": { "version": "4.0.1", "license": "Apache-2.0", "peer": true, @@ -8591,7 +19551,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/node-config-provider": { + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/node-config-provider": { "version": "4.0.1", "license": "Apache-2.0", "peer": true, @@ -8605,7 +19565,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/node-http-handler": { + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/node-http-handler": { "version": "4.0.3", "license": "Apache-2.0", "peer": true, @@ -8620,7 +19580,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/property-provider": { + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/property-provider": { "version": "4.0.1", "license": "Apache-2.0", "peer": true, @@ -8632,7 +19592,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/protocol-http": { + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/protocol-http": { "version": "5.0.1", "license": "Apache-2.0", "peer": true, @@ -8644,7 +19604,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/querystring-builder": { + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/querystring-builder": { "version": "4.0.1", "license": "Apache-2.0", "peer": true, @@ -8657,7 +19617,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/querystring-parser": { + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/querystring-parser": { "version": "4.0.1", "license": "Apache-2.0", "peer": true, @@ -8669,18 +19629,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/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/credential-provider-ini/node_modules/@smithy/shared-ini-file-loader": { + "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, @@ -8692,7 +19641,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/signature-v4": { + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/signature-v4": { "version": "5.0.1", "license": "Apache-2.0", "peer": true, @@ -8710,7 +19659,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/smithy-client": { + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/smithy-client": { "version": "4.1.6", "license": "Apache-2.0", "peer": true, @@ -8727,7 +19676,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/types": { + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/types": { "version": "4.1.0", "license": "Apache-2.0", "peer": true, @@ -8738,7 +19687,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/url-parser": { + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/url-parser": { "version": "4.0.1", "license": "Apache-2.0", "peer": true, @@ -8751,7 +19700,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-base64": { + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-base64": { "version": "4.0.0", "license": "Apache-2.0", "peer": true, @@ -8764,7 +19713,7 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-body-length-browser": { + "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, @@ -8775,132 +19724,406 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-body-length-node": { + "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" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-buffer-from": { + "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": { - "@smithy/is-array-buffer": "^4.0.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-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" }, "engines": { "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-web-identity/node_modules/@smithy/util-stream": { + "version": "4.1.2", "license": "Apache-2.0", "peer": true, "dependencies": { - "@smithy/property-provider": "^4.0.1", - "@smithy/smithy-client": "^4.1.6", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/node-http-handler": "^4.0.3", "@smithy/types": "^4.1.0", - "bowser": "^2.11.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/credential-provider-ini/node_modules/@smithy/util-defaults-mode-node": { - "version": "4.0.7", + "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": { - "@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/credential-provider-ini/node_modules/@smithy/util-endpoints": { - "version": "3.0.1", + "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/node-config-provider": "^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-ini/node_modules/@smithy/util-hex-encoding": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers": { + "version": "3.730.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.730.0.tgz", + "integrity": "sha512-Z25yfmHOehgIDVyY8h7GmAEbodHD2iLgNmrBBkkJXCE6d4GwDet3Qeyw4bQPPyuycBtYOUiz5Oco03+YGOEhYA==", "dependencies": { + "@aws-sdk/client-cognito-identity": "3.730.0", + "@aws-sdk/core": "3.730.0", + "@aws-sdk/credential-provider-cognito-identity": "3.730.0", + "@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-node": "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/types": "^4.0.0", "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, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/client-sso": { + "version": "3.730.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.730.0.tgz", + "integrity": "sha512-mI8kqkSuVlZklewEmN7jcbBMyVODBld3MsTjCKSl5ztduuPX69JD7nXLnWWPkw1PX4aGTO24AEoRMGNxntoXUg==", "dependencies": { - "@smithy/types": "^4.1.0", + "@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/credential-provider-ini/node_modules/@smithy/util-retry": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/core": { + "version": "3.730.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.730.0.tgz", + "integrity": "sha512-jonKyR+2GcqbZj2WDICZS0c633keLc9qwXnePu83DfAoFXMMIMyoR/7FOGf8F3OrIdGh8KzE9VvST+nZCK9EJA==", "dependencies": { - "@smithy/service-error-classification": "^4.0.1", - "@smithy/types": "^4.1.0", + "@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/credential-provider-ini/node_modules/@smithy/util-stream": { - "version": "4.1.2", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.730.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.730.0.tgz", + "integrity": "sha512-fFXgo3jBXLWqu8I07Hd96mS7RjrtpDgm3bZShm0F3lKtqDQF+hObFWq9A013SOE+RjMLVfbABhToXAYct3FcBw==", "dependencies": { - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/node-http-handler": "^4.0.3", - "@smithy/types": "^4.1.0", + "@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/credential-providers/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.730.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.730.0.tgz", + "integrity": "sha512-1aF3elbCzpVhWLAuV63iFElfLOqLGGTp4fkf2VAFIDO3hjshpXUQssTgIWiBwwtJYJdOSxaFrCU7u8frjr/5aQ==", + "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/credential-providers/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.730.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.730.0.tgz", + "integrity": "sha512-zwsxkBuQuPp06o45ATAnznHzj3+ibop/EaTytNzSv0O87Q59K/jnS/bdtv1n6bhe99XCieRNTihvtS7YklzK7A==", + "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/credential-providers/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.730.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.730.0.tgz", + "integrity": "sha512-ztRjh1edY7ut2wwrj1XqHtqPY/NXEYIk5fYf04KKsp8zBi81ScVqP7C+Cst6PFKixjgLSG6RsqMx9GSAalVv0Q==", + "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/credential-providers/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.730.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.730.0.tgz", + "integrity": "sha512-cNKUQ81eptfZN8MlSqwUq3+5ln8u/PcY57UmLZ+npxUHanqO1akpgcpNsLpmsIkoXGbtSQrLuDUgH86lS/SWOw==", + "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/credential-providers/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.730.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.730.0.tgz", + "integrity": "sha512-SdI2xrTbquJLMxUh5LpSwB8zfiKq3/jso53xWRgrVfeDlrSzZuyV6QghaMs3KEEjcNzwEnTfSIjGQyRXG9VrEw==", + "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/credential-providers/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.730.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.730.0.tgz", + "integrity": "sha512-l5vdPmvF/d890pbvv5g1GZrdjaSQkyPH/Bc8dO/ZqkWxkIP8JNgl48S2zgf4DkP3ik9K2axWO828L5RsMDQzdA==", + "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/credential-providers/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.723.0.tgz", + "integrity": "sha512-LLVzLvk299pd7v4jN9yOSaWDZDfH0SnBPb6q+FDPaOCMGBY8kuwQso7e/ozIKSmZHRMGO3IZrflasHM+rI+2YQ==", + "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/credential-providers/node_modules/@aws-sdk/middleware-logger": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.723.0.tgz", + "integrity": "sha512-chASQfDG5NJ8s5smydOEnNK7N0gDMyuPbx7dYYcm1t/PKtnVfvWF+DHCTrRC2Ej76gLJVCVizlAJKM8v8Kg3cg==", + "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/credential-providers/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.723.0.tgz", + "integrity": "sha512-7usZMtoynT9/jxL/rkuDOFQ0C2mhXl4yCm67Rg7GNTstl67u7w5WN1aIRImMeztaKlw8ExjoTyo6WTs1Kceh7A==", + "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/credential-providers/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.730.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.730.0.tgz", + "integrity": "sha512-aPMZvNmf2a42B41au3bA3ODU4HfHka2nYT/SAIhhVXH1ENYfAmZo7FraFPxetKepFMCtL7j4QE6/LDucK6liIw==", + "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/credential-providers/node_modules/@aws-sdk/nested-clients": { + "version": "3.730.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.730.0.tgz", + "integrity": "sha512-vilIgf1/7kre8DdE5zAQkDOwHFb/TahMn/6j2RZwFLlK7cDk91r19deSiVYnKQkupDMtOfNceNqnorM4I3PDzw==", + "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-buffer-from": "^4.0.0", - "@smithy/util-hex-encoding": "^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" }, @@ -8908,465 +20131,497 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-uri-escape": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.723.0.tgz", + "integrity": "sha512-tGF/Cvch3uQjZIj34LY2mg8M2Dr4kYG8VU8Yd0dFnB1ybOEOveIK/9ypUo9ycZpB9oO6q01KRe5ijBaxNueUQg==", "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/credential-provider-ini/node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/token-providers": { + "version": "3.730.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.730.0.tgz", + "integrity": "sha512-BSPssGj54B/AABWXARIPOT/1ybFahM1ldlfmXy9gRmZi/afe9geWJGlFYCCt3PmqR+1Ny5XIjSfue+kMd//drQ==", "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", + "@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/credential-provider-node": { - "version": "3.637.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/types": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.723.0.tgz", + "integrity": "sha512-LmK3kwiMZG1y5g3LGihT9mNkeNOmwEyPk6HGcJqh0wOSV4QpWoKu2epyKE4MLQNUUlz2kOVbVbOrwmI6ZcteuA==", "dependencies": { - "@aws-sdk/credential-provider-env": "3.620.1", - "@aws-sdk/credential-provider-http": "3.635.0", - "@aws-sdk/credential-provider-ini": "3.637.0", - "@aws-sdk/credential-provider-process": "3.620.1", - "@aws-sdk/credential-provider-sso": "3.637.0", - "@aws-sdk/credential-provider-web-identity": "3.621.0", - "@aws-sdk/types": "3.609.0", - "@smithy/credential-provider-imds": "^3.2.0", - "@smithy/property-provider": "^3.1.3", - "@smithy/shared-ini-file-loader": "^3.1.4", - "@smithy/types": "^3.3.0", + "@smithy/types": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.620.1", - "license": "Apache-2.0", + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/util-endpoints": { + "version": "3.730.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.730.0.tgz", + "integrity": "sha512-1KTFuVnk+YtLgWr6TwDiggcDqtPpOY2Cszt3r2lkXfaEAX6kHyOZi1vdvxXjPU5LsOBJem8HZ7KlkmrEi+xowg==", "dependencies": { - "@aws-sdk/types": "3.609.0", - "@smithy/property-provider": "^3.1.3", - "@smithy/types": "^3.3.0", + "@aws-sdk/types": "3.723.0", + "@smithy/types": "^4.0.0", + "@smithy/util-endpoints": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.637.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.723.0.tgz", + "integrity": "sha512-Wh9I6j2jLhNFq6fmXydIpqD1WyQLyTfSxjW9B+PXSnPyk3jtQW8AKQur7p97rO8LAUzVI0bv8kb3ZzDEVbquIg==", "dependencies": { - "@aws-sdk/credential-provider-env": "3.620.1", - "@aws-sdk/credential-provider-http": "3.635.0", - "@aws-sdk/credential-provider-process": "3.620.1", - "@aws-sdk/credential-provider-sso": "3.637.0", - "@aws-sdk/credential-provider-web-identity": "3.621.0", - "@aws-sdk/types": "3.609.0", - "@smithy/credential-provider-imds": "^3.2.0", - "@smithy/property-provider": "^3.1.3", - "@smithy/shared-ini-file-loader": "^3.1.4", - "@smithy/types": "^3.3.0", + "@aws-sdk/types": "3.723.0", + "@smithy/types": "^4.0.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.730.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.730.0.tgz", + "integrity": "sha512-yBvkOAjqsDEl1va4eHNOhnFBk0iCY/DBFNyhvtTMqPF4NO+MITWpFs3J9JtZKzJlQ6x0Yb9TLQ8NhDjEISz5Ug==", + "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": ">=16.0.0" + "node": ">=18.0.0" }, "peerDependencies": { - "@aws-sdk/client-sts": "^3.637.0" + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } } }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.620.1", - "license": "Apache-2.0", + "node_modules/@aws-sdk/credential-providers/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": { - "@aws-sdk/types": "3.609.0", - "@smithy/property-provider": "^3.1.3", - "@smithy/shared-ini-file-loader": "^3.1.4", - "@smithy/types": "^3.3.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.621.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/credential-providers/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": { - "@aws-sdk/types": "3.609.0", - "@smithy/property-provider": "^3.1.3", - "@smithy/types": "^3.3.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": ">=16.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/core": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.5.3.tgz", + "integrity": "sha512-xa5byV9fEguZNofCclv6v9ra0FYh5FATQW/da7FQUVTic94DfrN/NvmKZjrMyzbpqfot9ZjBaO8U1UeTbmSLuA==", + "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" }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.621.0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/types": { - "version": "3.609.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/credential-providers/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/types": "^3.3.0", + "@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": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/fetch-http-handler": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.4.tgz", + "integrity": "sha512-AMtBR5pHppYMVD7z7G+OlHHAcgAN7v0kVKEpHuTO4Gb199Gowh0taYi9oDStFeUhetkeP55JLSVlTW1n9rFtUw==", "dependencies": { - "@aws-sdk/core": "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/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/credential-providers/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": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-process/node_modules/@aws-sdk/core": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/credential-providers/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": { - "@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/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.637.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/credential-providers/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": { - "@aws-sdk/client-sso": "3.637.0", - "@aws-sdk/token-providers": "3.614.0", - "@aws-sdk/types": "3.609.0", - "@smithy/property-provider": "^3.1.3", - "@smithy/shared-ini-file-loader": "^3.1.4", - "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/types": { - "version": "3.609.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/credential-providers/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/types": "^3.3.0", + "@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/credential-provider-web-identity": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/middleware-endpoint": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.11.tgz", + "integrity": "sha512-zDogwtRLzKl58lVS8wPcARevFZNBOOqnmzWWxVe9XiaXU2CADFjvJ9XfNibgkOWs08sxLuSr81NrpY4mgp9OwQ==", "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/nested-clients": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", - "@smithy/types": "^4.1.0", + "@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/credential-provider-web-identity/node_modules/@aws-sdk/core": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/middleware-retry": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.12.tgz", + "integrity": "sha512-wvIH70c4e91NtRxdaLZF+mbLZ/HcC6yg7ySKUiufL6ESp6zJUSnJucZ309AvG9nqCFHSRB5I6T3Ez1Q9wCh0Ww==", "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" + "@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/credential-provider-web-identity/node_modules/@aws-sdk/types": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/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/credential-provider-web-identity/node_modules/@smithy/abort-controller": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/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/types": "^4.1.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/core": { - "version": "3.1.5", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/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/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/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/credential-provider-web-identity/node_modules/@smithy/fetch-http-handler": { - "version": "5.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/node-http-handler": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz", + "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==", "dependencies": { - "@smithy/protocol-http": "^5.0.1", - "@smithy/querystring-builder": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-base64": "^4.0.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": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/is-array-buffer": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/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/credential-provider-web-identity/node_modules/@smithy/middleware-endpoint": { - "version": "4.0.6", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/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/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/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/middleware-serde": { - "version": "4.0.2", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/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.1.0", + "@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/credential-provider-web-identity/node_modules/@smithy/middleware-stack": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/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.1.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/node-config-provider": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/service-error-classification": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.5.tgz", + "integrity": "sha512-LvcfhrnCBvCmTee81pRlh1F39yTS/+kYleVeLCwNtkY8wtGg8V/ca9rbZZvYIl8OjlMtL6KIjaiL/lgVqHD2nA==", "dependencies": { - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.3.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/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/credential-provider-web-identity/node_modules/@smithy/node-http-handler": { - "version": "4.0.3", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/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/abort-controller": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/querystring-builder": "^4.0.1", - "@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/credential-provider-web-identity/node_modules/@smithy/property-provider": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/smithy-client": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.3.tgz", + "integrity": "sha512-xxzNYgA0HD6ETCe5QJubsxP0hQH3QK3kbpJz3QrosBCuIWyEXLR/CO5hFb2OeawEKUxMNhz3a1nuJNN2np2RMA==", "dependencies": { - "@smithy/types": "^4.1.0", + "@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/credential-provider-web-identity/node_modules/@smithy/protocol-http": { - "version": "5.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/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/credential-provider-web-identity/node_modules/@smithy/querystring-builder": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/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/types": "^4.1.0", - "@smithy/util-uri-escape": "^4.0.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/credential-provider-web-identity/node_modules/@smithy/querystring-parser": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/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": { - "@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/credential-provider-web-identity/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/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": { - "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/signature-v4": { - "version": "5.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/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": { - "@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-web-identity/node_modules/@smithy/smithy-client": { - "version": "4.1.6", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/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/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/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/types": { - "version": "4.1.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/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" }, @@ -9374,86 +20629,95 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/url-parser": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.19", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.19.tgz", + "integrity": "sha512-mvLMh87xSmQrV5XqnUYEPoiFFeEGYeAKIDDKdhE2ahqitm8OHM3aSvhqL6rrK6wm1brIk90JhxDf5lf2hbrLbQ==", "dependencies": { - "@smithy/querystring-parser": "^4.0.1", - "@smithy/types": "^4.1.0", + "@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/credential-provider-web-identity/node_modules/@smithy/util-base64": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.19", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.19.tgz", + "integrity": "sha512-8tYnx+LUfj6m+zkUUIrIQJxPM1xVxfRBvoGHua7R/i6qAxOMjqR6CpEpDwKoIs1o0+hOjGvkKE23CafKL0vJ9w==", "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.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.3", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "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, + "node_modules/@aws-sdk/credential-providers/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/credential-provider-web-identity/node_modules/@smithy/util-buffer-from": { + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-hex-encoding": { "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-hex-encoding": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/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": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-middleware": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-retry": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.5.tgz", + "integrity": "sha512-V7MSjVDTlEt/plmOFBn1762Dyu5uqMrV2Pl2X0dYk4XvWfdWJNe9Bs5Bzb56wkCuiWjSfClVMGcsuKrGj7S/yg==", "dependencies": { - "@smithy/types": "^4.1.0", + "@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/credential-provider-web-identity/node_modules/@smithy/util-stream": { - "version": "4.1.2", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-stream": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.2.tgz", + "integrity": "sha512-aI+GLi7MJoVxg24/3J1ipwLoYzgkB4kUfogZfnslcYlynj3xsQ0e7vk4TnTro9hhsS5PvX1mwmkRqqHQjwcU7w==", "dependencies": { - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/node-http-handler": "^4.0.3", - "@smithy/types": "^4.1.0", + "@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", @@ -9464,10 +20728,10 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-uri-escape": { + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-uri-escape": { "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "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" }, @@ -9475,10 +20739,10 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-utf8": { + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-utf8": { "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "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" @@ -9760,6 +21024,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", @@ -9812,6 +21259,232 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/middleware-sdk-sts": { + "version": "3.363.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.363.0.tgz", + "integrity": "sha512-1yy2Ac50FO8BrODaw5bPWvVrRhaVLqXTFH6iHB+dJLPUkwtY5zLM3Mp+9Ilm7kME+r7oIB1wuO6ZB1Lf4ZszIw==", + "dependencies": { + "@aws-sdk/middleware-signing": "3.363.0", + "@aws-sdk/types": "3.357.0", + "@smithy/types": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-sts/node_modules/@aws-sdk/types": { + "version": "3.357.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.357.0.tgz", + "integrity": "sha512-/riCRaXg3p71BeWnShrai0y0QTdXcouPSM0Cn1olZbzTf7s71aLEewrc96qFrL70XhY4XvnxMpqQh+r43XIL3g==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-sts/node_modules/@smithy/types": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-1.2.0.tgz", + "integrity": "sha512-z1r00TvBqF3dh4aHhya7nz1HhvCg4TRmw51fjMrh5do3h+ngSstt/yKlNbHeb9QxJmFbmN8KEVSWgb1bRvfEoA==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-signing": { + "version": "3.363.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.363.0.tgz", + "integrity": "sha512-/7qia715pt9JKYIPDGu22WmdZxD8cfF/5xB+1kmILg7ZtjO0pPuTaCNJ7xiIuFd7Dn7JXp5lop08anX/GOhNRQ==", + "dependencies": { + "@aws-sdk/types": "3.357.0", + "@smithy/property-provider": "^1.0.1", + "@smithy/protocol-http": "^1.1.0", + "@smithy/signature-v4": "^1.0.1", + "@smithy/types": "^1.1.0", + "@smithy/util-middleware": "^1.0.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-signing/node_modules/@aws-crypto/crc32": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz", + "integrity": "sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==", + "dependencies": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-sdk/middleware-signing/node_modules/@aws-crypto/crc32/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-sdk/middleware-signing/node_modules/@aws-crypto/util": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-3.0.0.tgz", + "integrity": "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-sdk/middleware-signing/node_modules/@aws-crypto/util/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-sdk/middleware-signing/node_modules/@aws-sdk/types": { + "version": "3.357.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.357.0.tgz", + "integrity": "sha512-/riCRaXg3p71BeWnShrai0y0QTdXcouPSM0Cn1olZbzTf7s71aLEewrc96qFrL70XhY4XvnxMpqQh+r43XIL3g==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-signing/node_modules/@smithy/eventstream-codec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-1.1.0.tgz", + "integrity": "sha512-3tEbUb8t8an226jKB6V/Q2XU/J53lCwCzULuBPEaF4JjSh+FlCMp7TmogE/Aij5J9DwlsZ4VAD/IRDuQ/0ZtMw==", + "dependencies": { + "@aws-crypto/crc32": "3.0.0", + "@smithy/types": "^1.2.0", + "@smithy/util-hex-encoding": "^1.1.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/middleware-signing/node_modules/@smithy/is-array-buffer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-1.1.0.tgz", + "integrity": "sha512-twpQ/n+3OWZJ7Z+xu43MJErmhB/WO/mMTnqR6PwWQShvSJ/emx5d1N59LQZk6ZpTAeuRWrc+eHhkzTp9NFjNRQ==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-signing/node_modules/@smithy/property-provider": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-1.2.0.tgz", + "integrity": "sha512-qlJd9gT751i4T0t/hJAyNGfESfi08Fek8QiLcysoKPgR05qHhG0OYhlaCJHhpXy4ECW0lHyjvFM1smrCLIXVfw==", + "dependencies": { + "@smithy/types": "^1.2.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-signing/node_modules/@smithy/protocol-http": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-1.2.0.tgz", + "integrity": "sha512-GfGfruksi3nXdFok5RhgtOnWe5f6BndzYfmEXISD+5gAGdayFGpjWu5pIqIweTudMtse20bGbc+7MFZXT1Tb8Q==", + "dependencies": { + "@smithy/types": "^1.2.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-signing/node_modules/@smithy/signature-v4": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-1.1.0.tgz", + "integrity": "sha512-fDo3m7YqXBs7neciOePPd/X9LPm5QLlDMdIC4m1H6dgNLnXfLMFNIxEfPyohGA8VW9Wn4X8lygnPSGxDZSmp0Q==", + "dependencies": { + "@smithy/eventstream-codec": "^1.1.0", + "@smithy/is-array-buffer": "^1.1.0", + "@smithy/types": "^1.2.0", + "@smithy/util-hex-encoding": "^1.1.0", + "@smithy/util-middleware": "^1.1.0", + "@smithy/util-uri-escape": "^1.1.0", + "@smithy/util-utf8": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-signing/node_modules/@smithy/types": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-1.2.0.tgz", + "integrity": "sha512-z1r00TvBqF3dh4aHhya7nz1HhvCg4TRmw51fjMrh5do3h+ngSstt/yKlNbHeb9QxJmFbmN8KEVSWgb1bRvfEoA==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-signing/node_modules/@smithy/util-buffer-from": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-1.1.0.tgz", + "integrity": "sha512-9m6NXE0ww+ra5HKHCHig20T+FAwxBAm7DIdwc/767uGWbRcY720ybgPacQNB96JMOI7xVr/CDa3oMzKmW4a+kw==", + "dependencies": { + "@smithy/is-array-buffer": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-signing/node_modules/@smithy/util-hex-encoding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-1.1.0.tgz", + "integrity": "sha512-7UtIE9eH0u41zpB60Jzr0oNCQ3hMJUabMcKRUVjmyHTXiWDE4vjSqN6qlih7rCNeKGbioS7f/y2Jgym4QZcKFg==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-signing/node_modules/@smithy/util-middleware": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-1.1.0.tgz", + "integrity": "sha512-6hhckcBqVgjWAqLy2vqlPZ3rfxLDhFWEmM7oLh2POGvsi7j0tHkbN7w4DFhuBExVJAbJ/qqxqZdRY6Fu7/OezQ==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-signing/node_modules/@smithy/util-uri-escape": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-1.1.0.tgz", + "integrity": "sha512-/jL/V1xdVRt5XppwiaEU8Etp5WHZj609n0xMTuehmCqdoOFbId1M+aEeDWZsQ+8JbEB/BJ6ynY2SlYmOaKtt8w==", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-signing/node_modules/@smithy/util-utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-1.1.0.tgz", + "integrity": "sha512-p/MYV+JmqmPyjdgyN2UxAeYDj9cBqCjp0C/NsTWnnjoZUVqoeZ6IrW915L9CAKWVECgv9lVQGc4u/yz26/bI1A==", + "dependencies": { + "@smithy/util-buffer-from": "^1.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@aws-sdk/middleware-ssec": { "version": "3.693.0", "license": "Apache-2.0", @@ -10867,6 +22540,14 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/util-utf8-browser": { + "version": "3.259.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz", + "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==", + "dependencies": { + "tslib": "^2.3.1" + } + }, "node_modules/@aws-sdk/xml-builder": { "version": "3.693.0", "license": "Apache-2.0", @@ -10879,9 +22560,9 @@ } }, "node_modules/@aws-toolkits/telemetry": { - "version": "1.0.323", - "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.323.tgz", - "integrity": "sha512-Wc6HE+l5iJm/3TYx8Y8pU99ffmq78FgDDVMKjYG9Mfr4cXO4PEkB6XOkiVwGYnrNOGWqyYNlnkBFJ32WJRfkKg==", + "version": "1.0.329", + "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.329.tgz", + "integrity": "sha512-zMkljZDtIAxuZzPTLL5zIxn+zGmk767sbqGIc2ZYuv0sSU+UoYgB3tqwV5KVV2oDPKs5593nwJC97NVHJqzowQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -10905,39 +22586,50 @@ } }, "node_modules/@aws/chat-client-ui-types": { - "version": "0.1.26", - "resolved": "https://registry.npmjs.org/@aws/chat-client-ui-types/-/chat-client-ui-types-0.1.26.tgz", - "integrity": "sha512-WlF0fP1nojueknr815dg6Ivs+Q3e5onvWTH1nI05jysSzUHjsWwFDBrsxqJXfaPIFhPrbQzHqoxHbhIwQ1OLuw==", + "version": "0.1.47", + "resolved": "https://registry.npmjs.org/@aws/chat-client-ui-types/-/chat-client-ui-types-0.1.47.tgz", + "integrity": "sha512-Pu6UnAImpweLMcAmhNdw/NrajB25Ymzp1Om1V9NEVQJRMO/KJCDiErmbOYTYBXvgNoR10kObqiL1P/Tk/Fpu3g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes-types": "^0.1.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.81", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.81.tgz", - "integrity": "sha512-wnwa8ctVCAckIpfWSblHyLVzl6UKX5G7ft+yetH1pI0mZvseSNzHUhclxNl4WGaDgGnEbBjLD0XRNEy2yRrSYg==", - "dev": true, + "version": "0.2.128", + "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.128.tgz", + "integrity": "sha512-C666VAvY2PQ8CQkDzjL/+N9rfcFzY6vuGe733drMwwRVHt8On0B0PQPjy31ZjxHUUcjVp78Nb9vmSUEVBfxGTQ==", "license": "Apache-2.0", "dependencies": { - "@aws/language-server-runtimes-types": "^0.1.28", + "@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.0", + "@opentelemetry/resources": "^2.0.1", "@opentelemetry/sdk-logs": "^0.200.0", - "@opentelemetry/sdk-metrics": "^2.0.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": { @@ -10945,21 +22637,63 @@ } }, "node_modules/@aws/language-server-runtimes-types": { - "version": "0.1.28", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes-types/-/language-server-runtimes-types-0.1.28.tgz", - "integrity": "sha512-eDNcEXGAyD4rzl+eVJ6Ngfbm4iaR8MkoMk1wVcnV+VGqu63TyvV1aVWnZdl9tR4pmC0rIH3tj8FSCjhSU6eJlA==", - "dev": true, + "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==", "license": "Apache-2.0", "dependencies": { "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.5" } }, + "node_modules/@aws/language-server-runtimes/node_modules/@opentelemetry/core": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz", + "integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@aws/language-server-runtimes/node_modules/@opentelemetry/resources": { + "version": "2.0.1", + "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" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@aws/language-server-runtimes/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.0.1", + "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" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, "node_modules/@aws/language-server-runtimes/node_modules/@smithy/abort-controller": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.2.tgz", "integrity": "sha512-Sl/78VDtgqKxN2+1qduaVE140XF+Xg+TafkncspwM4jFP/LHr76ZHmIY/y3V1M0mMLNk+Je6IGbzxy23RSToMw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.2.0", @@ -10973,7 +22707,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.4.tgz", "integrity": "sha512-/mdqabuAT3o/ihBGjL94PUbTSPSRJ0eeVTdgADzow0wRJ0rN4A27EOrtlK56MYiO1fDvlO3jVTCxQtQmK9dZ1g==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.0.2", @@ -10990,7 +22723,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.0.tgz", "integrity": "sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.2.0", @@ -11004,7 +22736,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.2.tgz", "integrity": "sha512-NTOs0FwHw1vimmQM4ebh+wFQvOwkEf/kQL6bSM1Lock+Bv4I89B3hGYoUEPkmvYPkDKyp5UdXJYu+PoTQ3T31Q==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.2.0", @@ -11019,7 +22750,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.2.0.tgz", "integrity": "sha512-7eMk09zQKCO+E/ivsjQv+fDlOupcFUCSC/L2YUPgwhvowVGWbPQHjEFcmjt7QQ4ra5lyowS92SV53Zc6XD4+fg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -11032,7 +22762,6 @@ "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==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -11043,7 +22772,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", @@ -11058,7 +22786,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" @@ -11066,12 +22793,10 @@ }, "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" @@ -11079,7 +22804,6 @@ }, "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" @@ -11090,19 +22814,22 @@ }, "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", + "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.34.1", - "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.34.1.tgz", - "integrity": "sha512-CO65lwedf6Iw3a3ULOl+9EHafIekiPlP+n8QciN9a3POfsRamHl0kpBGaGBzBRgsQ/h5R0FvFG/gAuWoiK/YIA==", + "version": "4.35.4", + "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.35.4.tgz", + "integrity": "sha512-LuOexbuMSKYCl/Qa7zj9d4/ueTLK3ltoYHeA0I7gOpPC/vYACxqjVqX6HPhNCE+L5zBKNMN2Z+FUaox+fYhvAQ==", "hasInstallScript": true, - "license": "Apache License 2.0", "dependencies": { "escape-html": "^1.0.3", "highlight.js": "^11.11.0", @@ -11676,7 +23403,6 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8.0.0" @@ -11686,7 +23412,6 @@ "version": "0.200.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.200.0.tgz", "integrity": "sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -11699,7 +23424,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.0.tgz", "integrity": "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -11715,7 +23439,6 @@ "version": "0.200.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.200.0.tgz", "integrity": "sha512-KfWw49htbGGp9s8N4KI8EQ9XuqKJ0VG+yVYVYFiCYSjEV32qpQ5qZ9UZBzOZ6xRb+E16SXOSCT3RkqBVSABZ+g==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@opentelemetry/api-logs": "0.200.0", @@ -11735,7 +23458,6 @@ "version": "0.200.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.200.0.tgz", "integrity": "sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.0", @@ -11755,7 +23477,6 @@ "version": "0.200.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.200.0.tgz", "integrity": "sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.0", @@ -11765,14 +23486,13 @@ "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "node_modules/@opentelemetry/otlp-transformer": { "version": "0.200.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.200.0.tgz", "integrity": "sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@opentelemetry/api-logs": "0.200.0", @@ -11794,7 +23514,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.0.tgz", "integrity": "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.0", @@ -11811,7 +23530,6 @@ "version": "0.200.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.200.0.tgz", "integrity": "sha512-VZG870063NLfObmQQNtCVcdXXLzI3vOjjrRENmU37HYiPFa0ZXpXVDsTD02Nh3AT3xYJzQaWKl2X2lQ2l7TWJA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@opentelemetry/api-logs": "0.200.0", @@ -11825,11 +23543,41 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { + "version": "2.0.0", + "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" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { + "version": "2.0.0", + "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", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-metrics": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.0.tgz", "integrity": "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.0", @@ -11846,7 +23594,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.0.tgz", "integrity": "sha512-qQnYdX+ZCkonM7tA5iU4fSRsVxbFGml8jbxOgipRGMFHKaXKHQ30js03rTobYjKjIfnOsZSbHKWF0/0v0OQGfw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.0", @@ -11864,7 +23611,6 @@ "version": "1.33.0", "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.33.0.tgz", "integrity": "sha512-TIpZvE8fiEILFfTlfPnltpBaD3d9/+uQHVCyC3vfdh6WfCXKhNFzoP5RyDDIndfvZC5GrA4pyEDNyjPloJud+w==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=14" @@ -11905,35 +23651,30 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.1", @@ -11944,35 +23685,30 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@sindresorhus/is": { @@ -12020,7 +23756,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)" }, @@ -12353,6 +24091,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", @@ -13021,6 +24807,25 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/@svgdotjs/svg.js": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.4.tgz", + "integrity": "sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Fuzzyma" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "license": "MIT", @@ -13191,11 +24996,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": "*" @@ -13263,6 +25068,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jaro-winkler": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@types/jaro-winkler/-/jaro-winkler-0.2.4.tgz", + "integrity": "sha512-TNVu6vL0Z3h+hYcW78IRloINA0y0MTVJ1PFVtVpBSgk+ejmaH5aVfcVghzNXZ0fa6gXe4zapNMQtMGWOJKTLig==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/js-yaml": { "version": "4.0.5", "dev": true, @@ -13469,6 +25281,13 @@ "@types/node": "*" } }, + "node_modules/@types/svgdom": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/svgdom/-/svgdom-0.1.2.tgz", + "integrity": "sha512-ZFwX8cDhbz6jiv3JZdMVYq8SSWHOUchChPmRoMwdIu3lz89aCu/gVK9TdR1eeb0ARQ8+5rtjUKrk1UR8hh0dhQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/tcp-port-used": { "version": "1.0.1", "dev": true, @@ -14628,6 +26447,53 @@ "node": ">= 8" } }, + "node_modules/aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "1.1.7", + "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.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "node_modules/are-we-there-yet/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==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/are-we-there-yet/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==", + "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==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/arg": { "version": "4.1.3", "dev": true, @@ -14736,6 +26602,130 @@ "node": ">= 10.0.0" } }, + "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": { + "@types/sinon": "^17.0.3", + "sinon": "^18.0.1", + "tslib": "^2.1.0" + } + }, + "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": ">=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": { "version": "8.0.0", "license": "MIT", @@ -14979,9 +26969,7 @@ }, "node_modules/bl": { "version": "4.1.0", - "dev": true, "license": "MIT", - "optional": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -14990,7 +26978,6 @@ }, "node_modules/bl/node_modules/buffer": { "version": "5.7.1", - "dev": true, "funding": [ { "type": "github", @@ -15006,7 +26993,6 @@ } ], "license": "MIT", - "optional": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -15103,6 +27089,15 @@ "version": "1.1.0", "license": "MIT" }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, "node_modules/browser-stdout": { "version": "1.3.1", "dev": true, @@ -15529,9 +27524,7 @@ }, "node_modules/chownr": { "version": "1.1.4", - "dev": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/chrome-trace-event": { "version": "1.0.3", @@ -15722,6 +27715,15 @@ "dev": true, "license": "MIT" }, + "node_modules/code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color": { "version": "3.2.1", "license": "MIT", @@ -15868,6 +27870,12 @@ "node": ">=0.8" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, "node_modules/content-disposition": { "version": "0.5.4", "dev": true, @@ -16296,9 +28304,7 @@ }, "node_modules/deep-extend": { "version": "0.6.0", - "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">=4.0.0" } @@ -16379,7 +28385,6 @@ }, "node_modules/delegates": { "version": "1.0.0", - "dev": true, "license": "MIT" }, "node_modules/depd": { @@ -16421,8 +28426,16 @@ "dev": true, "license": "MIT" }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, "node_modules/diff": { - "version": "5.1.0", + "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" @@ -17366,9 +29379,7 @@ }, "node_modules/expand-template": { "version": "2.0.3", - "dev": true, "license": "(MIT OR WTFPL)", - "optional": true, "engines": { "node": ">=6" } @@ -17493,7 +29504,6 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "dev": true, "license": "MIT" }, "node_modules/fast-diff": { @@ -17537,7 +29547,6 @@ }, "node_modules/fast-uri": { "version": "3.0.6", - "dev": true, "funding": [ { "type": "github", @@ -17756,6 +29765,23 @@ } } }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, "node_modules/for-each": { "version": "0.3.3", "license": "MIT", @@ -17822,9 +29848,7 @@ }, "node_modules/fs-constants": { "version": "1.0.0", - "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/fs-extra": { "version": "11.3.0", @@ -17890,6 +29914,70 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "node_modules/gauge/node_modules/ansi-regex": { + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gauge/node_modules/is-fullwidth-code-point": { + "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==", + "license": "MIT", + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gauge/node_modules/string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "license": "MIT", + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gauge/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/geometry-interfaces": { "version": "1.1.4", "dev": true, @@ -17959,9 +30047,7 @@ }, "node_modules/github-from-package": { "version": "0.0.0", - "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/glob": { "version": "10.3.10", @@ -18203,6 +30289,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, "node_modules/hash-base": { "version": "3.1.0", "license": "MIT", @@ -18313,7 +30405,6 @@ }, "node_modules/hpagent": { "version": "1.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -18579,6 +30670,21 @@ "node": ">= 4" } }, + "node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, "node_modules/immediate": { "version": "3.0.6", "dev": true, @@ -18648,9 +30754,7 @@ }, "node_modules/ini": { "version": "1.3.8", - "dev": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/internal-slot": { "version": "1.0.3", @@ -18832,7 +30936,6 @@ }, "node_modules/is-electron": { "version": "2.2.2", - "dev": true, "license": "MIT" }, "node_modules/is-extglob": { @@ -19155,6 +31258,12 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jaro-winkler": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/jaro-winkler/-/jaro-winkler-0.2.8.tgz", + "integrity": "sha512-yr+mElb6dWxA1mzFu0+26njV5DWAQRNTi5pB6fFMm79zHrfAs3d0qjhe/IpZI4AHIUJkzvu5QXQRWOw2O0GQyw==", + "license": "MIT" + }, "node_modules/jest-worker": { "version": "27.5.1", "dev": true, @@ -19825,10 +31934,9 @@ "license": "MIT" }, "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "dev": true, + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", + "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==", "license": "Apache-2.0" }, "node_modules/lowercase-keys": { @@ -19859,7 +31967,6 @@ }, "node_modules/mac-ca": { "version": "3.1.1", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "node-forge": "^1.3.1", @@ -20231,9 +32338,7 @@ }, "node_modules/mkdirp-classic": { "version": "0.5.3", - "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/mocha": { "version": "10.1.0", @@ -20543,9 +32648,7 @@ }, "node_modules/napi-build-utils": { "version": "1.0.2", - "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -20644,7 +32747,6 @@ }, "node_modules/node-forge": { "version": "1.3.1", - "dev": true, "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" @@ -20655,6 +32757,12 @@ "dev": true, "license": "MIT" }, + "node_modules/noop-logger": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", + "integrity": "sha512-6kM8CLXvuW5crTxsAtva2YLrRrDaiTIkIePWs9moLHqbFWT94WpNFjwS/5dfLfECg5i/lkmw3aoqVidxt23TEQ==", + "license": "MIT" + }, "node_modules/normalize-package-data": { "version": "3.0.3", "dev": true, @@ -20698,6 +32806,19 @@ "node": ">=8" } }, + "node_modules/npmlog": { + "version": "4.1.2", + "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.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, "node_modules/nth-check": { "version": "2.1.1", "dev": true, @@ -20709,6 +32830,15 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/nunjucks": { "version": "3.2.4", "dev": true, @@ -20748,7 +32878,6 @@ }, "node_modules/object-assign": { "version": "4.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -21185,7 +33314,6 @@ }, "node_modules/pify": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -21557,10 +33685,9 @@ "license": "MIT" }, "node_modules/protobufjs": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.1.tgz", - "integrity": "sha512-3qx3IRjR9WPQKagdwrKjO3Gu8RgQR2qqw+1KnigWhoVjFqegIj1K3bP11sGqhxrO46/XL7lekuG4jmjL+4cLsw==", - "dev": true, + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -21681,6 +33808,15 @@ "dev": true, "license": "MIT" }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "dev": true, @@ -21754,9 +33890,7 @@ }, "node_modules/rc": { "version": "1.2.8", - "dev": true, "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "optional": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -21769,9 +33903,7 @@ }, "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", - "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -22016,6 +34148,117 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/registry-js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/registry-js/-/registry-js-1.16.1.tgz", + "integrity": "sha512-pQ2kD36lh+YNtpaXm6HCCb0QZtV/zQEeKnkfEIj5FDSpF/oFts7pwizEUkWSvP8IbGb4A4a5iBhhS9eUearMmQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^3.2.1", + "prebuild-install": "^5.3.5" + } + }, + "node_modules/registry-js/node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "license": "MIT", + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/registry-js/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/registry-js/node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/registry-js/node_modules/node-abi": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", + "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", + "license": "MIT", + "dependencies": { + "semver": "^5.4.1" + } + }, + "node_modules/registry-js/node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "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==", + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^2.7.0", + "noop-logger": "^0.1.1", + "npmlog": "^4.0.1", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^3.0.3", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0", + "which-pm-runs": "^1.0.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/registry-js/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/registry-js/node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "license": "MIT", + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/regjsparser": { "version": "0.10.0", "dev": true, @@ -22110,7 +34353,6 @@ }, "node_modules/require-from-string": { "version": "2.0.2", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -22214,6 +34456,12 @@ "lowercase-keys": "^2.0.0" } }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, "node_modules/retry": { "version": "0.13.1", "dev": true, @@ -22301,7 +34549,6 @@ }, "node_modules/rxjs": { "version": "7.8.2", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" @@ -22609,6 +34856,12 @@ "node": ">= 0.8" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/set-function-length": { "version": "1.2.2", "license": "MIT", @@ -22713,12 +34966,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", @@ -22733,8 +34984,7 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/simple-get": { "version": "4.0.1", @@ -23185,6 +35435,27 @@ "svg2ttf": "svg2ttf.js" } }, + "node_modules/svgdom": { + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/svgdom/-/svgdom-0.1.21.tgz", + "integrity": "sha512-PrMx2aEzjRgyK9nbff6/NOzNmGcRnkjwO9p3JnHISmqPTMGtBPi4uFp59fVhI9PqRp8rVEWgmXFbkgYRsTnapg==", + "license": "MIT", + "dependencies": { + "fontkit": "^2.0.4", + "image-size": "^1.2.1", + "sax": "^1.4.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Fuzzyma" + } + }, + "node_modules/svgdom/node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC" + }, "node_modules/svgicons2svgfont": { "version": "10.0.6", "dev": true, @@ -23272,9 +35543,7 @@ }, "node_modules/tar-fs": { "version": "2.1.1", - "dev": true, "license": "MIT", - "optional": true, "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -23284,9 +35553,7 @@ }, "node_modules/tar-stream": { "version": "2.2.0", - "dev": true, "license": "MIT", - "optional": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -23505,6 +35772,12 @@ "next-tick": "1" } }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/tmp": { "version": "0.2.1", "dev": true, @@ -23651,7 +35924,9 @@ } }, "node_modules/tslib": { - "version": "2.6.2", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/tsscmp": { @@ -23713,9 +35988,7 @@ }, "node_modules/tunnel-agent": { "version": "0.6.0", - "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "safe-buffer": "^5.0.1" }, @@ -23847,7 +36120,6 @@ }, "node_modules/undici": { "version": "6.21.2", - "dev": true, "license": "MIT", "engines": { "node": ">=18.17" @@ -23861,6 +36133,32 @@ "version": "1.1.0", "license": "MIT" }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, "node_modules/universalify": { "version": "2.0.1", "dev": true, @@ -24792,6 +37090,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-pm-runs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", + "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/which-typed-array": { "version": "1.1.8", "license": "MIT", @@ -24810,6 +37117,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/wildcard": { "version": "2.0.0", "dev": true, @@ -24817,7 +37133,6 @@ }, "node_modules/win-ca": { "version": "3.5.1", - "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -24829,7 +37144,6 @@ }, "node_modules/win-ca/node_modules/make-dir": { "version": "1.3.0", - "dev": true, "license": "MIT", "dependencies": { "pify": "^3.0.0" @@ -24953,7 +37267,6 @@ }, "node_modules/ws": { "version": "8.17.1", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -25291,7 +37604,7 @@ }, "packages/amazonq": { "name": "amazon-q-vscode", - "version": "1.76.0-SNAPSHOT", + "version": "1.103.0-SNAPSHOT", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" @@ -25309,6 +37622,8 @@ "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/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", @@ -25316,25 +37631,40 @@ "@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-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/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", "@aws-sdk/s3-request-presigner": "<3.731.0", "@aws-sdk/smithy-client": "<3.731.0", "@aws-sdk/util-arn-parser": "<3.731.0", - "@aws/mynah-ui": "^4.34.1", + "@aws/mynah-ui": "^4.35.4", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/fetch-http-handler": "^5.0.1", @@ -25344,6 +37674,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", @@ -25362,6 +37693,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", @@ -25371,9 +37703,11 @@ "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", @@ -25384,15 +37718,16 @@ "whatwg-url": "^14.0.0", "winston": "^3.11.0", "winston-transport": "^4.6.0", + "ws": "^8.16.0", "xml2js": "^0.6.1", "yaml-cfn": "^0.3.2" }, "devDependencies": { "@aws-sdk/types": "^3.13.1", "@aws/chat-client": "^0.1.4", - "@aws/chat-client-ui-types": "^0.1.24", - "@aws/language-server-runtimes": "^0.2.81", - "@aws/language-server-runtimes-types": "^0.1.28", + "@aws/chat-client-ui-types": "^0.1.47", + "@aws/language-server-runtimes": "^0.2.119", + "@aws/language-server-runtimes-types": "^0.1.47", "@cspotcode/source-map-support": "^0.8.1", "@sinonjs/fake-timers": "^10.0.2", "@types/adm-zip": "^0.4.34", @@ -25418,11 +37753,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", "@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", @@ -25707,6 +38044,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", @@ -27005,7 +39771,7 @@ }, "packages/toolkit": { "name": "aws-toolkit-vscode", - "version": "3.66.0-SNAPSHOT", + "version": "3.83.0-SNAPSHOT", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" @@ -27020,6 +39786,7 @@ "version": "1.0.0", "license": "Apache-2.0", "devDependencies": { + "@types/eslint": "^8.56.0", "mocha": "^10.1.0" }, "engines": { @@ -28664,10 +41431,6 @@ "tree-kill": "cli.js" } }, - "src.gen/@amzn/amazon-q-developer-streaming-client/node_modules/tslib": { - "version": "2.8.1", - "license": "0BSD" - }, "src.gen/@amzn/amazon-q-developer-streaming-client/node_modules/typescript": { "version": "5.2.2", "dev": true, @@ -30218,10 +42981,6 @@ "tree-kill": "cli.js" } }, - "src.gen/@amzn/codewhisperer-streaming/node_modules/tslib": { - "version": "2.8.1", - "license": "0BSD" - }, "src.gen/@amzn/codewhisperer-streaming/node_modules/typescript": { "version": "5.2.2", "dev": true, diff --git a/package.json b/package.json index 751144b9f47..dd196da079f 100644 --- a/package.json +++ b/package.json @@ -38,13 +38,15 @@ "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.323", + "@aws-toolkits/telemetry": "^1.0.329", "@playwright/browser-chromium": "^1.43.1", "@stylistic/eslint-plugin": "^2.11.0", "@types/he": "^1.2.3", + "@types/jaro-winkler": "^0.2.4", "@types/vscode": "^1.68.0", "@types/vscode-webview": "^1.57.1", "@types/webpack-env": "^1.18.5", @@ -73,7 +75,9 @@ "webpack-merge": "^5.10.0" }, "dependencies": { + "@aws/language-server-runtimes": "^0.2.128", "@types/node": "^22.7.5", + "jaro-winkler": "^0.2.8", "vscode-nls": "^5.2.0", "vscode-nls-dev": "^4.0.4" } 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.76.0.json b/packages/amazonq/.changes/1.76.0.json new file mode 100644 index 00000000000..eaa2ce8af56 --- /dev/null +++ b/packages/amazonq/.changes/1.76.0.json @@ -0,0 +1,18 @@ +{ + "date": "2025-06-18", + "version": "1.76.0", + "entries": [ + { + "type": "Bug Fix", + "description": "/transform: only show lines of code statistic in plan" + }, + { + "type": "Feature", + "description": "Add model selection feature" + }, + { + "type": "Feature", + "description": "Pin context items in chat and manage workspace rules" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.77.0.json b/packages/amazonq/.changes/1.77.0.json new file mode 100644 index 00000000000..37436c259f9 --- /dev/null +++ b/packages/amazonq/.changes/1.77.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-06-18", + "version": "1.77.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.78.0.json b/packages/amazonq/.changes/1.78.0.json new file mode 100644 index 00000000000..9a6f35cf36f --- /dev/null +++ b/packages/amazonq/.changes/1.78.0.json @@ -0,0 +1,10 @@ +{ + "date": "2025-06-20", + "version": "1.78.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Resolve missing chat options in Amazon Q chat interface." + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.79.0.json b/packages/amazonq/.changes/1.79.0.json new file mode 100644 index 00000000000..51d910cca2b --- /dev/null +++ b/packages/amazonq/.changes/1.79.0.json @@ -0,0 +1,18 @@ +{ + "date": "2025-06-25", + "version": "1.79.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Added automatic system certificate detection and VSCode proxy settings support" + }, + { + "type": "Bug Fix", + "description": "Improved Amazon Linux 2 support with better SageMaker environment detection" + }, + { + "type": "Feature", + "description": "/transform: run all builds client-side" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.80.0.json b/packages/amazonq/.changes/1.80.0.json new file mode 100644 index 00000000000..20e948b69f2 --- /dev/null +++ b/packages/amazonq/.changes/1.80.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-07-01", + "version": "1.80.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.81.0.json b/packages/amazonq/.changes/1.81.0.json new file mode 100644 index 00000000000..b93c5693ad4 --- /dev/null +++ b/packages/amazonq/.changes/1.81.0.json @@ -0,0 +1,10 @@ +{ + "date": "2025-07-02", + "version": "1.81.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Stop auto inline completion when deleting code" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.82.0.json b/packages/amazonq/.changes/1.82.0.json new file mode 100644 index 00000000000..816da045f4a --- /dev/null +++ b/packages/amazonq/.changes/1.82.0.json @@ -0,0 +1,10 @@ +{ + "date": "2025-07-07", + "version": "1.82.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Prompt re-authenticate if auto trigger failed with expired token" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.83.0.json b/packages/amazonq/.changes/1.83.0.json new file mode 100644 index 00000000000..5997b2b1b95 --- /dev/null +++ b/packages/amazonq/.changes/1.83.0.json @@ -0,0 +1,18 @@ +{ + "date": "2025-07-09", + "version": "1.83.0", + "entries": [ + { + "type": "Feature", + "description": "Amazon Q /test, /doc, and /dev capabilities integrated into Agentic coding." + }, + { + "type": "Feature", + "description": "Added image support to Amazon Q chat, users can now upload images from their local file system" + }, + { + "type": "Removal", + "description": "Deprecate \"amazon q is generating...\" UI for inline suggestion" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.84.0.json b/packages/amazonq/.changes/1.84.0.json new file mode 100644 index 00000000000..e73a685e054 --- /dev/null +++ b/packages/amazonq/.changes/1.84.0.json @@ -0,0 +1,18 @@ +{ + "date": "2025-07-17", + "version": "1.84.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Slightly delay rendering inline completion when user is typing" + }, + { + "type": "Bug Fix", + "description": "Render first response before receiving all paginated inline completion results" + }, + { + "type": "Feature", + "description": "Explain and Fix for any issue in Code Issues panel will pull the experience into chat. Also no more view details tab." + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.85.0.json b/packages/amazonq/.changes/1.85.0.json new file mode 100644 index 00000000000..b0aba38025b --- /dev/null +++ b/packages/amazonq/.changes/1.85.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-07-19", + "version": "1.85.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.86.0.json b/packages/amazonq/.changes/1.86.0.json new file mode 100644 index 00000000000..abe84ce5b5f --- /dev/null +++ b/packages/amazonq/.changes/1.86.0.json @@ -0,0 +1,18 @@ +{ + "date": "2025-07-30", + "version": "1.86.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Let Enter invoke auto completion more consistently" + }, + { + "type": "Bug Fix", + "description": "Faster and more responsive inline completion UX" + }, + { + "type": "Bug Fix", + "description": "Use documentChangeEvent as auto trigger condition" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.87.0.json b/packages/amazonq/.changes/1.87.0.json new file mode 100644 index 00000000000..d80e11a2bfa --- /dev/null +++ b/packages/amazonq/.changes/1.87.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-07-31", + "version": "1.87.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.88.0.json b/packages/amazonq/.changes/1.88.0.json new file mode 100644 index 00000000000..05e006954d8 --- /dev/null +++ b/packages/amazonq/.changes/1.88.0.json @@ -0,0 +1,14 @@ +{ + "date": "2025-08-06", + "version": "1.88.0", + "entries": [ + { + "type": "Feature", + "description": "Amazon Q Chat provides error explanations and fixes when hovering or right-clicking on error indicators and messages" + }, + { + "type": "Feature", + "description": "/transform: Show transformation history in Transformation Hub and allow users to resume jobs" + } + ] +} \ 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/.changes/next-release/Bug Fix-665b0f02-d6fe-4cfc-ac52-564f35d12aa5.json b/packages/amazonq/.changes/next-release/Bug Fix-665b0f02-d6fe-4cfc-ac52-564f35d12aa5.json deleted file mode 100644 index 7e59d131c8d..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-665b0f02-d6fe-4cfc-ac52-564f35d12aa5.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "/transform: only show lines of code statistic in plan" -} diff --git a/packages/amazonq/.changes/next-release/Feature-ab31cbb6-3fe4-4ee3-a0a3-290430277856.json b/packages/amazonq/.changes/next-release/Feature-ab31cbb6-3fe4-4ee3-a0a3-290430277856.json new file mode 100644 index 00000000000..71c1583e77b --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-ab31cbb6-3fe4-4ee3-a0a3-290430277856.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "Q CodeTransformation: add more job metadata to history table" +} diff --git a/packages/amazonq/CHANGELOG.md b/packages/amazonq/CHANGELOG.md index 82ac8b6440c..361633276b5 100644 --- a/packages/amazonq/CHANGELOG.md +++ b/packages/amazonq/CHANGELOG.md @@ -1,3 +1,125 @@ +## 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 +- **Feature** /transform: Show transformation history in Transformation Hub and allow users to resume jobs + +## 1.87.0 2025-07-31 + +- Miscellaneous non-user-facing changes + +## 1.86.0 2025-07-30 + +- **Bug Fix** Let Enter invoke auto completion more consistently +- **Bug Fix** Faster and more responsive inline completion UX +- **Bug Fix** Use documentChangeEvent as auto trigger condition + +## 1.85.0 2025-07-19 + +- Miscellaneous non-user-facing changes + +## 1.84.0 2025-07-17 + +- **Bug Fix** Slightly delay rendering inline completion when user is typing +- **Bug Fix** Render first response before receiving all paginated inline completion results +- **Feature** Explain and Fix for any issue in Code Issues panel will pull the experience into chat. Also no more view details tab. + +## 1.83.0 2025-07-09 + +- **Feature** Amazon Q /test, /doc, and /dev capabilities integrated into Agentic coding. +- **Feature** Added image support to Amazon Q chat, users can now upload images from their local file system +- **Removal** Deprecate "amazon q is generating..." UI for inline suggestion + +## 1.82.0 2025-07-07 + +- **Bug Fix** Prompt re-authenticate if auto trigger failed with expired token + +## 1.81.0 2025-07-02 + +- **Bug Fix** Stop auto inline completion when deleting code + +## 1.80.0 2025-07-01 + +- Miscellaneous non-user-facing changes + +## 1.79.0 2025-06-25 + +- **Bug Fix** Added automatic system certificate detection and VSCode proxy settings support +- **Bug Fix** Improved Amazon Linux 2 support with better SageMaker environment detection +- **Feature** /transform: run all builds client-side + +## 1.78.0 2025-06-20 + +- **Bug Fix** Resolve missing chat options in Amazon Q chat interface. + +## 1.77.0 2025-06-18 + +- Miscellaneous non-user-facing changes + +## 1.76.0 2025-06-18 + +- **Bug Fix** /transform: only show lines of code statistic in plan +- **Feature** Add model selection feature +- **Feature** Pin context items in chat and manage workspace rules + ## 1.75.0 2025-06-13 - Miscellaneous non-user-facing changes diff --git a/packages/amazonq/README.md b/packages/amazonq/README.md index 46091a98d10..e3ec16bb2ac 100644 --- a/packages/amazonq/README.md +++ b/packages/amazonq/README.md @@ -3,39 +3,33 @@ [![Youtube Channel Views](https://img.shields.io/youtube/channel/views/UCd6MoB9NC6uYN2grvUNT-Zg?style=flat-square&logo=youtube&label=Youtube)](https://www.youtube.com/@amazonwebservices) ![Marketplace Installs](https://img.shields.io/vscode-marketplace/i/AmazonWebServices.amazon-q-vscode.svg?label=Installs&style=flat-square) -# Agent capabilities +# Agentic coding experience + +Amazon Q Developer uses information across native and MCP server-based tools to intelligently perform actions beyond code suggestions, such as reading files, generating code diffs, and running commands based on your natural language instruction. Simply type your prompt in your preferred language and Q Developer will provide continuous status updates and iteratively apply changes based on your feedback, helping you accomplish tasks faster. ### Implement new features -`/dev` to task Amazon Q with generating new code across your entire project and implement features. +Generate new code across your entire project and implement features. ### Generate documentation -`/doc` to task Amazon Q with writing API, technical design, and onboarding documentation. +Write API, technical design, and onboarding documentation. ### Automate code reviews -`/review` to ask Amazon Q to perform code reviews, flagging suspicious code patterns and assessing deployment risk. +Perform code reviews, flagging suspicious code patterns and assessing deployment risk. ### Generate unit tests -`/test` to ask Amazon Q to generate unit tests and add them to your project, helping you improve code quality, fast. - -### Transform workloads - -`/transform` to upgrade your Java applications in minutes, not weeks. +Generate unit tests and add them to your project, helping you improve code quality, fast.
# Core features -### Inline chat - -Seamlessly initiate chat within the inline coding experience. Select a section of code that you need assistance with and initiate chat within the editor to request actions such as "Optimize this code", "Add comments", or "Write tests". - -### Chat +### MCP support -Generate code, explain code, and get answers about software development. +Add Model Context Protocol (MCP) servers to give Amazon Q Developer access to important context. ### Inline suggestions @@ -43,9 +37,13 @@ Receive real-time code suggestions ranging from snippets to full functions based [_15+ languages supported including Python, TypeScript, Rust, Terraform, AWS Cloudformation, and more_](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/q-language-ide-support.html) -### Code reference log +### Inline chat + +Seamlessly chat within the inline coding experience. Select a section of code that you need assistance with and initiate chat within the editor to request actions such as "Optimize this code", "Add comments", or "Write tests". -Attribute code from Amazon Q that is similar to training data. When code suggestions similar to training data are accepted, they will be added to the code reference log. +### Chat + +Generate code, explain code, and get answers about software development.
@@ -55,8 +53,6 @@ Attribute code from Amazon Q that is similar to training data. When code suggest **Pro Tier** - if your organization is on the Amazon Q Developer Pro tier, log in with single sign-on. -![Authentication gif](https://raw.githubusercontent.com/aws/aws-toolkit-vscode/HEAD/docs/marketplace/vscode/amazonq/auth-Q.gif) - # Troubleshooting & feedback [File a bug](https://github.com/aws/aws-toolkit-vscode/issues/new?assignees=&labels=bug&projects=&template=bug_report.md) or [submit a feature request](https://github.com/aws/aws-toolkit-vscode/issues/new?assignees=&labels=feature-request&projects=&template=feature_request.md) on our Github repository. diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index c3e525cf42b..3e239d0cd0e 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -1,8 +1,8 @@ { "name": "amazon-q-vscode", "displayName": "Amazon Q", - "description": "The most capable generative AI-powered assistant for building, operating, and transforming software, with advanced capabilities for managing data and AI", - "version": "1.76.0-SNAPSHOT", + "description": "The most capable generative AI–powered assistant for software development.", + "version": "1.103.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 } } }, @@ -408,6 +413,11 @@ "when": "(view == aws.amazonq.AmazonQChatView) && aws.codewhisperer.connected && !aws.isSageMakerUnifiedStudio", "group": "2_amazonQ@4" }, + { + "command": "aws.amazonq.showLogs", + "when": "!aws.isSageMakerUnifiedStudio", + "group": "1_amazonQ@5" + }, { "command": "aws.amazonq.reconnect", "when": "(view == aws.amazonq.AmazonQChatView) && aws.codewhisperer.connectionExpired", @@ -442,17 +452,22 @@ }, { "command": "aws.amazonq.openSecurityIssuePanel", + "when": "false && view == aws.amazonq.SecurityIssuesTree && (viewItem == issueWithoutFix || viewItem == issueWithFix || viewItem == issueWithFixDisabled)", + "group": "inline@4" + }, + { + "command": "aws.amazonq.security.explain", "when": "view == aws.amazonq.SecurityIssuesTree && (viewItem == issueWithoutFix || viewItem == issueWithFix || viewItem == issueWithFixDisabled)", "group": "inline@4" }, { - "command": "aws.amazonq.security.ignore", + "command": "aws.amazonq.security.generateFix", "when": "view == aws.amazonq.SecurityIssuesTree && (viewItem == issueWithoutFix || viewItem == issueWithFix || viewItem == issueWithFixDisabled)", "group": "inline@5" }, { - "command": "aws.amazonq.security.generateFix", - "when": "view == aws.amazonq.SecurityIssuesTree && viewItem == issueWithoutFix", + "command": "aws.amazonq.security.ignore", + "when": "view == aws.amazonq.SecurityIssuesTree && (viewItem == issueWithoutFix || viewItem == issueWithFix || viewItem == issueWithFixDisabled)", "group": "inline@6" }, { @@ -514,37 +529,33 @@ "command": "aws.amazonq.walkthrough.show", "group": "1_help@1" }, - { - "command": "aws.amazonq.exploreAgents", - "when": "!aws.isSageMaker", - "group": "1_help@2" - }, { "command": "aws.amazonq.github", - "group": "1_help@3" + "group": "1_help@2" }, { "command": "aws.amazonq.aboutExtension", - "group": "1_help@4" + "group": "1_help@3" }, { "command": "aws.amazonq.viewLogs", - "group": "1_help@5" + "group": "1_help@4" } ], "aws.amazonq.submenu.securityIssueMoreActions": [ { "command": "aws.amazonq.security.explain", + "when": "false", "group": "1_more@1" }, { "command": "aws.amazonq.applySecurityFix", - "when": "view == aws.amazonq.SecurityIssuesTree && viewItem == issueWithFix", + "when": "false && view == aws.amazonq.SecurityIssuesTree && viewItem == issueWithFix", "group": "1_more@3" }, { "command": "aws.amazonq.security.regenerateFix", - "when": "view == aws.amazonq.SecurityIssuesTree && viewItem == issueWithFix", + "when": "false && view == aws.amazonq.SecurityIssuesTree && viewItem == issueWithFix", "group": "1_more@4" }, { @@ -554,6 +565,21 @@ ] }, "commands": [ + { + "command": "aws.amazonq.stopCmdExecution", + "title": "Stop Amazon Q", + "category": "%AWS.amazonq.title%" + }, + { + "command": "aws.amazonq.runCmdExecution", + "title": "Run Amazon Q Tool", + "category": "%AWS.amazonq.title%" + }, + { + "command": "aws.amazonq.rejectCmdExecution", + "title": "Reject Amazon Q Tool", + "category": "%AWS.amazonq.title%" + }, { "command": "_aws.amazonq.notifications.dismiss", "title": "%AWS.generic.dismiss%", @@ -567,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%", @@ -615,6 +635,11 @@ "category": "%AWS.amazonq.title%", "enablement": "aws.codewhisperer.connected" }, + { + "command": "aws.amazonq.showLogs", + "title": "%AWS.command.codewhisperer.showLogs%", + "category": "%AWS.amazonq.title%" + }, { "command": "aws.amazonq.selectRegionProfile", "title": "Change Profile", @@ -720,7 +745,7 @@ }, { "command": "aws.amazonq.showHistoryInHub", - "title": "%AWS.command.q.transform.viewJobStatus%" + "title": "%AWS.command.q.transform.viewJobHistory%" }, { "command": "aws.amazonq.selectCustomization", @@ -778,6 +803,7 @@ { "command": "aws.amazonq.security.explain", "title": "%AWS.command.amazonq.explainIssue%", + "icon": "$(search)", "enablement": "view == aws.amazonq.SecurityIssuesTree" }, { @@ -814,12 +840,6 @@ "title": "%AWS.amazonq.openChat%", "category": "%AWS.amazonq.title%" }, - { - "command": "aws.amazonq.exploreAgents", - "title": "%AWS.amazonq.exploreAgents%", - "category": "%AWS.amazonq.title%", - "enablement": "aws.codewhisperer.connected && !aws.isSageMaker" - }, { "command": "aws.amazonq.walkthrough.show", "title": "%AWS.amazonq.welcomeWalkthrough%" @@ -828,15 +848,63 @@ "command": "aws.amazonq.clearCache", "title": "%AWS.amazonq.clearCache%", "category": "%AWS.amazonq.title%" + }, + { + "command": "aws.amazonq.inline.acceptEdit", + "title": "%AWS.amazonq.inline.acceptEdit%", + "category": "%AWS.amazonq.title%", + "enablement": "aws.codewhisperer.connected" + }, + { + "command": "aws.amazonq.inline.rejectEdit", + "title": "%AWS.amazonq.inline.rejectEdit%", + "category": "%AWS.amazonq.title%", + "enablement": "aws.codewhisperer.connected" + }, + { + "command": "aws.amazonq.toggleNextEditPredictionPanel", + "title": "%AWS.amazonq.toggleNextEditPredictionPanel%", + "category": "%AWS.amazonq.title%", + "enablement": "aws.codewhisperer.connected" } ], "keybindings": [ + { + "command": "aws.amazonq.stopCmdExecution", + "key": "ctrl+shift+backspace", + "mac": "cmd+shift+backspace", + "when": "aws.amazonq.amazonqChatLSP.isFocus" + }, + { + "command": "aws.amazonq.runCmdExecution", + "key": "ctrl+shift+enter", + "mac": "cmd+shift+enter", + "when": "aws.amazonq.amazonqChatLSP.isFocus" + }, + { + "command": "aws.amazonq.rejectCmdExecution", + "key": "ctrl+shift+r", + "mac": "cmd+shift+r", + "when": "aws.amazonq.amazonqChatLSP.isFocus" + }, { "command": "_aws.amazonq.focusChat.keybinding", "win": "win+alt+i", "mac": "cmd+alt+i", "linux": "meta+alt+i" }, + { + "command": "aws.amazonq.inline.debugAcceptEdit", + "key": "ctrl+alt+a", + "mac": "cmd+alt+a", + "when": "editorTextFocus" + }, + { + "command": "aws.amazonq.inline.debugRejectEdit", + "key": "ctrl+alt+r", + "mac": "cmd+alt+r", + "when": "editorTextFocus" + }, { "command": "aws.amazonq.explainCode", "win": "win+alt+e", @@ -851,7 +919,7 @@ }, { "command": "aws.amazonq.fixCode", - "win": "win+alt+y", + "win": "win+alt+h", "mac": "cmd+alt+y", "linux": "meta+alt+y" }, @@ -869,7 +937,7 @@ }, { "command": "aws.amazonq.generateUnitTests", - "key": "win+alt+t", + "key": "win+alt+n", "mac": "cmd+alt+t", "linux": "meta+alt+t" }, @@ -893,12 +961,16 @@ }, { "key": "right", - "command": "editor.action.inlineSuggest.showNext", + "command": "aws.amazonq.showNext", "when": "inlineSuggestionVisible && !editorReadonly && aws.codewhisperer.connected" }, { "key": "left", - "command": "editor.action.inlineSuggest.showPrevious", + "command": "aws.amazonq.showPrev", + "when": "inlineSuggestionVisible && !editorReadonly && aws.codewhisperer.connected" + }, + { + "command": "aws.amazonq.checkInlineSuggestionVisibility", "when": "inlineSuggestionVisible && !editorReadonly && aws.codewhisperer.connected" }, { @@ -917,6 +989,16 @@ "command": "aws.amazonq.inline.waitForUserDecisionRejectAll", "key": "escape", "when": "editorTextFocus && aws.codewhisperer.connected && amazonq.inline.codelensShortcutEnabled" + }, + { + "command": "aws.amazonq.inline.acceptEdit", + "key": "tab", + "when": "editorTextFocus && aws.amazonq.editSuggestionActive" + }, + { + "command": "aws.amazonq.inline.rejectEdit", + "key": "escape", + "when": "editorTextFocus && aws.amazonq.editSuggestionActive" } ], "icons": { @@ -1193,110 +1275,173 @@ "fontCharacter": "\\f1d0" } }, - "aws-lambda-function": { + "aws-lambda-create-stack": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d1" } }, - "aws-mynah-MynahIconBlack": { + "aws-lambda-create-stack-light": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d2" } }, - "aws-mynah-MynahIconWhite": { + "aws-lambda-function": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d3" } }, - "aws-mynah-logo": { + "aws-mynah-MynahIconBlack": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d4" } }, - "aws-redshift-cluster": { + "aws-mynah-MynahIconWhite": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d5" } }, - "aws-redshift-cluster-connected": { + "aws-mynah-logo": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d6" } }, - "aws-redshift-database": { + "aws-redshift-cluster": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d7" } }, - "aws-redshift-redshift-cluster-connected": { + "aws-redshift-cluster-connected": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d8" } }, - "aws-redshift-schema": { + "aws-redshift-database": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d9" } }, - "aws-redshift-table": { + "aws-redshift-redshift-cluster-connected": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1da" } }, - "aws-s3-bucket": { + "aws-redshift-schema": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1db" } }, - "aws-s3-create-bucket": { + "aws-redshift-table": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1dc" } }, - "aws-schemas-registry": { + "aws-s3-bucket": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1dd" } }, - "aws-schemas-schema": { + "aws-s3-create-bucket": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1de" } }, - "aws-stepfunctions-preview": { + "aws-sagemaker-code-editor": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1df" } + }, + "aws-sagemaker-jupyter-lab": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e0" + } + }, + "aws-sagemakerunifiedstudio-catalog": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e1" + } + }, + "aws-sagemakerunifiedstudio-spaces": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e2" + } + }, + "aws-sagemakerunifiedstudio-spaces-dark": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e3" + } + }, + "aws-sagemakerunifiedstudio-symbol-int": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e4" + } + }, + "aws-sagemakerunifiedstudio-table": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e5" + } + }, + "aws-schemas-registry": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e6" + } + }, + "aws-schemas-schema": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e7" + } + }, + "aws-stepfunctions-preview": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e8" + } } }, "walkthroughs": [ @@ -1325,17 +1470,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 21857163bd2..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,7 +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' export function init(appContext: AmazonQAppInitContext) { const scanChatControllerEventEmitters: ScanChatControllerEventEmitters = { @@ -49,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 = '' @@ -74,18 +65,6 @@ export function init(appContext: AmazonQAppInitContext) { return debouncedEvent() }) - 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/amazonqScan/models/constants.ts b/packages/amazonq/src/app/amazonqScan/models/constants.ts index 93e815884e1..4180b130b78 100644 --- a/packages/amazonq/src/app/amazonqScan/models/constants.ts +++ b/packages/amazonq/src/app/amazonqScan/models/constants.ts @@ -97,3 +97,5 @@ const getIconForStep = (targetStep: number, currentStep: number) => { ? checkIcons.done : checkIcons.wait } + +export const codeReviewInChat = true 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/chat/node/activateAgents.ts b/packages/amazonq/src/app/chat/node/activateAgents.ts index 954f2892eda..cd0309d7f2d 100644 --- a/packages/amazonq/src/app/chat/node/activateAgents.ts +++ b/packages/amazonq/src/app/chat/node/activateAgents.ts @@ -11,9 +11,6 @@ export function activateAgents() { const appInitContext = DefaultAmazonQAppInitContext.instance amazonqNode.cwChatAppInit(appInitContext) - amazonqNode.featureDevChatAppInit(appInitContext) amazonqNode.gumbyChatAppInit(appInitContext) - amazonqNode.testChatAppInit(appInitContext) - amazonqNode.docChatAppInit(appInitContext) scanChatAppInit(appInitContext) } diff --git a/packages/amazonq/src/app/inline/EditRendering/diffUtils.ts b/packages/amazonq/src/app/inline/EditRendering/diffUtils.ts new file mode 100644 index 00000000000..7f9ca54b6aa --- /dev/null +++ b/packages/amazonq/src/app/inline/EditRendering/diffUtils.ts @@ -0,0 +1,93 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +// TODO: deprecate this file in favor of core/shared/utils/diffUtils +import { applyPatch } from 'diff' + +export type LineDiff = + | { type: 'added'; content: string } + | { type: 'removed'; content: string } + | { type: 'modified'; before: string; after: string } + +/** + * Apply a unified diff to original code to generate modified code + * @param originalCode The original code as a string + * @param unifiedDiff The unified diff content + * @returns The modified code after applying the diff + */ +export function applyUnifiedDiff(docText: string, unifiedDiff: string): string { + try { + // First try the standard diff package + try { + const result = applyPatch(docText, unifiedDiff) + if (result !== false) { + return result + } + } catch (error) {} + + // Parse the unified diff to extract the changes + const diffLines = unifiedDiff.split('\n') + let result = docText + + // Find all hunks in the diff + const hunkStarts = diffLines + .map((line, index) => (line.startsWith('@@ ') ? index : -1)) + .filter((index) => index !== -1) + + // Process each hunk + for (const hunkStart of hunkStarts) { + // Parse the hunk header + const hunkHeader = diffLines[hunkStart] + const match = hunkHeader.match(/@@ -(\d+),(\d+) \+(\d+),(\d+) @@/) + + if (!match) { + continue + } + + const oldStart = parseInt(match[1]) + const oldLines = parseInt(match[2]) + + // Extract the content lines for this hunk + let i = hunkStart + 1 + const contentLines = [] + while (i < diffLines.length && !diffLines[i].startsWith('@@')) { + contentLines.push(diffLines[i]) + i++ + } + + // Build the old and new text + let oldText = '' + let newText = '' + + for (const line of contentLines) { + if (line.startsWith('-')) { + oldText += line.substring(1) + '\n' + } else if (line.startsWith('+')) { + newText += line.substring(1) + '\n' + } else if (line.startsWith(' ')) { + oldText += line.substring(1) + '\n' + newText += line.substring(1) + '\n' + } + } + + // Remove trailing newline if it was added + oldText = oldText.replace(/\n$/, '') + newText = newText.replace(/\n$/, '') + + // Find the text to replace in the document + const docLines = docText.split('\n') + const startLine = oldStart - 1 // Convert to 0-based + const endLine = startLine + oldLines + + // Extract the text that should be replaced + const textToReplace = docLines.slice(startLine, endLine).join('\n') + + // Replace the text + result = result.replace(textToReplace, newText) + } + return result + } catch (error) { + return docText // Return original text if all methods fail + } +} diff --git a/packages/amazonq/src/app/inline/EditRendering/displayImage.ts b/packages/amazonq/src/app/inline/EditRendering/displayImage.ts new file mode 100644 index 00000000000..7ccedc3489b --- /dev/null +++ b/packages/amazonq/src/app/inline/EditRendering/displayImage.ts @@ -0,0 +1,507 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getContext, getLogger, setContext } from 'aws-core-vscode/shared' +import * as vscode from 'vscode' +import { applyPatch, diffLines } from 'diff' +import { LanguageClient } from 'vscode-languageclient' +import { CodeWhispererSession } from '../sessionManager' +import { LogInlineCompletionSessionResultsParams } from '@aws/language-server-runtimes/protocol' +import { InlineCompletionItemWithReferences } from '@aws/language-server-runtimes/protocol' +import path from 'path' +import { imageVerticalOffset } from './svgGenerator' +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 + private removedCodeDecorationType: vscode.TextEditorDecorationType + private currentImageDecoration: vscode.DecorationOptions | undefined + private currentRemovedCodeDecorations: vscode.DecorationOptions[] = [] + private acceptHandler: (() => void) | undefined + private rejectHandler: ((isDiscard: boolean) => void) | undefined + + constructor() { + this.registerCommandHandlers() + this.imageDecorationType = vscode.window.createTextEditorDecorationType({ + isWholeLine: true, + }) + + this.removedCodeDecorationType = vscode.window.createTextEditorDecorationType({ + backgroundColor: 'rgba(255, 0, 0, 0.2)', + }) + } + + private imageToDecoration(image: vscode.Uri, range: vscode.Range) { + return { + range, + renderOptions: { + after: { + contentIconPath: image, + verticalAlign: 'text-top', + width: '100%', + height: 'auto', + margin: '1px 0', + }, + }, + hoverMessage: new vscode.MarkdownString('Edit suggestion. Press [Tab] to accept or [Esc] to reject.'), + } + } + + /** + * Highlights code that will be removed using the provided highlight ranges + * @param editor The active text editor + * @param startLine The line where the edit starts + * @param highlightRanges Array of ranges specifying which parts to highlight + * @returns Array of decoration options + */ + private highlightRemovedLines( + editor: vscode.TextEditor, + startLine: number, + highlightRanges: Array<{ line: number; start: number; end: number }> + ): vscode.DecorationOptions[] { + const decorations: vscode.DecorationOptions[] = [] + + // Group ranges by line for more efficient processing + const rangesByLine = new Map>() + + // Process each range and adjust line numbers relative to document + for (const range of highlightRanges) { + const documentLine = startLine + range.line + + // Skip if line is out of bounds + if (documentLine >= editor.document.lineCount) { + continue + } + + // Add to ranges map, grouped by line + if (!rangesByLine.has(documentLine)) { + rangesByLine.set(documentLine, []) + } + rangesByLine.get(documentLine)!.push({ + start: range.start, + end: range.end, + }) + } + + // Process each line with ranges + for (const [lineNumber, ranges] of rangesByLine.entries()) { + const lineLength = editor.document.lineAt(lineNumber).text.length + + if (ranges.length === 0) { + continue + } + + // Check if we should highlight the entire line + if (ranges.length === 1 && ranges[0].start === 0 && ranges[0].end >= lineLength) { + // Highlight entire line + const range = new vscode.Range( + new vscode.Position(lineNumber, 0), + new vscode.Position(lineNumber, lineLength) + ) + decorations.push({ range }) + } else { + // Create individual decorations for each range on this line + for (const range of ranges) { + const end = Math.min(range.end, lineLength) + if (range.start < end) { + const vsRange = new vscode.Range( + new vscode.Position(lineNumber, range.start), + new vscode.Position(lineNumber, end) + ) + decorations.push({ range: vsRange }) + } + } + } + } + + return decorations + } + + /** + * Displays an edit suggestion as an SVG image in the editor and highlights removed code + */ + public async displayEditSuggestion( + editor: vscode.TextEditor, + svgImage: vscode.Uri, + startLine: number, + onAccept: () => Promise, + onReject: (isDiscard: boolean) => Promise, + originalCode: string, + newCode: string, + originalCodeHighlightRanges: Array<{ line: number; start: number; end: number }> + ): Promise { + // 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 + + // Get the line text to determine the end position + const lineText = editor.document.lineAt(Math.max(0, startLine - imageVerticalOffset)).text + const endPosition = new vscode.Position(Math.max(0, startLine - imageVerticalOffset), lineText.length) + const range = new vscode.Range(endPosition, endPosition) + + this.currentImageDecoration = this.imageToDecoration(svgImage, range) + + // Apply image decoration + editor.setDecorations(this.imageDecorationType, [this.currentImageDecoration]) + + // Highlight removed code with red background + this.currentRemovedCodeDecorations = this.highlightRemovedLines(editor, startLine, originalCodeHighlightRanges) + editor.setDecorations(this.removedCodeDecorationType, this.currentRemovedCodeDecorations) + } + + /** + * Clears all edit suggestion decorations + */ + public async clearDecorations(editor: vscode.TextEditor): Promise { + editor.setDecorations(this.imageDecorationType, []) + editor.setDecorations(this.removedCodeDecorationType, []) + this.currentImageDecoration = undefined + this.currentRemovedCodeDecorations = [] + this.acceptHandler = undefined + this.rejectHandler = undefined + await setContext('aws.amazonq.editSuggestionActive' as any, false) + EditSuggestionState.setEditSuggestionActive(false) + } + + /** + * Registers command handlers for accepting/rejecting suggestions + */ + public registerCommandHandlers(): void { + // Register Tab key handler for accepting suggestion + vscode.commands.registerCommand('aws.amazonq.inline.acceptEdit', () => { + if (this.acceptHandler) { + this.acceptHandler() + } + }) + + // Register Esc key handler for rejecting suggestion + vscode.commands.registerCommand('aws.amazonq.inline.rejectEdit', (isDiscard: boolean = false) => { + if (this.rejectHandler) { + this.rejectHandler(isDiscard) + } + }) + } + + /** + * Disposes resources + */ + public dispose(): void { + this.imageDecorationType.dispose() + this.removedCodeDecorationType.dispose() + } + + // Use process-wide singleton to prevent multiple instances on Windows + static readonly decorationManagerKey = Symbol.for('aws.amazonq.decorationManager') + + static getDecorationManager(): EditDecorationManager { + const globalObj = global as any + if (!globalObj[this.decorationManagerKey]) { + globalObj[this.decorationManagerKey] = new EditDecorationManager() + } + return globalObj[this.decorationManagerKey] + } +} + +export const decorationManager = EditDecorationManager.getDecorationManager() + +/** + * Function to replace editor's content with new code + */ +async function replaceEditorContent(editor: vscode.TextEditor, newCode: string): Promise { + const document = editor.document + const fullRange = new vscode.Range( + 0, + 0, + document.lineCount - 1, + document.lineAt(document.lineCount - 1).text.length + ) + + await editor.edit((editBuilder) => { + editBuilder.replace(fullRange, newCode) + }) +} + +/** + * Calculates the end position of the actual edited content by finding the last changed part + */ +function getEndOfEditPosition(originalCode: string, newCode: string): vscode.Position { + const changes = diffLines(originalCode, newCode) + let lineOffset = 0 + + // Track the end position of the last added chunk + let lastChangeEndLine = 0 + let lastChangeEndColumn = 0 + let foundAddedContent = false + + for (const part of changes) { + if (part.added) { + foundAddedContent = true + + // Calculate lines in this added part + const lines = part.value.split('\n') + const linesCount = lines.length + + // Update position to the end of this added chunk + lastChangeEndLine = lineOffset + linesCount - 1 + + // Get the length of the last line in this added chunk + lastChangeEndColumn = lines[linesCount - 1].length + } + + // Update line offset (skip removed parts) + if (!part.removed) { + const partLineCount = part.value.split('\n').length + lineOffset += partLineCount - 1 + } + } + + // If we found added content, return position at the end of the last addition + if (foundAddedContent) { + return new vscode.Position(lastChangeEndLine, lastChangeEndColumn) + } + + // Fallback to current cursor position if no changes were found + const editor = vscode.window.activeTextEditor + return editor ? editor.selection.active : new vscode.Position(0, 0) +} + +/** + * Helper function to create discard telemetry params + */ +function createDiscardTelemetryParams( + session: CodeWhispererSession, + item: InlineCompletionItemWithReferences +): LogInlineCompletionSessionResultsParams { + return { + sessionId: session.sessionId, + completionSessionResult: { + [item.itemId]: { + seen: false, + accepted: false, + discarded: true, + }, + }, + totalSessionDisplayTime: Date.now() - session.requestStartTime, + firstCompletionDisplayLatency: session.firstCompletionDisplayLatency, + isInlineEdit: true, + } +} + +/** + * Helper function to display SVG decorations + */ +export async function displaySvgDecoration( + editor: vscode.TextEditor, + svgImage: vscode.Uri, + startLine: number, + newCode: string, + originalCodeHighlightRanges: Array<{ line: number; start: number; end: number }>, + session: CodeWhispererSession, + languageClient: LanguageClient, + item: InlineCompletionItemWithReferences, + 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) + 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) + 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) + 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) { + logSuggestionFailure('REJECT', 'Invalid patch due to document change', item.insertText as string) + 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) { + logSuggestionFailure( + 'REJECT', + `cursor position move too far away off ${autoRejectEditCursorDistance} lines`, + item.insertText as string + ) + void vscode.commands.executeCommand('aws.amazonq.inline.rejectEdit') + } + }) + await decorationManager.displayEditSuggestion( + editor, + svgImage, + startLine, + async () => { + // Handle accept + getLogger().info('Edit suggestion accepted') + + // Replace content + try { + vsCodeState.isCodeWhispererEditing = true + await replaceEditorContent(editor, newCode) + } finally { + vsCodeState.isCodeWhispererEditing = false + } + + // Move cursor to end of the actual changed content + const endPosition = getEndOfEditPosition(originalCode, newCode) + editor.selection = new vscode.Selection(endPosition, endPosition) + + await decorationManager.clearDecorations(editor) + documentChangeListener.dispose() + cursorChangeListener.dispose() + const params: LogInlineCompletionSessionResultsParams = { + sessionId: session.sessionId, + completionSessionResult: { + [item.itemId]: { + seen: true, + accepted: true, + discarded: false, + }, + }, + totalSessionDisplayTime: Date.now() - session.requestStartTime, + firstCompletionDisplayLatency: session.firstCompletionDisplayLatency, + isInlineEdit: true, + } + languageClient.sendNotification('aws/logInlineCompletionSessionResults', params) + session.triggerOnAcceptance = true + }, + async (isDiscard: boolean) => { + // Handle reject + if (isDiscard) { + getLogger().info('Edit suggestion discarded') + } else { + getLogger().info('Edit suggestion rejected') + } + await decorationManager.clearDecorations(editor) + documentChangeListener.dispose() + cursorChangeListener.dispose() + const suggestionState = isDiscard + ? { + seen: false, + accepted: false, + discarded: true, + } + : { + seen: true, + accepted: false, + discarded: false, + } + const params: LogInlineCompletionSessionResultsParams = { + sessionId: session.sessionId, + completionSessionResult: { + [item.itemId]: suggestionState, + }, + totalSessionDisplayTime: Date.now() - session.requestStartTime, + firstCompletionDisplayLatency: session.firstCompletionDisplayLatency, + isInlineEdit: true, + } + languageClient.sendNotification('aws/logInlineCompletionSessionResults', params) + }, + originalCode, + newCode, + originalCodeHighlightRanges + ) +} + +export function deactivate() { + decorationManager.dispose() +} + +let decorationType: vscode.TextEditorDecorationType | undefined + +export function decorateLinesWithGutterIcon(lineNumbers: number[]) { + const editor = vscode.window.activeTextEditor + if (!editor) { + return + } + + // Dispose previous decoration if it exists + if (decorationType) { + decorationType.dispose() + } + + // Create a new gutter decoration with a small green dot + decorationType = vscode.window.createTextEditorDecorationType({ + gutterIconPath: vscode.Uri.file( + path.join(__dirname, 'media', 'green-dot.svg') // put your svg file in a `media` folder + ), + gutterIconSize: 'contain', + }) + + const decorations: vscode.DecorationOptions[] = lineNumbers.map((line) => ({ + range: new vscode.Range(new vscode.Position(line, 0), new vscode.Position(line, 0)), + })) + + editor.setDecorations(decorationType, decorations) +} diff --git a/packages/amazonq/src/app/inline/EditRendering/imageRenderer.ts b/packages/amazonq/src/app/inline/EditRendering/imageRenderer.ts new file mode 100644 index 00000000000..6c52dc2d6a0 --- /dev/null +++ b/packages/amazonq/src/app/inline/EditRendering/imageRenderer.ts @@ -0,0 +1,59 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { displaySvgDecoration } from './displayImage' +import { SvgGenerationService } from './svgGenerator' +import { getLogger } from 'aws-core-vscode/shared' +import { LanguageClient } from 'vscode-languageclient' +import { InlineCompletionItemWithReferences } from '@aws/language-server-runtimes/protocol' +import { CodeWhispererSession } from '../sessionManager' +import type { AmazonQInlineCompletionItemProvider } from '../completion' + +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 + ) + + // 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') + return + } + + if (svgImage) { + // display the SVG image + await displaySvgDecoration( + editor, + svgImage, + startLine, + newCode, + originalCodeHighlightRange, + session, + languageClient, + item, + inlineCompletionProvider + ) + } else { + getLogger('nextEditPrediction').error('SVG image generation returned an empty result.') + } + } catch (error) { + getLogger('nextEditPrediction').error(`Error generating SVG image: ${error}`) + } +} 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 new file mode 100644 index 00000000000..59752a7b08a --- /dev/null +++ b/packages/amazonq/src/app/inline/EditRendering/svgGenerator.ts @@ -0,0 +1,519 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +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') +export const imageVerticalOffset = 1 +export const emptyDiffSvg = { + svgImage: vscode.Uri.parse(''), + startLine: 0, + newCode: '', + originalCodeHighlightRange: [], +} + +const defaultLineHighlightLength = 4 + +export class SvgGenerationService { + /** + * Generates an SVG image representing a code diff + * @param originalCode The original code + * @param newCode The new code with editsss + * @param theme The editor theme information + * @param offSet The margin to add to the left of the image + */ + public async generateDiffSvg( + filePath: string, + udiff: string + ): Promise<{ + svgImage: vscode.Uri + startLine: number + newCode: string + originalCodeHighlightRange: Range[] + }> { + const textDoc = await vscode.workspace.openTextDocument(filePath) + const originalCode = textDoc.getText().replaceAll('\r\n', '\n') + if (originalCode === '') { + logger.error(`udiff format error`) + throw new ToolkitError('udiff format error') + } + const newCode = await diffUtilities.getPatchedCode(filePath, udiff) + + const { createSVGWindow } = await import('svgdom') + + const svgjs = await import('@svgdotjs/svg.js') + const SVG = svgjs.SVG + const registerWindow = svgjs.registerWindow + + // Get editor theme info + const currentTheme = this.getEditorTheme() + + // Get edit diffs with highlight + const { addedLines, removedLines } = this.getEditedLinesFromCode(originalCode, newCode) + + const modifiedLines = diffUtilities.getModifiedLinesFromCode(addedLines, removedLines) + // TODO remove + // eslint-disable-next-line aws-toolkits/no-json-stringify-in-log + logger.info(`Line mapping: ${JSON.stringify(modifiedLines)}`) + + // Calculate dimensions based on code content + const { offset, editStartLine, isPositionValid } = this.calculatePosition( + originalCode.split('\n'), + newCode.split('\n'), + addedLines, + currentTheme + ) + + // if the position for the EDITS suggestion is not valid (there is no difference between new + // and current code content), return EMPTY_DIFF_SVG and skip the suggestion. + if (!isPositionValid) { + return emptyDiffSvg + } + + 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() + const document = window.document + registerWindow(window, document) + const draw = SVG(document.documentElement) as any + + const { width, height } = this.calculateDimensions(addedLines, currentTheme) + draw.size(width + offset, height) + + // Generate CSS for syntax highlighting HTML content based on theme + const styles = this.generateStyles(currentTheme) + const htmlContent = this.generateHtmlContent(normalizedDiffLines, styles, offset) + + // Create foreignObject to embed HTML + const foreignObject = draw.foreignObject(width + offset, height) + foreignObject.node.innerHTML = htmlContent.trim() + + const svgData = draw.svg() + const svgResult = `data:image/svg+xml;base64,${Buffer.from(svgData).toString('base64')}` + + return { + svgImage: vscode.Uri.parse(svgResult), + startLine: editStartLine, + newCode: newCode, + originalCodeHighlightRange: highlightRanges.removedRanges, + } + } + + private calculateDimensions(newLines: string[], currentTheme: editorThemeInfo): { width: number; height: number } { + // Calculate appropriate width and height based on diff content + const maxLineLength = Math.max(...newLines.map((line) => line.length)) + + const headerFrontSize = Math.ceil(currentTheme.fontSize * 0.66) + + // Estimate width based on character count and font size + const width = Math.max(41 * headerFrontSize * 0.7, maxLineLength * currentTheme.fontSize * 0.7) + + // Calculate height based on diff line count and line height + const totalLines = newLines.length + 1 // +1 for header + const height = totalLines * currentTheme.lingHeight + 25 // +25 for padding + + return { width, height } + } + + private generateStyles(theme: editorThemeInfo): string { + // Generate CSS styles based on editor theme + const fontSize = theme.fontSize + const headerFrontSize = Math.ceil(fontSize * 0.66) + const lineHeight = theme.lingHeight + const foreground = theme.foreground + const bordeColor = 'rgba(212, 212, 212, 0.5)' + const background = theme.background || '#1e1e1e' + const diffRemoved = theme.diffRemoved || 'rgba(255, 0, 0, 0.2)' + const diffAdded = 'rgba(72, 128, 72, 0.52)' + return ` + .code-container { + font-family: ${'monospace'}; + color: ${foreground}; + font-size: ${fontSize}px; + line-height: ${lineHeight}px; + background-color: ${background}; + border: 1px solid ${bordeColor}; + border-radius: 0px; + padding-top: 3px; + padding-bottom: 5px; + padding-left: 10px; + } + .diff-header { + color: ${theme.foreground || '#d4d4d4'}; + margin: 0; + font-size: ${headerFrontSize}px; + padding: 0px; + } + .diff-removed { + background-color: ${diffRemoved}; + white-space: pre-wrap; /* Preserve whitespace */ + text-decoration: line-through; + opacity: 0.7; + } + .diff-changed { + white-space: pre-wrap; /* Preserve whitespace */ + background-color: ${diffAdded}; + } + .diff-unchanged { + white-space: pre-wrap; /* Preserve indentation for unchanged lines */ + } + ` + } + + private generateHtmlContent(diffLines: string[], styles: string, offSet: number): string { + return ` +
+ +
+
Q: Press [Tab] to accept or [Esc] to reject:
+ ${diffLines.map((line) => `
${line}
`).join('')} +
+
+ ` + } + + /** + * Extract added and removed lines by comparing original and new code + * @param originalCode The original code string + * @param newCode The new code string + * @returns Object containing arrays of added and removed lines + */ + private getEditedLinesFromCode( + originalCode: string, + newCode: string + ): { addedLines: string[]; removedLines: string[] } { + const addedLines: string[] = [] + const removedLines: string[] = [] + + const changes = diffLines(originalCode, newCode) + + for (const change of changes) { + if (change.added) { + addedLines.push(...change.value.split('\n').filter((line) => line.length > 0)) + } else if (change.removed) { + removedLines.push(...change.value.split('\n').filter((line) => line.length > 0)) + } + } + + return { addedLines, removedLines } + } + + /** + * Applies highlighting to code lines based on the specified ranges + * @param newLines Array of code lines to highlight + * @param highlightRanges Array of ranges specifying which parts of the lines to highlight + * @returns Array of HTML strings with appropriate spans for highlighting + */ + private getHighlightEdit(newLines: string[], highlightRanges: Range[]): string[] { + const result: string[] = [] + + // Group ranges by line for easier lookup + const rangesByLine = new Map() + for (const range of highlightRanges) { + if (!rangesByLine.has(range.line)) { + rangesByLine.set(range.line, []) + } + rangesByLine.get(range.line)!.push(range) + } + + // Process each line of code + for (let lineIndex = 0; lineIndex < newLines.length; lineIndex++) { + const line = newLines[lineIndex] + // Get ranges for this line + const lineRanges = rangesByLine.get(lineIndex) || [] + + // If no ranges for this line, leave it as-is with HTML escaping + if (lineRanges.length === 0) { + result.push(`${this.escapeHtml(line)}`) + continue + } + + // Sort ranges by start position to ensure correct ordering + lineRanges.sort((a, b) => a.start - b.start) + + // Build the highlighted line + let highlightedLine = '' + let currentPos = 0 + + for (const range of lineRanges) { + // Add text before the current range (with HTML escaping) + if (range.start > currentPos) { + const beforeText = line.substring(currentPos, range.start) + highlightedLine += `${this.escapeHtml(beforeText)}` + } + + // Add the highlighted part (with HTML escaping) + const highlightedText = line.substring(range.start, range.end) + highlightedLine += `${this.escapeHtml(highlightedText)}` + + // Update current position + currentPos = range.end + } + + // Add any remaining text after the last range (with HTML escaping) + if (currentPos < line.length) { + const afterText = line.substring(currentPos) + highlightedLine += `${this.escapeHtml(afterText)}` + } + + result.push(highlightedLine) + } + + return result + } + + private getEditorTheme(): editorThemeInfo { + const editorConfig = vscode.workspace.getConfiguration('editor') + const fontSize = editorConfig.get('fontSize', 12) // Default to 12 if not set + const lineHeightSetting = editorConfig.get('lineHeight', 0) // Default to 0 if not set + + /** + * Calculate effective line height, documented as such: + * Use 0 to automatically compute the line height from the font size. + * Values between 0 and 8 will be used as a multiplier with the font size. + * Values greater than or equal to 8 will be used as effective values. + */ + let effectiveLineHeight: number + if (lineHeightSetting > 0 && lineHeightSetting < 8) { + effectiveLineHeight = lineHeightSetting * fontSize + } else if (lineHeightSetting >= 8) { + effectiveLineHeight = lineHeightSetting + } else { + effectiveLineHeight = Math.round(1.5 * fontSize) + } + + const themeName = vscode.workspace.getConfiguration('workbench').get('colorTheme', 'Default') + const themeColors = this.getThemeColors(themeName) + + return { + fontSize: fontSize, + lingHeight: effectiveLineHeight, + ...themeColors, + } + } + + private getThemeColors(themeName: string): { + foreground: string + background: string + diffAdded: string + diffRemoved: string + } { + // Define default dark theme colors + const darkThemeColors = { + foreground: 'rgba(212, 212, 212, 1)', + background: 'rgba(30, 30, 30, 1)', + diffAdded: 'rgba(231, 245, 231, 0.2)', + diffRemoved: 'rgba(255, 0, 0, 0.2)', + } + + // Define default light theme colors + const lightThemeColors = { + foreground: 'rgba(0, 0, 0, 1)', + background: 'rgba(255, 255, 255, 1)', + diffAdded: 'rgba(198, 239, 206, 0.2)', + diffRemoved: 'rgba(255, 199, 206, 0.5)', + } + + // For dark and light modes + const themeNameLower = themeName.toLowerCase() + + if (themeNameLower.includes('dark')) { + return darkThemeColors + } else if (themeNameLower.includes('light')) { + return lightThemeColors + } + + // Define colors for specific themes, add more if needed. + const themeColorMap: { + [key: string]: { foreground: string; background: string; diffAdded: string; diffRemoved: string } + } = { + Abyss: { + foreground: 'rgba(255, 255, 255, 1)', + background: 'rgba(0, 12, 24, 1)', + diffAdded: 'rgba(0, 255, 0, 0.2)', + diffRemoved: 'rgba(255, 0, 0, 0.3)', + }, + Red: { + foreground: 'rgba(255, 0, 0, 1)', + background: 'rgba(51, 0, 0, 1)', + diffAdded: 'rgba(255, 100, 100, 0.2)', + diffRemoved: 'rgba(255, 0, 0, 0.5)', + }, + } + + // Return colors for the specific theme or default to light theme + return themeColorMap[themeName] || lightThemeColors + } + + private calculatePosition( + originalLines: string[], + newLines: string[], + diffLines: string[], + theme: editorThemeInfo + ): { offset: number; editStartLine: number; isPositionValid: boolean } { + // Determine the starting line of the edit in the original file + let editStartLineInOldFile = 0 + const maxLength = Math.min(originalLines.length, newLines.length) + + for (let i = 0; i <= maxLength; i++) { + // if there is no difference between the original lines and the new lines, skip calculating for the start position. + if (i === maxLength && originalLines[i] === newLines[i] && originalLines.length === newLines.length) { + logger.info( + 'There is no difference between current and new code suggestion. Skip calculating for start position.' + ) + return { + offset: 0, + editStartLine: 0, + isPositionValid: false, + } + } + if (originalLines[i] !== newLines[i] || i === maxLength) { + editStartLineInOldFile = i + break + } + } + const shiftedStartLine = Math.max(0, editStartLineInOldFile - imageVerticalOffset) + + // Determine the range to consider + const startLine = shiftedStartLine + const endLine = Math.min(editStartLineInOldFile + diffLines.length, originalLines.length) + + // Find the longest line within the specified range + let maxLineLength = 0 + for (let i = startLine; i <= endLine; i++) { + const lineLength = originalLines[i]?.length || 0 + if (lineLength > maxLineLength) { + maxLineLength = lineLength + } + } + + // Calculate the offset based on the longest line and the starting line length + const startLineLength = originalLines[startLine]?.length || 0 + const offset = (maxLineLength - startLineLength) * theme.fontSize * 0.7 + 10 // padding + + return { offset, editStartLine: editStartLineInOldFile, isPositionValid: true } + } + + private escapeHtml(text: string): string { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + } + + /** + * Generates character-level highlight ranges for both original and modified code. + * @param originalCode Array of original code lines + * @param afterCode Array of code lines after modification + * @param modifiedLines Map of original lines to modified lines + * @returns Object containing ranges for original and after code character level highlighting + */ + private generateHighlightRanges( + originalCode: string[], + afterCode: string[], + modifiedLines: Map + ): { removedRanges: Range[]; addedRanges: Range[] } { + const originalRanges: Range[] = [] + const afterRanges: Range[] = [] + + // Create reverse mapping for quicker lookups + const reverseMap = new Map() + for (const [original, modified] of modifiedLines.entries()) { + reverseMap.set(modified, original) + } + + // Process original code lines - produces highlight ranges in current editor text + 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) && line.trim().length > 0) { + const modifiedLine = modifiedLines.get(line)! + const changes = diffWordsWithSpace(line, modifiedLine) + + let charPos = 0 + for (const part of changes) { + if (part.removed) { + originalRanges.push({ + line: lineIndex, + start: charPos, + end: charPos + part.value.length, + }) + } + + if (!part.added) { + charPos += part.value.length + } + } + } else { + // Line doesn't exist in modifiedLines values, highlight entire line + originalRanges.push({ + line: lineIndex, + start: 0, + end: line.length ?? defaultLineHighlightLength, + }) + } + } + + // Process after code lines - used for highlight in SVG image + for (let lineIndex = 0; lineIndex < afterCode.length; lineIndex++) { + const line = afterCode[lineIndex] + + if (reverseMap.has(line)) { + const originalLine = reverseMap.get(line)! + const changes = diffWordsWithSpace(originalLine, line) + + let charPos = 0 + for (const part of changes) { + if (part.added) { + afterRanges.push({ + line: lineIndex, + start: charPos, + end: charPos + part.value.length, + }) + } + + if (!part.removed) { + charPos += part.value.length + } + } + } else { + afterRanges.push({ + line: lineIndex, + start: 0, + end: line.length, + }) + } + } + + return { + removedRanges: originalRanges, + addedRanges: afterRanges, + } + } +} + +interface editorThemeInfo { + fontSize: number + lingHeight: number + foreground?: string + background?: string + diffAdded?: string + diffRemoved?: string +} diff --git a/packages/amazonq/src/app/inline/activation.ts b/packages/amazonq/src/app/inline/activation.ts index d786047b2aa..12deb2310fa 100644 --- a/packages/amazonq/src/app/inline/activation.ts +++ b/packages/amazonq/src/app/inline/activation.ts @@ -5,6 +5,7 @@ import vscode from 'vscode' import { + acceptSuggestion, AuthUtil, CodeSuggestionsState, CodeWhispererCodeCoverageTracker, @@ -22,14 +23,16 @@ import { vsCodeState, } from 'aws-core-vscode/codewhisperer' import { Commands, getLogger, globals, sleep } from 'aws-core-vscode/shared' +import { LanguageClient } from 'vscode-languageclient' -export async function activate() { +export async function activate(languageClient: LanguageClient) { const codewhispererSettings = CodeWhispererSettings.instance const client = new DefaultCodeWhispererClient() if (isInlineCompletionEnabled()) { await setSubscriptionsforInlineCompletion() await AuthUtil.instance.setVscodeContextProps() + RecommendationHandler.instance.setLanguageClient(languageClient) } function getAutoTriggerStatus(): boolean { @@ -59,6 +62,7 @@ export async function activate() { * Automated trigger */ globals.context.subscriptions.push( + acceptSuggestion.register(globals.context), vscode.window.onDidChangeActiveTextEditor(async (editor) => { await RecommendationHandler.instance.onEditorChange() }), @@ -94,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 @@ -115,7 +119,7 @@ export async function activate() { vscode.window.activeTextEditor as vscode.TextEditor, client, await getConfigEntry() - ).catch((e) => { + ).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 be390cef34c..c113d3cd2fb 100644 --- a/packages/amazonq/src/app/inline/completion.ts +++ b/packages/amazonq/src/app/inline/completion.ts @@ -2,13 +2,12 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ - +import * as vscode from 'vscode' import { CancellationToken, InlineCompletionContext, InlineCompletionItem, InlineCompletionItemProvider, - InlineCompletionList, Position, TextDocument, commands, @@ -16,6 +15,8 @@ import { Disposable, window, TextEditor, + InlineCompletionTriggerKind, + Range, } from 'vscode' import { LanguageClient } from 'vscode-languageclient' import { @@ -23,14 +24,28 @@ import { LogInlineCompletionSessionResultsParams, } from '@aws/language-server-runtimes/protocol' import { SessionManager } from './sessionManager' -import { RecommendationService } from './recommendationService' +import { GetAllRecommendationsOptions, RecommendationService } from './recommendationService' import { CodeWhispererConstants, ReferenceHoverProvider, - ReferenceInlineProvider, ReferenceLogViewProvider, ImportAdderProvider, + CodeSuggestionsState, + vsCodeState, + noInlineSuggestionsMsg, + getDiagnosticsDifferences, + getDiagnosticsOfCurrentFile, + toIdeDiagnostics, + handleExtraBrackets, } 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 { messageUtils } from 'aws-core-vscode/utils' +import { showEdits } from './EditRendering/imageRenderer' +import { ICursorUpdateRecorder } from './cursorUpdateManager' +import { DocumentEventListener } from './documentEventListener' export class InlineCompletionManager implements Disposable { private disposable: Disposable @@ -38,26 +53,51 @@ export class InlineCompletionManager implements Disposable { private languageClient: LanguageClient private sessionManager: SessionManager private recommendationService: RecommendationService + private lineTracker: LineTracker + + private inlineTutorialAnnotation: InlineTutorialAnnotation private readonly logSessionResultMessageName = 'aws/logInlineCompletionSessionResults' + private documentEventListener: DocumentEventListener - constructor(languageClient: LanguageClient) { + constructor( + languageClient: LanguageClient, + sessionManager: SessionManager, + lineTracker: LineTracker, + inlineTutorialAnnotation: InlineTutorialAnnotation, + cursorUpdateRecorder?: ICursorUpdateRecorder + ) { this.languageClient = languageClient - this.sessionManager = new SessionManager() - this.recommendationService = new RecommendationService(this.sessionManager) + this.sessionManager = sessionManager + this.lineTracker = lineTracker + this.recommendationService = new RecommendationService(this.sessionManager, cursorUpdateRecorder) + this.inlineTutorialAnnotation = inlineTutorialAnnotation + this.documentEventListener = new DocumentEventListener() this.inlineCompletionProvider = new AmazonQInlineCompletionItemProvider( languageClient, this.recommendationService, - this.sessionManager + this.sessionManager, + this.inlineTutorialAnnotation, + this.documentEventListener ) + this.disposable = languages.registerInlineCompletionItemProvider( CodeWhispererConstants.platformLanguageIds, this.inlineCompletionProvider ) + this.lineTracker.ready() + } + + public getInlineCompletionProvider(): AmazonQInlineCompletionItemProvider { + return this.inlineCompletionProvider } public dispose(): void { if (this.disposable) { this.disposable.dispose() + this.lineTracker.dispose() + } + if (this.documentEventListener) { + this.documentEventListener.dispose() } } @@ -67,155 +107,483 @@ export class InlineCompletionManager implements Disposable { item: InlineCompletionItemWithReferences, editor: TextEditor, requestStartTime: number, - startLine: number, + position: vscode.Position, firstCompletionDisplayLatency?: number ) => { - // TODO: also log the seen state for other suggestions in session - const params: LogInlineCompletionSessionResultsParams = { - sessionId: sessionId, - completionSessionResult: { - [item.itemId]: { - seen: true, - accepted: true, - discarded: false, + try { + vsCodeState.isCodeWhispererEditing = true + const startLine = position.line + // TODO: also log the seen state for other suggestions in session + // Calculate timing metrics before diagnostic delay + const totalSessionDisplayTime = Date.now() - requestStartTime + await sleep(500) + const diagnosticDiff = getDiagnosticsDifferences( + this.sessionManager.getActiveSession()?.diagnosticsBeforeAccept, + getDiagnosticsOfCurrentFile() + ) + // try remove the extra } ) ' " if there is a new reported problem + // the extra } will cause syntax error + if (diagnosticDiff.added.length > 0) { + await handleExtraBrackets(editor, editor.selection.active, position) + } + const params: LogInlineCompletionSessionResultsParams = { + sessionId: sessionId, + completionSessionResult: { + [item.itemId]: { + seen: true, + accepted: true, + discarded: false, + }, }, - }, - totalSessionDisplayTime: Date.now() - requestStartTime, - firstCompletionDisplayLatency: firstCompletionDisplayLatency, - } - this.languageClient.sendNotification(this.logSessionResultMessageName, params) - this.disposable.dispose() - this.disposable = languages.registerInlineCompletionItemProvider( - CodeWhispererConstants.platformLanguageIds, - this.inlineCompletionProvider - ) - if (item.references && item.references.length) { - const referenceLog = ReferenceLogViewProvider.getReferenceLog( - item.insertText as string, - item.references, - editor + totalSessionDisplayTime: totalSessionDisplayTime, + firstCompletionDisplayLatency: firstCompletionDisplayLatency, + addedDiagnostics: diagnosticDiff.added.map((it) => toIdeDiagnostics(it)), + removedDiagnostics: diagnosticDiff.removed.map((it) => toIdeDiagnostics(it)), + } + this.languageClient.sendNotification(this.logSessionResultMessageName, params) + this.disposable.dispose() + this.disposable = languages.registerInlineCompletionItemProvider( + CodeWhispererConstants.platformLanguageIds, + this.inlineCompletionProvider ) - ReferenceLogViewProvider.instance.addReferenceLog(referenceLog) - ReferenceHoverProvider.instance.addCodeReferences(item.insertText as string, item.references) - } - if (item.mostRelevantMissingImports?.length) { - await ImportAdderProvider.instance.onAcceptRecommendation(editor, item, startLine) + if (item.references && item.references.length) { + const referenceLog = ReferenceLogViewProvider.getReferenceLog( + item.insertText as string, + item.references, + editor + ) + ReferenceLogViewProvider.instance.addReferenceLog(referenceLog) + ReferenceHoverProvider.instance.addCodeReferences(item.insertText as string, item.references) + } + if (item.mostRelevantMissingImports?.length) { + await ImportAdderProvider.instance.onAcceptRecommendation(editor, item, startLine) + } + this.sessionManager.incrementSuggestionCount() + // clear session manager states once accepted + this.sessionManager.clear() + } finally { + vsCodeState.isCodeWhispererEditing = false } } commands.registerCommand('aws.amazonq.acceptInline', onInlineAcceptance) const onInlineRejection = async () => { - await commands.executeCommand('editor.action.inlineSuggest.hide') - // TODO: also log the seen state for other suggestions in session - this.disposable.dispose() - this.disposable = languages.registerInlineCompletionItemProvider( - CodeWhispererConstants.platformLanguageIds, - this.inlineCompletionProvider - ) - const sessionId = this.sessionManager.getActiveSession()?.sessionId - const itemId = this.sessionManager.getActiveRecommendation()[0]?.itemId - if (!sessionId || !itemId) { - return - } - const params: LogInlineCompletionSessionResultsParams = { - sessionId: sessionId, - completionSessionResult: { - [itemId]: { - seen: true, - accepted: false, - discarded: false, + try { + vsCodeState.isCodeWhispererEditing = true + const session = this.sessionManager.getActiveSession() + if (session === undefined) { + return + } + const requestStartTime = session.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() + this.disposable = languages.registerInlineCompletionItemProvider( + CodeWhispererConstants.platformLanguageIds, + this.inlineCompletionProvider + ) + const sessionId = session.sessionId + const itemId = this.sessionManager.getActiveRecommendation()[0]?.itemId + if (!itemId) { + return + } + const params: LogInlineCompletionSessionResultsParams = { + sessionId: sessionId, + completionSessionResult: { + [itemId]: { + seen: true, + accepted: false, + discarded: false, + }, }, - }, + firstCompletionDisplayLatency: session.firstCompletionDisplayLatency, + totalSessionDisplayTime: totalSessionDisplayTime, + } + this.languageClient.sendNotification(this.logSessionResultMessageName, params) + // clear session manager states once rejected + this.sessionManager.clear() + } finally { + vsCodeState.isCodeWhispererEditing = false } - this.languageClient.sendNotification(this.logSessionResultMessageName, params) } commands.registerCommand('aws.amazonq.rejectCodeSuggestion', onInlineRejection) - - /* - We have to overwrite the prev. and next. commands because the inlineCompletionProvider only contained the current item - To show prev. and next. recommendation we need to re-register a new provider with the previous or next item - */ - - const swapProviderAndShow = async () => { - await commands.executeCommand('editor.action.inlineSuggest.hide') - this.disposable.dispose() - this.disposable = languages.registerInlineCompletionItemProvider( - CodeWhispererConstants.platformLanguageIds, - new AmazonQInlineCompletionItemProvider( - this.languageClient, - this.recommendationService, - this.sessionManager, - false - ) - ) - await commands.executeCommand('editor.action.inlineSuggest.trigger') - } - - const prevCommandHandler = async () => { - this.sessionManager.decrementActiveIndex() - await swapProviderAndShow() - } - commands.registerCommand('editor.action.inlineSuggest.showPrevious', prevCommandHandler) - - const nextCommandHandler = async () => { - this.sessionManager.incrementActiveIndex() - await swapProviderAndShow() - } - commands.registerCommand('editor.action.inlineSuggest.showNext', nextCommandHandler) } } export class AmazonQInlineCompletionItemProvider implements InlineCompletionItemProvider { + private logger = getLogger() + private pendingRequest: Promise | undefined + constructor( private readonly languageClient: LanguageClient, private readonly recommendationService: RecommendationService, private readonly sessionManager: SessionManager, - private readonly isNewSession: boolean = true + private readonly inlineTutorialAnnotation: InlineTutorialAnnotation, + private readonly documentEventListener: DocumentEventListener ) {} + private readonly logSessionResultMessageName = 'aws/logInlineCompletionSessionResults' + + // Ideally use this API handleDidShowCompletionItem + // https://github.com/microsoft/vscode/blob/main/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts#L83 + // we need this because the returned items of provideInlineCompletionItems may not be actually rendered on screen + // if VS Code believes the user is actively typing then it will not show such item + async checkWhetherInlineCompletionWasShown() { + // this line is to force VS Code to re-render the inline completion + // if it decides the inline completion can be shown + await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger') + // yield event loop to let backend state transition finish plus wait for vsc to render + await sleep(10) + // run the command to detect if inline suggestion is really shown or not + await vscode.commands.executeCommand(`aws.amazonq.checkInlineSuggestionVisibility`) + } + + /** + * Check if a completion suggestion is currently active/displayed + */ + public async isCompletionActive(): Promise { + const session = this.sessionManager.getActiveSession() + if (session === undefined || !session.displayed || session.suggestions.some((item) => item.isInlineEdit)) { + return false + } + + // 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 = Date.now() - session.lastVisibleTime < 50 + return isInlineSuggestionVisible + } + + /** + * Batch discard telemetry for completion suggestions when edit suggestion is active + */ + public batchDiscardTelemetryForEditSuggestion(items: any[], session: any): void { + // Emit DISCARD telemetry for completion suggestions that can't be shown due to active edit + const completionSessionResult: { + [key: string]: { seen: boolean; accepted: boolean; discarded: boolean } + } = {} + + for (const item of items) { + if (!item.isInlineEdit && item.itemId) { + completionSessionResult[item.itemId] = { + seen: false, + accepted: false, + discarded: true, + } + } + } + + // Send single telemetry event for all discarded items + if (Object.keys(completionSessionResult).length > 0) { + const params: LogInlineCompletionSessionResultsParams = { + sessionId: session.sessionId, + completionSessionResult, + firstCompletionDisplayLatency: session.firstCompletionDisplayLatency, + totalSessionDisplayTime: Date.now() - session.requestStartTime, + } + this.languageClient.sendNotification(this.logSessionResultMessageName, params) + } + } + + // this method is automatically invoked by VS Code as user types async provideInlineCompletionItems( document: TextDocument, position: Position, context: InlineCompletionContext, - token: CancellationToken - ): Promise { - if (this.isNewSession) { - // make service requests if it's a new session + token: CancellationToken, + getAllRecommendationsOptions?: GetAllRecommendationsOptions + ): Promise { + getLogger().info('_provideInlineCompletionItems called with: %O', { + documentUri: document.uri.toString(), + position, + context, + triggerKind: context.triggerKind === InlineCompletionTriggerKind.Automatic ? 'Automatic' : 'Invoke', + options: JSON.stringify(getAllRecommendationsOptions), + }) + + // 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 [] + } + + // 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 = Date.now() - vsCodeState.lastManualTriggerTime > 50 + if (isAutoTrigger && !CodeSuggestionsState.instance.isSuggestionsEnabled()) { + // return early when suggestions are disabled with auto trigger + return [] + } + + // yield event loop to let the document listen catch updates + await sleep(1) + + let logstr = `GenerateCompletion activity:\\n` + try { + const t0 = Date.now() + vsCodeState.isRecommendationsActive = true + // handling previous session + const prevSession = this.sessionManager.getActiveSession() + const prevSessionId = prevSession?.sessionId + const prevItemId = this.sessionManager.getActiveRecommendation()?.[0]?.itemId + const prevStartPosition = prevSession?.startPosition + const editsTriggerOnAcceptance = prevSession?.triggerOnAcceptance + if (editsTriggerOnAcceptance) { + getAllRecommendationsOptions = { + ...getAllRecommendationsOptions, + editsStreakToken: prevSession?.editsStreakPartialResultToken, + } + } + const editor = window.activeTextEditor + // Skip prefix matching for Edits suggestions that trigger on acceptance. + if (prevSession && prevSessionId && prevItemId && prevStartPosition && !editsTriggerOnAcceptance) { + const prefix = document.getText(new Range(prevStartPosition, position)) + const prevItemMatchingPrefix = [] + for (const item of this.sessionManager.getActiveRecommendation()) { + // if item is an Edit suggestion, insertText is a diff instead of new code contents, skip the logic to check for prefix. + if (item.isInlineEdit) { + continue + } + const text = typeof item.insertText === 'string' ? item.insertText : item.insertText.value + if (text.startsWith(prefix) && position.isAfterOrEqual(prevStartPosition)) { + item.command = { + command: 'aws.amazonq.acceptInline', + title: 'On acceptance', + arguments: [ + prevSessionId, + item, + editor, + prevSession?.requestStartTime, + position, + prevSession?.firstCompletionDisplayLatency, + ], + } + item.range = new Range(prevStartPosition, position) + prevItemMatchingPrefix.push(item as 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 ${Date.now() - t0}` + void this.checkWhetherInlineCompletionWasShown() + return prevItemMatchingPrefix + } + + // if no such suggestions, report the previous suggestion as Reject or Discarded + const params: LogInlineCompletionSessionResultsParams = { + sessionId: prevSessionId, + completionSessionResult: { + [prevItemId]: { + seen: prevSession.displayed, + accepted: false, + discarded: !prevSession.displayed, + }, + }, + firstCompletionDisplayLatency: prevSession.firstCompletionDisplayLatency, + totalSessionDisplayTime: Date.now() - prevSession.requestStartTime, + } + 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 + await this.inlineTutorialAnnotation.triggered(context.triggerKind) + + TelemetryHelper.instance.setInvokeSuggestionStartTime() + TelemetryHelper.instance.setTriggerType(context.triggerKind) + + const t1 = Date.now() + await this.recommendationService.getAllRecommendations( this.languageClient, document, position, - context, - token + { + triggerKind: isAutoTrigger ? 1 : 0, + selectedCompletionInfo: context.selectedCompletionInfo, + }, + token, + isAutoTrigger, + this.documentEventListener, + getAllRecommendationsOptions ) - } - // get active item from session for displaying - const items = this.sessionManager.getActiveRecommendation() - const session = this.sessionManager.getActiveSession() - if (!session || !items.length) { - return [] - } - const editor = window.activeTextEditor - for (const item of items) { - item.command = { - command: 'aws.amazonq.acceptInline', - title: 'On acceptance', - arguments: [ - session.sessionId, - item, - editor, - session.requestStartTime, - position.line, - session.firstCompletionDisplayLatency, - ], + // get active item from session for displaying + const items = this.sessionManager.getActiveRecommendation() + const itemId = this.sessionManager.getActiveRecommendation()?.[0]?.itemId + + // eslint-disable-next-line @typescript-eslint/no-base-to-string + const itemLog = items[0] ? `${items[0].insertText.toString()}` : `no suggestion` + + const t2 = Date.now() + + logstr += `- number of suggestions: ${items.length} +- sessionId: ${this.sessionManager.getActiveSession()?.sessionId} +- first suggestion content (next line): +${itemLog} +- duration between trigger to before sending LSP call: ${t1 - t0}ms +- duration between trigger to after receiving LSP response: ${t2 - t0}ms +- duration between before sending LSP call to after receving LSP response: ${t2 - t1}ms +` + const session = this.sessionManager.getActiveSession() + + // Show message to user when manual invoke fails to produce results. + if (items.length === 0 && context.triggerKind === InlineCompletionTriggerKind.Invoke) { + void messageUtils.showTimedMessage(noInlineSuggestionsMsg, 2000) } - ReferenceInlineProvider.instance.setInlineReference( - position.line, - item.insertText as string, - item.references - ) - ImportAdderProvider.instance.onShowRecommendation(document, position.line, item) + + if (!session || !items.length || !editor) { + logstr += `Failed to produce inline suggestion results. Received ${items.length} items from service` + return [] + } + + const cursorPosition = document.validatePosition(position) + + // Completion will not be rendered if users cursor moves to a position which is before the position when the service is invoked + if (items.length > 0 && !items[0].isInlineEdit) { + if (position.isAfter(editor.selection.active)) { + const params: LogInlineCompletionSessionResultsParams = { + sessionId: session.sessionId, + completionSessionResult: { + [itemId]: { + seen: false, + accepted: false, + discarded: true, + }, + }, + } + this.languageClient.sendNotification(this.logSessionResultMessageName, params) + this.sessionManager.clear() + logstr += `- cursor moved behind trigger position. Discarding completion suggestion...` + return [] + } + } + + // delay the suggestion rendeing if user is actively typing + // see https://github.com/aws/aws-toolkit-vscode/commit/a537602a96f498f372ed61ec9d82cf8577a9d854 + for (let i = 0; i < 30; i++) { + const lastDocumentChange = this.documentEventListener.getLastDocumentChangeEvent(document.uri.fsPath) + if ( + lastDocumentChange && + Date.now() - lastDocumentChange.timestamp < CodeWhispererConstants.inlineSuggestionShowDelay + ) { + await sleep(CodeWhispererConstants.showRecommendationTimerPollPeriod) + } else { + break + } + } + + // the user typed characters from invoking suggestion cursor position to receiving suggestion position + const typeahead = document.getText(new Range(position, editor.selection.active)) + + const itemsMatchingTypeahead = [] + + for (const item of items) { + 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: ${Date.now() - t0}ms` + } + return [] + } + + item.insertText = typeof item.insertText === 'string' ? item.insertText : item.insertText.value + if (item.insertText.startsWith(typeahead)) { + item.command = { + command: 'aws.amazonq.acceptInline', + title: 'On acceptance', + arguments: [ + session.sessionId, + item, + editor, + session.requestStartTime, + cursorPosition, + session.firstCompletionDisplayLatency, + ], + } + item.range = new Range(cursorPosition, cursorPosition) + itemsMatchingTypeahead.push(item) + } + } + + // report discard if none of suggestions match typeahead + if (itemsMatchingTypeahead.length === 0) { + const params: LogInlineCompletionSessionResultsParams = { + sessionId: session.sessionId, + completionSessionResult: { + [itemId]: { + seen: false, + accepted: false, + discarded: true, + }, + }, + } + this.languageClient.sendNotification(this.logSessionResultMessageName, params) + this.sessionManager.clear() + logstr += `- suggestion does not match user typeahead from insertion position. Discarding suggestion...` + return [] + } + + this.sessionManager.updateCodeReferenceAndImports() + // suggestions returned here will be displayed on screen + logstr += `- duration between trigger to completion suggestion is displayed: ${Date.now() - t0}ms` + void this.checkWhetherInlineCompletionWasShown() + return itemsMatchingTypeahead as InlineCompletionItem[] + } catch (e) { + getLogger('amazonqLsp').error('Failed to provide completion items: %O', e) + logstr += `- failed to provide completion items ${(e as Error).message}` + return [] + } finally { + vsCodeState.isRecommendationsActive = false + this.logger.info(logstr) } - return items as InlineCompletionItem[] } } diff --git a/packages/amazonq/src/app/inline/cursorUpdateManager.ts b/packages/amazonq/src/app/inline/cursorUpdateManager.ts new file mode 100644 index 00000000000..3c0ad6f6add --- /dev/null +++ b/packages/amazonq/src/app/inline/cursorUpdateManager.ts @@ -0,0 +1,211 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { LanguageClient } from 'vscode-languageclient' +import { getLogger } from 'aws-core-vscode/shared' +import { globals } from 'aws-core-vscode/shared' +import { AmazonQInlineCompletionItemProvider } from './completion' +import { CodeSuggestionsState } from 'aws-core-vscode/codewhisperer' + +// Configuration section for cursor updates +export const cursorUpdateConfigurationSection = 'aws.q.cursorUpdate' + +/** + * Interface for recording completion requests + */ +export interface ICursorUpdateRecorder { + recordCompletionRequest(): void +} + +/** + * Manages periodic cursor position updates for Next Edit Prediction + */ +export class CursorUpdateManager implements vscode.Disposable, ICursorUpdateRecorder { + private readonly logger = getLogger('amazonqLsp') + private updateIntervalMs = 250 + private updateTimer?: NodeJS.Timeout + private lastPosition?: vscode.Position + private lastDocumentUri?: string + private lastSentPosition?: vscode.Position + private lastSentDocumentUri?: string + private isActive = false + private lastRequestTime = 0 + private autotriggerStateDisposable?: vscode.Disposable + + constructor( + private readonly languageClient: LanguageClient, + private readonly inlineCompletionProvider?: AmazonQInlineCompletionItemProvider + ) { + // Listen for autotrigger state changes to enable/disable the timer + this.autotriggerStateDisposable = CodeSuggestionsState.instance.onDidChangeState((isEnabled: boolean) => { + if (isEnabled && this.isActive) { + // If autotrigger is enabled and we're active, ensure timer is running + this.setupUpdateTimer() + } else { + // If autotrigger is disabled, clear the timer but keep isActive state + this.clearUpdateTimer() + } + }) + } + + /** + * Start tracking cursor positions and sending periodic updates + */ + public async start(): Promise { + if (this.isActive) { + return + } + + // Request configuration from server + try { + const config = await this.languageClient.sendRequest('aws/getConfigurationFromServer', { + section: cursorUpdateConfigurationSection, + }) + + if ( + config && + typeof config === 'object' && + 'intervalMs' in config && + typeof config.intervalMs === 'number' && + config.intervalMs > 0 + ) { + this.updateIntervalMs = config.intervalMs + } + } catch (error) { + this.logger.warn(`Failed to get cursor update configuration from server: ${error}`) + } + + this.isActive = true + if (CodeSuggestionsState.instance.isSuggestionsEnabled()) { + this.setupUpdateTimer() + } + } + + /** + * Stop tracking cursor positions and sending updates + */ + public stop(): void { + this.isActive = false + this.clearUpdateTimer() + } + + /** + * Update the current cursor position + */ + public updatePosition(position: vscode.Position, documentUri: string): void { + // If the document changed, set the last sent position to the current position + // This prevents triggering an immediate recommendation when switching tabs + if (this.lastDocumentUri !== documentUri) { + this.lastSentPosition = position.with() // Create a copy + this.lastSentDocumentUri = documentUri + } + + this.lastPosition = position.with() // Create a copy + this.lastDocumentUri = documentUri + } + + /** + * Record that a regular InlineCompletionWithReferences request was made + * This will prevent cursor updates from being sent for the update interval + */ + public recordCompletionRequest(): void { + this.lastRequestTime = globals.clock.Date.now() + } + + /** + * Set up the timer for periodic cursor position updates + */ + private setupUpdateTimer(): void { + this.clearUpdateTimer() + + this.updateTimer = globals.clock.setInterval(async () => { + await this.sendCursorUpdate() + }, this.updateIntervalMs) + } + + /** + * Clear the update timer + */ + private clearUpdateTimer(): void { + if (this.updateTimer) { + globals.clock.clearInterval(this.updateTimer) + this.updateTimer = undefined + } + } + + /** + * Creates a cancellation token source + * This method exists to make testing easier by allowing it to be stubbed + */ + private createCancellationTokenSource(): vscode.CancellationTokenSource { + return new vscode.CancellationTokenSource() + } + + /** + * Request LSP generate a completion for the current cursor position. + */ + private async sendCursorUpdate(): Promise { + // Don't send an update if a regular request was made recently + const now = globals.clock.Date.now() + if (now - this.lastRequestTime < this.updateIntervalMs) { + return + } + + const editor = vscode.window.activeTextEditor + if (!editor || editor.document.uri.toString() !== this.lastDocumentUri) { + return + } + + // Don't send an update if the position hasn't changed since the last update + if ( + this.lastSentPosition && + this.lastPosition && + this.lastSentDocumentUri === this.lastDocumentUri && + this.lastSentPosition.line === this.lastPosition.line && + this.lastSentPosition.character === this.lastPosition.character + ) { + return + } + + // Only proceed if we have a valid position and provider + if (this.lastPosition && this.inlineCompletionProvider) { + const position = this.lastPosition.with() // Create a copy + + // Call the inline completion provider instead of directly calling getAllRecommendations + try { + await this.inlineCompletionProvider.provideInlineCompletionItems( + editor.document, + position, + { + triggerKind: vscode.InlineCompletionTriggerKind.Automatic, + selectedCompletionInfo: undefined, + }, + this.createCancellationTokenSource().token, + { emitTelemetry: false, showUi: false } + ) + + // Only update the last sent position after successfully sending the request + this.lastSentPosition = position + this.lastSentDocumentUri = this.lastDocumentUri + } catch (error) { + this.logger.error(`Error sending cursor update: ${error}`) + } + } + } + + /** + * Dispose of resources + */ + public dispose(): void { + // Dispose of the autotrigger state change listener + if (this.autotriggerStateDisposable) { + this.autotriggerStateDisposable.dispose() + this.autotriggerStateDisposable = undefined + } + + this.stop() + } +} diff --git a/packages/amazonq/src/app/inline/documentEventListener.ts b/packages/amazonq/src/app/inline/documentEventListener.ts new file mode 100644 index 00000000000..7af22a3015a --- /dev/null +++ b/packages/amazonq/src/app/inline/documentEventListener.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' + +export interface DocumentChangeEvent { + event: vscode.TextDocumentChangeEvent + timestamp: number +} + +export class DocumentEventListener { + private lastDocumentChangeEventMap: Map = new Map() + private documentChangeListener: vscode.Disposable + private _maxDocument = 1000 + + constructor() { + this.documentChangeListener = vscode.workspace.onDidChangeTextDocument((e) => { + if (e.contentChanges.length > 0) { + if (this.lastDocumentChangeEventMap.size > this._maxDocument) { + this.lastDocumentChangeEventMap.clear() + } + 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) { + void vscode.commands.executeCommand('editor.action.inlineSuggest.trigger') + } + } + }) + } + + public isLastEventDeletion(filepath: string): boolean { + const result = this.lastDocumentChangeEventMap.get(filepath) + if (result) { + const event = result.event + const eventTime = result.timestamp + const isDelete = + (event && event.contentChanges.length === 1 && event.contentChanges[0].text === '') || false + const timeDiff = Math.abs(Date.now() - eventTime) + return timeDiff < 500 && isDelete + } + return false + } + + public getLastDocumentChangeEvent(filepath: string): DocumentChangeEvent | undefined { + return this.lastDocumentChangeEventMap.get(filepath) + } + + public dispose(): void { + if (this.documentChangeListener) { + this.documentChangeListener.dispose() + } + } + + private isEnter(e: vscode.TextDocumentChangeEvent): boolean { + if (e.contentChanges.length !== 1) { + return false + } + const str = e.contentChanges[0].text + if (str.length === 0) { + return false + } + return ( + (str.startsWith('\r\n') && str.substring(2).trim() === '') || + (str[0] === '\n' && str.substring(1).trim() === '') + ) + } +} diff --git a/packages/amazonq/src/app/inline/editSuggestionState.ts b/packages/amazonq/src/app/inline/editSuggestionState.ts new file mode 100644 index 00000000000..61e4aebd142 --- /dev/null +++ b/packages/amazonq/src/app/inline/editSuggestionState.ts @@ -0,0 +1,27 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Manages the state of edit suggestions to avoid circular dependencies + */ +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 45dd0099ebd..bc9f8052695 100644 --- a/packages/amazonq/src/app/inline/recommendationService.ts +++ b/packages/amazonq/src/app/inline/recommendationService.ts @@ -2,57 +2,289 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ - import { InlineCompletionListWithReferences, InlineCompletionWithReferencesParams, inlineCompletionWithReferencesRequestType, + TextDocumentContentChangeEvent, + editCompletionRequestType, + LogInlineCompletionSessionResultsParams, } from '@aws/language-server-runtimes/protocol' -import { CancellationToken, InlineCompletionContext, Position, TextDocument } from 'vscode' +import { CancellationToken, InlineCompletionContext, Position, TextDocument, commands } from 'vscode' import { LanguageClient } from 'vscode-languageclient' import { SessionManager } from './sessionManager' +import { + AuthUtil, + CodeWhispererConstants, + CodeWhispererStatusBarManager, + vsCodeState, +} from 'aws-core-vscode/codewhisperer' +import { TelemetryHelper } from './telemetryHelper' +import { ICursorUpdateRecorder } from './cursorUpdateManager' +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 { + emitTelemetry?: boolean + showUi?: boolean + editsStreakToken?: number | string +} export class RecommendationService { - constructor(private readonly sessionManager: SessionManager) {} + private logger = getLogger() + + constructor( + private readonly sessionManager: SessionManager, + private cursorUpdateRecorder?: ICursorUpdateRecorder + ) {} + /** + * Set the recommendation service + */ + public setCursorUpdateRecorder(recorder: ICursorUpdateRecorder): void { + this.cursorUpdateRecorder = recorder + } + + async getRecommendationsWithTimeout( + languageClient: LanguageClient, + request: InlineCompletionWithReferencesParams, + token: CancellationToken + ) { + const resultPromise: Promise = languageClient.sendRequest( + inlineCompletionWithReferencesRequestType.method, + request, + token + ) + return await asyncCallWithTimeout( + resultPromise, + `${inlineCompletionWithReferencesRequestType.method} time out`, + CodeWhispererConstants.promiseTimeoutLimit * 1000 + ) + } async getAllRecommendations( languageClient: LanguageClient, document: TextDocument, position: Position, context: InlineCompletionContext, - token: CancellationToken + token: CancellationToken, + isAutoTrigger: boolean, + documentEventListener: DocumentEventListener, + options: GetAllRecommendationsOptions = { emitTelemetry: true, showUi: true } ) { - const request: InlineCompletionWithReferencesParams = { + const documentChangeEvent = documentEventListener?.getLastDocumentChangeEvent(document.uri.fsPath)?.event + + // Record that a regular request is being made + this.cursorUpdateRecorder?.recordCompletionRequest() + const documentChangeParams = documentChangeEvent + ? { + textDocument: { + uri: document.uri.toString(), + version: document.version, + }, + contentChanges: documentChangeEvent.contentChanges.map((x) => x as TextDocumentContentChangeEvent), + } + : undefined + const openTabs = await getOpenFilesInWindow() + let request: InlineCompletionWithReferencesParams = { textDocument: { uri: document.uri.toString(), }, position, context, + documentChangeParams: documentChangeParams, + openTabFilepaths: openTabs, + } + if (options.editsStreakToken) { + request = { ...request, partialResultToken: options.editsStreakToken } + } + if (document.uri.scheme === 'vscode-notebook-cell') { + request.fileContextOverride = extractFileContextInNotebooks(document, position) } const requestStartTime = Date.now() + const statusBar = CodeWhispererStatusBarManager.instance - // Handle first request - const firstResult: InlineCompletionListWithReferences = await languageClient.sendRequest( - inlineCompletionWithReferencesRequestType as any, - request, - token - ) + // Only track telemetry if enabled + TelemetryHelper.instance.setInvokeSuggestionStartTime() + TelemetryHelper.instance.setPreprocessEndTime() + TelemetryHelper.instance.setSdkApiCallStartTime() - const firstCompletionDisplayLatency = Date.now() - requestStartTime - this.sessionManager.startSession( - firstResult.sessionId, - firstResult.items, - requestStartTime, - firstCompletionDisplayLatency - ) + try { + // Show UI indicators only if UI is enabled + if (options.showUi) { + await statusBar.setLoading() + } - if (firstResult.partialResultToken) { - // If there are more results to fetch, handle them in the background - this.processRemainingRequests(languageClient, request, firstResult, token).catch((error) => { - languageClient.warn(`Error when getting suggestions: ${error}`) + // Handle first request + this.logger.info('Sending inline completion request: %O', { + method: inlineCompletionWithReferencesRequestType.method, + request: { + textDocument: request.textDocument, + position: request.position, + context: request.context, + nextToken: request.partialResultToken, + }, }) - } else { - this.sessionManager.closeSession() + const t0 = Date.now() + + // Best effort estimate of deletion + const isTriggerByDeletion = documentEventListener.isLastEventDeletion(document.uri.fsPath) + + const ps: Promise[] = [] + /** + * IsTriggerByDeletion is to prevent user deletion invoking Completions. + * PartialResultToken is not a hack for now since only Edits suggestion use partialResultToken across different calls of [getAllRecommendations], + * Completions use PartialResultToken with single 1 call of [getAllRecommendations]. + * Edits leverage partialResultToken to achieve EditStreak such that clients can pull all continuous suggestions generated by the model within 1 EOS block. + */ + if (!isTriggerByDeletion && !request.partialResultToken && !EditSuggestionState.isEditSuggestionActive()) { + const completionPromise: Promise = languageClient.sendRequest( + inlineCompletionWithReferencesRequestType.method, + request, + token + ) + ps.push(completionPromise) + } + + /** + * Though Edit request is sent on keystrokes everytime, the language server will execute the request in a debounced manner so that it won't be immediately executed. + */ + const editPromise: Promise = languageClient.sendRequest( + editCompletionRequestType.method, + request, + token + ) + ps.push(editPromise) + + /** + * First come first serve, ideally we should simply return the first response returned. However there are some caviar here because either + * (1) promise might be returned early without going through service + * (2) some users are not enabled with edits suggestion, therefore service will return empty result without passing through the model + * With the scenarios listed above or others, it's possible that 1 promise will ALWAYS win the race and users will NOT get any suggestion back. + * This is the hack to return first "NON-EMPTY" response + */ + let result = await Promise.race(ps) + if (ps.length > 1 && result.items.length === 0) { + for (const p of ps) { + const r = await p + if (r.items.length > 0) { + result = r + } + } + } + + this.logger.info('Received inline completion response from LSP: %O', { + sessionId: result.sessionId, + latency: Date.now() - t0, + itemCount: result.items?.length || 0, + items: result.items?.map((item) => ({ + itemId: item.itemId, + insertText: + (typeof item.insertText === 'string' ? item.insertText : String(item.insertText))?.substring( + 0, + 50 + ) + '...', + })), + }) + + 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, + }, + ]) + ), + } + 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) { + TelemetryHelper.instance.setFirstResponseRequestId(result.items[0].itemId as string) + } + TelemetryHelper.instance.setFirstSuggestionShowTime() + + const firstCompletionDisplayLatency = Date.now() - requestStartTime + this.sessionManager.startSession( + result.sessionId, + result.items, + requestStartTime, + position, + firstCompletionDisplayLatency + ) + + const isInlineEdit = result.items.some((item) => item.isInlineEdit) + + // 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 + logstr += 'Suggestion type is COMPLETIONS. Start pulling more items' + this.processRemainingRequests(languageClient, request, result, token).catch((error) => { + languageClient.warn(`Error when getting suggestions: ${error}`) + }) + } else { + // 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. + logstr += 'Suggestion type is EDITS. Skip pulling more items' + this.sessionManager.updateActiveEditsStreakToken(result.partialResultToken) + } + + this.logger.info(logstr) + } + } catch (error: any) { + 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 + // show re-auth once if connection expired + if (AuthUtil.instance.isConnectionExpired()) { + await AuthUtil.instance.notifyReauthenticate(isAutoTrigger) + } else { + // get a new bearer token, if this failed, the connection will be marked as expired. + // new tokens will be synced per 10 seconds in auth.startTokenRefreshInterval + await AuthUtil.instance.getBearerToken() + } + } + return [] + } finally { + // Remove all UI indicators if UI is enabled + if (options.showUi) { + void statusBar.refreshStatusBar() // effectively "stop loading" + } } } @@ -65,14 +297,22 @@ export class RecommendationService { let nextToken = firstResult.partialResultToken while (nextToken) { const request = { ...initialRequest, partialResultToken: nextToken } - const result: InlineCompletionListWithReferences = await languageClient.sendRequest( - inlineCompletionWithReferencesRequestType as any, - request, - token - ) + + const result = await this.getRecommendationsWithTimeout(languageClient, request, token) + // when pagination is in progress, but user has already accepted or rejected an inline completion + // then stop pagination + if (this.sessionManager.getActiveSession() === undefined || vsCodeState.isCodeWhispererEditing) { + break + } this.sessionManager.updateSessionSuggestions(result.items) nextToken = result.partialResultToken } + this.sessionManager.closeSession() + + // refresh inline completion items to render paginated responses + // All pagination requests completed + TelemetryHelper.instance.setAllPaginationEndTime() + TelemetryHelper.instance.tryRecordClientComponentLatency() } } diff --git a/packages/amazonq/src/app/inline/sessionManager.ts b/packages/amazonq/src/app/inline/sessionManager.ts index 4b70a684001..ef2ee2a84d0 100644 --- a/packages/amazonq/src/app/inline/sessionManager.ts +++ b/packages/amazonq/src/app/inline/sessionManager.ts @@ -2,38 +2,61 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ - +import * as vscode from 'vscode' import { InlineCompletionItemWithReferences } from '@aws/language-server-runtimes-types' +import { + FileDiagnostic, + getDiagnosticsOfCurrentFile, + ImportAdderProvider, + ReferenceInlineProvider, +} from 'aws-core-vscode/codewhisperer' // TODO: add more needed data to the session interface -interface CodeWhispererSession { +export interface CodeWhispererSession { sessionId: string suggestions: InlineCompletionItemWithReferences[] // TODO: might need to convert to enum states isRequestInProgress: boolean requestStartTime: number firstCompletionDisplayLatency?: number + startPosition: vscode.Position + diagnosticsBeforeAccept: FileDiagnostic | undefined + // partialResultToken for the next trigger if user accepts an EDITS suggestion + editsStreakPartialResultToken?: number | string + triggerOnAcceptance?: boolean + // whether any suggestion in this session was displayed on screen + displayed: boolean + // timestamp when the suggestion was last visible + lastVisibleTime: number } export class SessionManager { private activeSession?: CodeWhispererSession - private activeIndex: number = 0 + private _acceptedSuggestionCount: number = 0 + private _refreshedSessions = new Set() + private _currentSuggestionIndex = 0 constructor() {} public startSession( sessionId: string, suggestions: InlineCompletionItemWithReferences[], requestStartTime: number, + startPosition: vscode.Position, firstCompletionDisplayLatency?: number ) { + const diagnosticsBeforeAccept = getDiagnosticsOfCurrentFile() this.activeSession = { sessionId, suggestions, isRequestInProgress: true, requestStartTime, + startPosition, firstCompletionDisplayLatency, + diagnosticsBeforeAccept, + displayed: false, + lastVisibleTime: 0, } - this.activeIndex = 0 + this._currentSuggestionIndex = 0 } public closeSession() { @@ -54,49 +77,107 @@ export class SessionManager { this.activeSession.suggestions = [...this.activeSession.suggestions, ...suggestions] } - public incrementActiveIndex() { - const suggestionCount = this.activeSession?.suggestions?.length - if (!suggestionCount) { + public getActiveRecommendation(): InlineCompletionItemWithReferences[] { + return this.activeSession?.suggestions ?? [] + } + + public get acceptedSuggestionCount(): number { + return this._acceptedSuggestionCount + } + + public incrementSuggestionCount() { + this._acceptedSuggestionCount += 1 + } + + public updateActiveEditsStreakToken(partialResultToken: number | string) { + if (!this.activeSession) { return } - this.activeIndex === suggestionCount - 1 ? suggestionCount - 1 : this.activeIndex++ + this.activeSession.editsStreakPartialResultToken = partialResultToken } - public decrementActiveIndex() { - this.activeIndex === 0 ? 0 : this.activeIndex-- + public clear() { + this.activeSession = undefined + this._currentSuggestionIndex = 0 + this.clearReferenceInlineHintsAndImportHints() } - /* - We have to maintain the active suggestion index ourselves because VS Code doesn't expose which suggestion it's currently showing - In order to keep track of the right suggestion state, and for features such as reference tracker, this hack is still needed - */ - - public getActiveRecommendation(): InlineCompletionItemWithReferences[] { - let suggestionCount = this.activeSession?.suggestions.length - if (!suggestionCount) { - return [] + // re-render the session ghost text to display paginated responses once per completed session + public async maybeRefreshSessionUx() { + if ( + this.activeSession && + !this.activeSession.isRequestInProgress && + !this._refreshedSessions.has(this.activeSession.sessionId) + ) { + await vscode.commands.executeCommand('editor.action.inlineSuggest.hide') + await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger') + if (this._refreshedSessions.size > 1000) { + this._refreshedSessions.clear() + } + this._refreshedSessions.add(this.activeSession.sessionId) } - if (suggestionCount === 1 && this.activeSession?.isRequestInProgress) { - suggestionCount += 1 + } + + public onNextSuggestion() { + if (this.activeSession?.suggestions && this.activeSession?.suggestions.length > 0) { + this._currentSuggestionIndex = (this._currentSuggestionIndex + 1) % this.activeSession.suggestions.length + this.updateCodeReferenceAndImports() } + } - const activeSuggestion = this.activeSession?.suggestions[this.activeIndex] - if (!activeSuggestion) { - return [] + public onPrevSuggestion() { + if (this.activeSession?.suggestions && this.activeSession.suggestions.length > 0) { + this._currentSuggestionIndex = + (this._currentSuggestionIndex - 1 + this.activeSession.suggestions.length) % + this.activeSession.suggestions.length + this.updateCodeReferenceAndImports() } - const items = [activeSuggestion] - // to make the total number of suggestions match the actual number - for (let i = 1; i < suggestionCount; i++) { - items.push({ - ...activeSuggestion, - insertText: `${i}`, - }) + } + + public checkInlineSuggestionVisibility() { + if (this.activeSession) { + this.activeSession.displayed = true + this.activeSession.lastVisibleTime = Date.now() } - return items } - public clear() { - this.activeSession = undefined - this.activeIndex = 0 + private clearReferenceInlineHintsAndImportHints() { + ReferenceInlineProvider.instance.removeInlineReference() + ImportAdderProvider.instance.clear() + } + + // Ideally use this API handleDidShowCompletionItem + // https://github.com/microsoft/vscode/blob/main/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts#L83 + updateCodeReferenceAndImports() { + try { + this.clearReferenceInlineHintsAndImportHints() + if ( + this.activeSession?.suggestions && + this.activeSession.suggestions[this._currentSuggestionIndex] && + this.activeSession.suggestions.length > 0 + ) { + const reference = this.activeSession.suggestions[this._currentSuggestionIndex].references + const insertText = this.activeSession.suggestions[this._currentSuggestionIndex].insertText + if (reference && reference.length > 0) { + const insertTextStr = + typeof insertText === 'string' ? insertText : (insertText.value ?? String(insertText)) + + ReferenceInlineProvider.instance.setInlineReference( + this.activeSession.startPosition.line, + insertTextStr, + reference + ) + } + if (vscode.window.activeTextEditor) { + ImportAdderProvider.instance.onShowRecommendation( + vscode.window.activeTextEditor.document, + this.activeSession.startPosition.line, + this.activeSession.suggestions[this._currentSuggestionIndex] + ) + } + } + } catch { + // do nothing as this is not critical path + } } } diff --git a/packages/amazonq/src/app/inline/stateTracker/lineTracker.ts b/packages/amazonq/src/app/inline/stateTracker/lineTracker.ts new file mode 100644 index 00000000000..58bee329a40 --- /dev/null +++ b/packages/amazonq/src/app/inline/stateTracker/lineTracker.ts @@ -0,0 +1,178 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { editorUtilities, setContext } from 'aws-core-vscode/shared' + +export interface LineSelection { + anchor: number + active: number +} + +export interface LinesChangeEvent { + readonly editor: vscode.TextEditor | undefined + readonly selections: LineSelection[] | undefined + + readonly reason: 'editor' | 'selection' | 'content' +} + +/** + * This class providees a single interface to manage and access users' "line" selections + * Callers could use it by subscribing onDidChangeActiveLines to do UI updates or logic needed to be executed when line selections get changed + */ +export class LineTracker implements vscode.Disposable { + private _onDidChangeActiveLines = new vscode.EventEmitter() + get onDidChangeActiveLines(): vscode.Event { + return this._onDidChangeActiveLines.event + } + + private _editor: vscode.TextEditor | undefined + private _disposable: vscode.Disposable | undefined + + private _selections: LineSelection[] | undefined + get selections(): LineSelection[] | undefined { + return this._selections + } + + private _onReady: vscode.EventEmitter = new vscode.EventEmitter() + get onReady(): vscode.Event { + return this._onReady.event + } + + private _ready: boolean = false + get isReady() { + return this._ready + } + + constructor() { + this._disposable = vscode.Disposable.from( + vscode.window.onDidChangeActiveTextEditor(async (e) => { + await this.onActiveTextEditorChanged(e) + }), + vscode.window.onDidChangeTextEditorSelection(async (e) => { + await this.onTextEditorSelectionChanged(e) + }), + vscode.workspace.onDidChangeTextDocument((e) => { + this.onContentChanged(e) + }) + ) + + queueMicrotask(async () => await this.onActiveTextEditorChanged(vscode.window.activeTextEditor)) + } + + dispose() { + this._disposable?.dispose() + } + + ready() { + if (this._ready) { + throw new Error('Linetracker is already activated') + } + + this._ready = true + queueMicrotask(() => this._onReady.fire()) + } + + // @VisibleForTesting + async onActiveTextEditorChanged(editor: vscode.TextEditor | undefined) { + if (editor === this._editor) { + return + } + + this._editor = editor + this._selections = toLineSelections(editor?.selections) + if (this._selections && this._selections[0]) { + const s = this._selections.map((item) => item.active + 1) + await setContext('codewhisperer.activeLine', s) + } + + this.notifyLinesChanged('editor') + } + + // @VisibleForTesting + async onTextEditorSelectionChanged(e: vscode.TextEditorSelectionChangeEvent) { + // If this isn't for our cached editor and its not a real editor -- kick out + if (this._editor !== e.textEditor && !editorUtilities.isTextEditor(e.textEditor)) { + return + } + + const selections = toLineSelections(e.selections) + if (this._editor === e.textEditor && this.includes(selections)) { + return + } + + this._editor = e.textEditor + this._selections = selections + if (this._selections && this._selections[0]) { + const s = this._selections.map((item) => item.active + 1) + await setContext('codewhisperer.activeLine', s) + } + + this.notifyLinesChanged('selection') + } + + // @VisibleForTesting + onContentChanged(e: vscode.TextDocumentChangeEvent) { + const editor = vscode.window.activeTextEditor + if (e.document === editor?.document && e.contentChanges.length > 0 && editorUtilities.isTextEditor(editor)) { + this._editor = editor + this._selections = toLineSelections(this._editor?.selections) + + this.notifyLinesChanged('content') + } + } + + notifyLinesChanged(reason: 'editor' | 'selection' | 'content') { + const e: LinesChangeEvent = { editor: this._editor, selections: this.selections, reason: reason } + this._onDidChangeActiveLines.fire(e) + } + + includes(selections: LineSelection[]): boolean + includes(line: number, options?: { activeOnly: boolean }): boolean + includes(lineOrSelections: number | LineSelection[], options?: { activeOnly: boolean }): boolean { + if (typeof lineOrSelections !== 'number') { + return isIncluded(lineOrSelections, this._selections) + } + + if (this._selections === undefined || this._selections.length === 0) { + return false + } + + const line = lineOrSelections + const activeOnly = options?.activeOnly ?? true + + for (const selection of this._selections) { + if ( + line === selection.active || + (!activeOnly && + ((selection.anchor >= line && line >= selection.active) || + (selection.active >= line && line >= selection.anchor))) + ) { + return true + } + } + return false + } +} + +function isIncluded(selections: LineSelection[] | undefined, within: LineSelection[] | undefined): boolean { + if (selections === undefined && within === undefined) { + return true + } + if (selections === undefined || within === undefined || selections.length !== within.length) { + return false + } + + return selections.every((s, i) => { + const match = within[i] + return s.active === match.active && s.anchor === match.anchor + }) +} + +function toLineSelections(selections: readonly vscode.Selection[]): LineSelection[] +function toLineSelections(selections: readonly vscode.Selection[] | undefined): LineSelection[] | undefined +function toLineSelections(selections: readonly vscode.Selection[] | undefined) { + return selections?.map((s) => ({ active: s.active.line, anchor: s.anchor.line })) +} diff --git a/packages/amazonq/src/app/inline/telemetryHelper.ts b/packages/amazonq/src/app/inline/telemetryHelper.ts new file mode 100644 index 00000000000..41db4c7469a --- /dev/null +++ b/packages/amazonq/src/app/inline/telemetryHelper.ts @@ -0,0 +1,162 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { AuthUtil, getSelectedCustomization } from 'aws-core-vscode/codewhisperer' +import { CodewhispererLanguage } from 'aws-core-vscode/shared' +import { CodewhispererTriggerType, telemetry } from 'aws-core-vscode/telemetry' +import { InlineCompletionTriggerKind } from 'vscode' + +export class TelemetryHelper { + // Variables needed for client component latency + private _invokeSuggestionStartTime = 0 + private _preprocessEndTime = 0 + private _sdkApiCallStartTime = 0 + private _sdkApiCallEndTime = 0 + private _allPaginationEndTime = 0 + private _firstSuggestionShowTime = 0 + private _firstResponseRequestId = '' + private _sessionId = '' + private _language: CodewhispererLanguage = 'java' + private _triggerType: CodewhispererTriggerType = 'OnDemand' + + constructor() {} + + static #instance: TelemetryHelper + + public static get instance() { + return (this.#instance ??= new this()) + } + + public resetClientComponentLatencyTime() { + this._invokeSuggestionStartTime = 0 + this._preprocessEndTime = 0 + this._sdkApiCallStartTime = 0 + this._sdkApiCallEndTime = 0 + this._firstSuggestionShowTime = 0 + this._allPaginationEndTime = 0 + this._firstResponseRequestId = '' + } + + public setInvokeSuggestionStartTime() { + this.resetClientComponentLatencyTime() + this._invokeSuggestionStartTime = Date.now() + } + + get invokeSuggestionStartTime(): number { + return this._invokeSuggestionStartTime + } + + public setPreprocessEndTime() { + this._preprocessEndTime = Date.now() + } + + get preprocessEndTime(): number { + return this._preprocessEndTime + } + + public setSdkApiCallStartTime() { + if (this._sdkApiCallStartTime === 0) { + this._sdkApiCallStartTime = Date.now() + } + } + + get sdkApiCallStartTime(): number { + return this._sdkApiCallStartTime + } + + public setSdkApiCallEndTime() { + if (this._sdkApiCallEndTime === 0 && this._sdkApiCallStartTime !== 0) { + this._sdkApiCallEndTime = Date.now() + } + } + + get sdkApiCallEndTime(): number { + return this._sdkApiCallEndTime + } + + public setAllPaginationEndTime() { + if (this._allPaginationEndTime === 0 && this._sdkApiCallEndTime !== 0) { + this._allPaginationEndTime = Date.now() + } + } + + get allPaginationEndTime(): number { + return this._allPaginationEndTime + } + + public setFirstSuggestionShowTime() { + if (this._firstSuggestionShowTime === 0 && this._sdkApiCallEndTime !== 0) { + this._firstSuggestionShowTime = Date.now() + } + } + + get firstSuggestionShowTime(): number { + return this._firstSuggestionShowTime + } + + public setFirstResponseRequestId(requestId: string) { + if (this._firstResponseRequestId === '') { + this._firstResponseRequestId = requestId + } + } + + get firstResponseRequestId(): string { + return this._firstResponseRequestId + } + + public setSessionId(sessionId: string) { + if (this._sessionId === '') { + this._sessionId = sessionId + } + } + + get sessionId(): string { + return this._sessionId + } + + public setLanguage(language: CodewhispererLanguage) { + this._language = language + } + + get language(): CodewhispererLanguage { + return this._language + } + + public setTriggerType(triggerType: InlineCompletionTriggerKind) { + if (triggerType === InlineCompletionTriggerKind.Invoke) { + this._triggerType = 'OnDemand' + } else if (triggerType === InlineCompletionTriggerKind.Automatic) { + this._triggerType = 'AutoTrigger' + } + } + + get triggerType(): string { + return this._triggerType + } + + // report client component latency after all pagination call finish + // and at least one suggestion is shown to the user + public tryRecordClientComponentLatency() { + if (this._firstSuggestionShowTime === 0 || this._allPaginationEndTime === 0) { + return + } + telemetry.codewhisperer_clientComponentLatency.emit({ + codewhispererAllCompletionsLatency: this._allPaginationEndTime - this._sdkApiCallStartTime, + codewhispererCompletionType: 'Line', + codewhispererCredentialFetchingLatency: 0, // no longer relevant, because we don't re-build the sdk. Flare already has that set + codewhispererCustomizationArn: getSelectedCustomization().arn, + codewhispererEndToEndLatency: this._firstSuggestionShowTime - this._invokeSuggestionStartTime, + codewhispererFirstCompletionLatency: this._sdkApiCallEndTime - this._sdkApiCallStartTime, + codewhispererLanguage: this._language, + codewhispererPostprocessingLatency: this._firstSuggestionShowTime - this._sdkApiCallEndTime, + codewhispererPreprocessingLatency: this._preprocessEndTime - this._invokeSuggestionStartTime, + codewhispererRequestId: this._firstResponseRequestId, + codewhispererSessionId: this._sessionId, + codewhispererTriggerType: this._triggerType, + credentialStartUrl: AuthUtil.instance.startUrl, + result: 'Succeeded', + }) + } +} diff --git a/packages/amazonq/src/inlineChat/decorations/inlineLineAnnotationController.ts b/packages/amazonq/src/app/inline/tutorials/inlineChatTutorialAnnotation.ts similarity index 72% rename from packages/amazonq/src/inlineChat/decorations/inlineLineAnnotationController.ts rename to packages/amazonq/src/app/inline/tutorials/inlineChatTutorialAnnotation.ts index 9ec5e08122d..1208b4766af 100644 --- a/packages/amazonq/src/inlineChat/decorations/inlineLineAnnotationController.ts +++ b/packages/amazonq/src/app/inline/tutorials/inlineChatTutorialAnnotation.ts @@ -3,14 +3,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Container } from 'aws-core-vscode/codewhisperer' import * as vscode from 'vscode' +import { InlineTutorialAnnotation } from './inlineTutorialAnnotation' +import { globals } from 'aws-core-vscode/shared' -export class InlineLineAnnotationController { +export class InlineChatTutorialAnnotation { private enabled: boolean = true - constructor(context: vscode.ExtensionContext) { - context.subscriptions.push( + constructor(private readonly inlineTutorialAnnotation: InlineTutorialAnnotation) { + globals.context.subscriptions.push( vscode.window.onDidChangeTextEditorSelection(async ({ selections, textEditor }) => { let showShow = false @@ -33,12 +34,12 @@ export class InlineLineAnnotationController { private async setVisible(editor: vscode.TextEditor, visible: boolean) { let needsRefresh: boolean if (visible) { - needsRefresh = await Container.instance.lineAnnotationController.tryShowInlineHint() + needsRefresh = await this.inlineTutorialAnnotation.tryShowInlineHint() } else { - needsRefresh = await Container.instance.lineAnnotationController.tryHideInlineHint() + needsRefresh = await this.inlineTutorialAnnotation.tryHideInlineHint() } if (needsRefresh) { - await Container.instance.lineAnnotationController.refresh(editor, 'codewhisperer') + await this.inlineTutorialAnnotation.refresh(editor, 'codewhisperer') } } diff --git a/packages/amazonq/src/app/inline/tutorials/inlineTutorialAnnotation.ts b/packages/amazonq/src/app/inline/tutorials/inlineTutorialAnnotation.ts new file mode 100644 index 00000000000..ad0807df94c --- /dev/null +++ b/packages/amazonq/src/app/inline/tutorials/inlineTutorialAnnotation.ts @@ -0,0 +1,519 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as os from 'os' +import { AnnotationChangeSource, AuthUtil, inlinehintKey, runtimeLanguageContext } from 'aws-core-vscode/codewhisperer' +import { editorUtilities, getLogger, globals, setContext, vscodeUtilities } from 'aws-core-vscode/shared' +import { LinesChangeEvent, LineSelection, LineTracker } from '../stateTracker/lineTracker' +import { telemetry } from 'aws-core-vscode/telemetry' +import { cancellableDebounce } from 'aws-core-vscode/utils' +import { SessionManager } from '../sessionManager' + +const case3TimeWindow = 30000 // 30 seconds + +const maxSmallIntegerV8 = 2 ** 30 // Max number that can be stored in V8's smis (small integers) + +function fromId(id: string | undefined, sessionManager: SessionManager): AnnotationState | undefined { + switch (id) { + case AutotriggerState.id: + return new AutotriggerState(sessionManager) + case PressTabState.id: + return new AutotriggerState(sessionManager) + case ManualtriggerState.id: + return new ManualtriggerState() + case TryMoreExState.id: + return new TryMoreExState() + case EndState.id: + return new EndState() + case InlineChatState.id: + return new InlineChatState() + default: + return undefined + } +} + +interface AnnotationState { + id: string + suppressWhileRunning: boolean + decorationRenderOptions?: vscode.ThemableDecorationAttachmentRenderOptions + + text: () => string + updateState(changeSource: AnnotationChangeSource, force: boolean): AnnotationState | undefined + isNextState(state: AnnotationState | undefined): boolean +} + +/** + * case 1: How Cwspr triggers + * Trigger Criteria: + * User opens an editor file && + * CW is not providing a suggestion && + * User has not accepted any suggestion + * + * Exit criteria: + * User accepts 1 suggestion + * + */ +export class AutotriggerState implements AnnotationState { + static id = 'codewhisperer_learnmore_case_1' + id = AutotriggerState.id + + suppressWhileRunning = true + text = () => 'Amazon Q Tip 1/3: Start typing to get suggestions ([ESC] to exit)' + static acceptedCount = 0 + + constructor(private readonly sessionManager: SessionManager) {} + + updateState(changeSource: AnnotationChangeSource, force: boolean): AnnotationState | undefined { + if (AutotriggerState.acceptedCount < this.sessionManager.acceptedSuggestionCount) { + return new ManualtriggerState() + } else if (this.sessionManager.getActiveRecommendation().length > 0) { + return new PressTabState(this.sessionManager) + } else { + return this + } + } + + isNextState(state: AnnotationState | undefined): boolean { + return state instanceof ManualtriggerState + } +} + +/** + * case 1-a: Tab to accept + * Trigger Criteria: + * Case 1 && + * Inline suggestion is being shown + * + * Exit criteria: + * User accepts 1 suggestion + */ +export class PressTabState implements AnnotationState { + static id = 'codewhisperer_learnmore_case_1a' + id = PressTabState.id + + suppressWhileRunning = false + + text = () => 'Amazon Q Tip 1/3: Press [TAB] to accept ([ESC] to exit)' + + constructor(private readonly sessionManager: SessionManager) {} + + updateState(changeSource: AnnotationChangeSource, force: boolean): AnnotationState | undefined { + return new AutotriggerState(this.sessionManager).updateState(changeSource, force) + } + + isNextState(state: AnnotationState | undefined): boolean { + return state instanceof ManualtriggerState + } +} + +/** + * case 2: Manual trigger + * Trigger Criteria: + * User exists case 1 && + * User navigates to a new line + * + * Exit criteria: + * User inokes manual trigger shortcut + */ +export class ManualtriggerState implements AnnotationState { + static id = 'codewhisperer_learnmore_case_2' + id = ManualtriggerState.id + + suppressWhileRunning = true + + text = () => { + if (os.platform() === 'win32') { + return 'Amazon Q Tip 2/3: Invoke suggestions with [Alt] + [C] ([ESC] to exit)' + } + + return 'Amazon Q Tip 2/3: Invoke suggestions with [Option] + [C] ([ESC] to exit)' + } + hasManualTrigger: boolean = false + hasValidResponse: boolean = false + + updateState(changeSource: AnnotationChangeSource, force: boolean): AnnotationState | undefined { + if (this.hasManualTrigger && this.hasValidResponse) { + if (changeSource !== 'codewhisperer') { + return new TryMoreExState() + } else { + return undefined + } + } else { + return this + } + } + + isNextState(state: AnnotationState | undefined): boolean { + return state instanceof TryMoreExState + } +} + +/** + * case 3: Learn more + * Trigger Criteria: + * User exists case 2 && + * User navigates to a new line + * + * Exit criteria: + * User accepts or rejects the suggestion + */ +export class TryMoreExState implements AnnotationState { + static id = 'codewhisperer_learnmore_case_3' + id = TryMoreExState.id + + suppressWhileRunning = true + + text = () => 'Amazon Q Tip 3/3: For settings, open the Amazon Q menu from the status bar ([ESC] to exit)' + updateState(changeSource: AnnotationChangeSource, force: boolean): AnnotationState { + if (force) { + return new EndState() + } + return this + } + + isNextState(state: AnnotationState | undefined): boolean { + return state instanceof EndState + } + + static learnmoeCount: number = 0 +} + +export class EndState implements AnnotationState { + static id = 'codewhisperer_learnmore_end' + id = EndState.id + + suppressWhileRunning = true + text = () => '' + updateState(changeSource: AnnotationChangeSource, force: boolean): AnnotationState { + return this + } + isNextState(state: AnnotationState): boolean { + return false + } +} + +export class InlineChatState implements AnnotationState { + static id = 'amazonq_annotation_inline_chat' + id = InlineChatState.id + suppressWhileRunning = false + + text = () => { + if (os.platform() === 'darwin') { + return 'Amazon Q: Edit \u2318I' + } + return 'Amazon Q: Edit (Ctrl+I)' + } + updateState(_changeSource: AnnotationChangeSource, _force: boolean): AnnotationState { + return this + } + isNextState(_state: AnnotationState | undefined): boolean { + return false + } +} + +/** + * There are + * - existing users + * - new users + * -- new users who has not seen tutorial + * -- new users who has seen tutorial + * + * "existing users" should have the context key "CODEWHISPERER_AUTO_TRIGGER_ENABLED" + * "new users who has seen tutorial" should have the context key "inlineKey" and "CODEWHISPERER_AUTO_TRIGGER_ENABLED" + * the remaining grouop of users should belong to "new users who has not seen tutorial" + */ +export class InlineTutorialAnnotation implements vscode.Disposable { + private readonly _disposable: vscode.Disposable + private _editor: vscode.TextEditor | undefined + + private _currentState: AnnotationState + + private readonly cwLineHintDecoration: vscode.TextEditorDecorationType = + vscode.window.createTextEditorDecorationType({ + after: { + margin: '0 0 0 3em', + // "borderRadius" and "padding" are not available on "after" type of decoration, this is a hack to inject these css prop to "after" content. Refer to https://github.com/microsoft/vscode/issues/68845 + textDecoration: ';border-radius:0.25rem;padding:0rem 0.5rem;', + width: 'fit-content', + }, + rangeBehavior: vscode.DecorationRangeBehavior.OpenOpen, + }) + + constructor( + private readonly lineTracker: LineTracker, + private readonly sessionManager: SessionManager + ) { + const cachedState = fromId(globals.globalState.get(inlinehintKey), sessionManager) + const cachedAutotriggerEnabled = globals.globalState.get('CODEWHISPERER_AUTO_TRIGGER_ENABLED') + + // new users (has or has not seen tutorial) + if (cachedAutotriggerEnabled === undefined || cachedState !== undefined) { + this._currentState = cachedState ?? new AutotriggerState(this.sessionManager) + getLogger().debug( + `codewhisperer: new user login, activating inline tutorial. (autotriggerEnabled=${cachedAutotriggerEnabled}; inlineState=${cachedState?.id})` + ) + } else { + this._currentState = new EndState() + getLogger().debug(`codewhisperer: existing user login, disabling inline tutorial.`) + } + + this._disposable = vscode.Disposable.from( + vscodeUtilities.subscribeOnce(this.lineTracker.onReady)(async (_) => { + await this.onReady() + }), + this.lineTracker.onDidChangeActiveLines(async (e) => { + await this.onActiveLinesChanged(e) + }), + AuthUtil.instance.auth.onDidChangeConnectionState(async (e) => { + if (e.state !== 'authenticating') { + await this.refresh(vscode.window.activeTextEditor, 'editor') + } + }), + AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(async () => { + await this.refresh(vscode.window.activeTextEditor, 'editor') + }) + ) + } + + dispose() { + this._disposable.dispose() + } + + private _isReady: boolean = false + + private async onReady(): Promise { + this._isReady = !(this._currentState instanceof EndState) + await this._refresh(vscode.window.activeTextEditor, 'editor') + } + + async triggered(triggerType: vscode.InlineCompletionTriggerKind): Promise { + // TODO: this logic will take ~200ms each trigger, need to root cause and re-enable once it's fixed, or it should only be invoked when the tutorial is actually needed + // await telemetry.withTraceId(async () => { + // if (!this._isReady) { + // return + // } + // if (this._currentState instanceof ManualtriggerState) { + // if ( + // triggerType === vscode.InlineCompletionTriggerKind.Invoke && + // this._currentState.hasManualTrigger === false + // ) { + // this._currentState.hasManualTrigger = true + // } + // if ( + // this.sessionManager.getActiveRecommendation().length > 0 && + // this._currentState.hasValidResponse === false + // ) { + // this._currentState.hasValidResponse = true + // } + // } + // await this.refresh(vscode.window.activeTextEditor, 'codewhisperer') + // }, TelemetryHelper.instance.traceId) + } + + isTutorialDone(): boolean { + return this._currentState.id === new EndState().id + } + + isInlineChatHint(): boolean { + return this._currentState.id === new InlineChatState().id + } + + async dismissTutorial() { + this._currentState = new EndState() + await setContext('aws.codewhisperer.tutorial.workInProgress', false) + await globals.globalState.update(inlinehintKey, this._currentState.id) + } + + /** + * Trys to show the inline hint, if the tutorial is not finished it will not be shown + */ + async tryShowInlineHint(): Promise { + if (this.isTutorialDone()) { + this._isReady = true + this._currentState = new InlineChatState() + return true + } + return false + } + + async tryHideInlineHint(): Promise { + if (this._currentState instanceof InlineChatState) { + this._currentState = new EndState() + return true + } + return false + } + + private async onActiveLinesChanged(e: LinesChangeEvent) { + if (!this._isReady) { + return + } + + this.clear() + + await this.refresh(e.editor, e.reason) + } + + clear() { + this._editor?.setDecorations(this.cwLineHintDecoration, []) + } + + async refresh(editor: vscode.TextEditor | undefined, source: AnnotationChangeSource, force?: boolean) { + if (force) { + this.refreshDebounced.cancel() + await this._refresh(editor, source, true) + } else { + await this.refreshDebounced.promise(editor, source) + } + } + + private readonly refreshDebounced = cancellableDebounce( + async (editor: vscode.TextEditor | undefined, source: AnnotationChangeSource, force?: boolean) => { + await this._refresh(editor, source, force) + }, + 250 + ) + + private async _refresh(editor: vscode.TextEditor | undefined, source: AnnotationChangeSource, force?: boolean) { + if (!this._isReady) { + this.clear() + return + } + + if (this.isTutorialDone()) { + this.clear() + return + } + + if (editor === undefined && this._editor === undefined) { + this.clear() + return + } + + const selections = this.lineTracker.selections + if (editor === undefined || selections === undefined || !editorUtilities.isTextEditor(editor)) { + this.clear() + return + } + + if (this._editor !== editor) { + // Clear any annotations on the previously active editor + this.clear() + this._editor = editor + } + + // Make sure the editor hasn't died since the await above and that we are still on the same line(s) + if (editor.document === undefined || !this.lineTracker.includes(selections)) { + this.clear() + return + } + + if (!AuthUtil.instance.isConnectionValid()) { + this.clear() + return + } + + // Disable Tips when language is not supported by Amazon Q. + if (!runtimeLanguageContext.isLanguageSupported(editor.document)) { + return + } + + await this.updateDecorations(editor, selections, source, force) + } + + private async updateDecorations( + editor: vscode.TextEditor, + lines: LineSelection[], + source: AnnotationChangeSource, + force?: boolean + ) { + const range = editor.document.validateRange( + new vscode.Range(lines[0].active, maxSmallIntegerV8, lines[0].active, maxSmallIntegerV8) + ) + + const decorationOptions = this.getInlineDecoration(editor, lines, source, force) as + | vscode.DecorationOptions + | undefined + + if (decorationOptions === undefined) { + this.clear() + await setContext('aws.codewhisperer.tutorial.workInProgress', false) + return + } else if (this.isTutorialDone()) { + // special case + // Endstate is meaningless and doesnt need to be rendered + this.clear() + await this.dismissTutorial() + return + } else if (decorationOptions.renderOptions?.after?.contentText === new TryMoreExState().text()) { + // special case + // case 3 exit criteria is to fade away in 30s + setTimeout(async () => { + await this.refresh(editor, source, true) + }, case3TimeWindow) + } + + decorationOptions.range = range + + await globals.globalState.update(inlinehintKey, this._currentState.id) + if (!this.isInlineChatHint()) { + await setContext('aws.codewhisperer.tutorial.workInProgress', true) + } + editor.setDecorations(this.cwLineHintDecoration, [decorationOptions]) + } + + getInlineDecoration( + editor: vscode.TextEditor, + lines: LineSelection[], + source: AnnotationChangeSource, + force?: boolean + ): Partial | undefined { + const isCWRunning = this.sessionManager.getActiveSession()?.isRequestInProgress ?? false + + const textOptions: vscode.ThemableDecorationAttachmentRenderOptions = { + contentText: '', + fontWeight: 'normal', + fontStyle: 'normal', + textDecoration: 'none', + color: 'var(--vscode-editor-background)', + backgroundColor: 'var(--vscode-foreground)', + } + + if (isCWRunning && this._currentState.suppressWhileRunning) { + return undefined + } + + const updatedState: AnnotationState | undefined = this._currentState.updateState(source, force ?? false) + + if (updatedState === undefined) { + return undefined + } + + if (this._currentState.isNextState(updatedState)) { + // special case because PressTabState is part of case_1 (1a) which possibly jumps directly from case_1a to case_2 and miss case_1 + if (this._currentState instanceof PressTabState) { + telemetry.ui_click.emit({ elementId: AutotriggerState.id, passive: true }) + } + telemetry.ui_click.emit({ elementId: this._currentState.id, passive: true }) + } + + // update state + this._currentState = updatedState + + // take snapshot of accepted session so that we can compre if there is delta -> users accept 1 suggestion after seeing this state + AutotriggerState.acceptedCount = this.sessionManager.acceptedSuggestionCount + + textOptions.contentText = this._currentState.text() + + return { + renderOptions: { after: textOptions }, + } + } + + public get currentState(): AnnotationState { + return this._currentState + } +} diff --git a/packages/amazonq/src/app/inline/webViewPanel.ts b/packages/amazonq/src/app/inline/webViewPanel.ts new file mode 100644 index 00000000000..2effa94429c --- /dev/null +++ b/packages/amazonq/src/app/inline/webViewPanel.ts @@ -0,0 +1,450 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +/* eslint-disable no-restricted-imports */ +import fs from 'fs' +import { getLogger } from 'aws-core-vscode/shared' + +/** + * Interface for JSON request log data + */ +interface RequestLogEntry { + timestamp: string + request: string + response: string + endpoint: string + error: string + requestId: string + responseCode: number + applicationLogs?: { + rts?: string[] + ceo?: string[] + [key: string]: string[] | undefined + } + latency?: number + latencyBreakdown?: { + rts?: number + ceo?: number + [key: string]: number | undefined + } + miscellaneous?: any +} + +/** + * Manages the webview panel for displaying insert text content and request logs + */ +export class NextEditPredictionPanel implements vscode.Disposable { + public static readonly viewType = 'nextEditPrediction' + + private static instance: NextEditPredictionPanel | undefined + private panel: vscode.WebviewPanel | undefined + private disposables: vscode.Disposable[] = [] + private statusBarItem: vscode.StatusBarItem + private isVisible = false + private fileWatcher: vscode.FileSystemWatcher | undefined + private requestLogs: RequestLogEntry[] = [] + private logFilePath = '/tmp/request_log.jsonl' + private fileReadTimeout: NodeJS.Timeout | undefined + + private constructor() { + // Create status bar item - higher priority (1) to ensure visibility + this.statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 1) + this.statusBarItem.text = '$(eye) NEP' // Add icon for better visibility + this.statusBarItem.tooltip = 'Toggle Next Edit Prediction Panel' + this.statusBarItem.command = 'aws.amazonq.toggleNextEditPredictionPanel' + this.statusBarItem.show() + + // Register command for toggling the panel + this.disposables.push( + vscode.commands.registerCommand('aws.amazonq.toggleNextEditPredictionPanel', () => { + this.toggle() + }) + ) + } + + /** + * Get or create the NextEditPredictionPanel instance + */ + public static getInstance(): NextEditPredictionPanel { + if (!NextEditPredictionPanel.instance) { + NextEditPredictionPanel.instance = new NextEditPredictionPanel() + } + return NextEditPredictionPanel.instance + } + + /** + * Setup file watcher to monitor the request log file + */ + private setupFileWatcher(): void { + if (this.fileWatcher) { + return + } + + try { + // Create the watcher for the specific file + this.fileWatcher = vscode.workspace.createFileSystemWatcher(this.logFilePath) + + // When file is changed, read it after a delay + this.fileWatcher.onDidChange(() => { + this.scheduleFileRead() + }) + + // When file is created, read it after a delay + this.fileWatcher.onDidCreate(() => { + this.scheduleFileRead() + }) + + this.disposables.push(this.fileWatcher) + + // Initial read of the file if it exists + if (fs.existsSync(this.logFilePath)) { + this.scheduleFileRead() + } + + getLogger('nextEditPrediction').info(`File watcher set up for ${this.logFilePath}`) + } catch (error) { + getLogger('nextEditPrediction').error(`Error setting up file watcher: ${error}`) + } + } + + /** + * Schedule file read with a delay to ensure file is fully written + */ + private scheduleFileRead(): void { + // Clear any existing timeout + if (this.fileReadTimeout) { + clearTimeout(this.fileReadTimeout) + } + + // Schedule new read after 1 second delay + this.fileReadTimeout = setTimeout(() => { + this.readRequestLogFile() + }, 1000) + } + + /** + * Read the request log file and update the panel content + */ + private readRequestLogFile(): void { + getLogger('nextEditPrediction').info(`Attempting to read log file: ${this.logFilePath}`) + try { + if (!fs.existsSync(this.logFilePath)) { + getLogger('nextEditPrediction').info(`Log file does not exist: ${this.logFilePath}`) + return + } + + const content = fs.readFileSync(this.logFilePath, 'utf8') + this.requestLogs = [] + + // Process JSONL format (one JSON object per line) + const lines = content.split('\n').filter((line: string) => line.trim() !== '') + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim() + try { + // Try to parse the JSON, handling potential trailing characters + let jsonString = line + + // Find the last valid JSON by looking for the last closing brace/bracket + const lastClosingBrace = line.lastIndexOf('}') + const lastClosingBracket = line.lastIndexOf(']') + const lastValidChar = Math.max(lastClosingBrace, lastClosingBracket) + + if (lastValidChar > 0 && lastValidChar < line.length - 1) { + // If there are characters after the last valid JSON ending, trim them + jsonString = line.substring(0, lastValidChar + 1) + getLogger('nextEditPrediction').info(`Trimmed extra characters from line ${i + 1}`) + } + + // Step 1: Parse the JSON string to get an object + const parsed = JSON.parse(jsonString) + // Step 2: Stringify the object to normalize it + const normalized = JSON.stringify(parsed) + // Step 3: Parse the normalized string back to an object + const logEntry = JSON.parse(normalized) as RequestLogEntry + + // Parse request and response fields if they're JSON stringss + if (typeof logEntry.request === 'string') { + try { + // Apply the same double-parse technique to nested JSON + const requestObj = JSON.parse(logEntry.request) + const requestNormalized = JSON.stringify(requestObj) + logEntry.request = JSON.parse(requestNormalized) + } catch (e) { + // Keep as string if it's not valid JSON + getLogger('nextEditPrediction').info(`Could not parse request as JSON: ${e}`) + } + } + + if (typeof logEntry.response === 'string') { + try { + // Apply the same double-parse technique to nested JSON + const responseObj = JSON.parse(logEntry.response) + const responseNormalized = JSON.stringify(responseObj) + logEntry.response = JSON.parse(responseNormalized) + } catch (e) { + // Keep as string if it's not valid JSON + getLogger('nextEditPrediction').info(`Could not parse response as JSON: ${e}`) + } + } + + this.requestLogs.push(logEntry) + } catch (e) { + getLogger('nextEditPrediction').error(`Error parsing log entry ${i + 1}: ${e}`) + getLogger('nextEditPrediction').error( + `Problematic line: ${line.length > 100 ? line.substring(0, 100) + '...' : line}` + ) + } + } + + if (this.isVisible && this.panel) { + this.updateRequestLogsView() + } + + getLogger('nextEditPrediction').info(`Read ${this.requestLogs.length} log entries`) + } catch (error) { + getLogger('nextEditPrediction').error(`Error reading log file: ${error}`) + } + } + + /** + * Update the panel with request logs data + */ + private updateRequestLogsView(): void { + if (this.panel) { + this.panel.webview.html = this.getWebviewContent() + getLogger('nextEditPrediction').info('Webview panel updated with request logs') + } + } + + /** + * Toggle the panel visibility + */ + public toggle(): void { + if (this.isVisible) { + this.hide() + } else { + this.show() + } + } + + /** + * Show the panel + */ + public show(): void { + if (!this.panel) { + // Create the webview panel + this.panel = vscode.window.createWebviewPanel( + NextEditPredictionPanel.viewType, + 'Next Edit Prediction', + vscode.ViewColumn.Beside, + { + enableScripts: true, + retainContextWhenHidden: true, + } + ) + + // Set initial content + this.panel.webview.html = this.getWebviewContent() + + // Handle panel disposal + this.panel.onDidDispose( + () => { + this.panel = undefined + this.isVisible = false + this.updateStatusBarItem() + }, + undefined, + this.disposables + ) + + // Handle webview messages + this.panel.webview.onDidReceiveMessage( + (message) => { + switch (message.command) { + case 'refresh': + getLogger('nextEditPrediction').info(`Refresh button clicked`) + this.readRequestLogFile() + break + case 'clear': + getLogger('nextEditPrediction').info(`Clear logs button clicked`) + this.clearLogFile() + break + } + }, + undefined, + this.disposables + ) + } else { + this.panel.reveal() + } + + this.isVisible = true + this.updateStatusBarItem() + + // Setup file watcher when panel is shown + this.setupFileWatcher() + + // If we already have logs, update the view + if (this.requestLogs.length > 0) { + this.updateRequestLogsView() + } else { + // Try to read the log file + this.scheduleFileRead() + } + } + + /** + * Hide the panel + */ + private hide(): void { + if (this.panel) { + this.panel.dispose() + this.panel = undefined + this.isVisible = false + this.updateStatusBarItem() + } + } + + /** + * Update the status bar item appearance based on panel state + */ + private updateStatusBarItem(): void { + if (this.isVisible) { + this.statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground') + } else { + this.statusBarItem.backgroundColor = undefined + } + } + + /** + * Update the panel content with new text + */ + public updateContent(text: string): void { + if (this.panel) { + try { + // Store the text for display in a separate section + const customContent = text + + // Update the panel with both the custom content and the request logs + this.panel.webview.html = this.getWebviewContent(customContent) + getLogger('nextEditPrediction').info('Webview panel content updated') + } catch (error) { + getLogger('nextEditPrediction').error(`Error updating webview: ${error}`) + } + } + } + + /** + * Generate HTML content for the webview + */ + private getWebviewContent(customContent?: string): string { + // Path to the debug.html file + const debugHtmlPath = vscode.Uri.file( + vscode.Uri.joinPath( + vscode.Uri.file(__dirname), + '..', + '..', + '..', + 'app', + 'inline', + 'EditRendering', + 'debug.html' + ).fsPath + ) + + // Read the HTML file content + try { + const htmlContent = fs.readFileSync(debugHtmlPath.fsPath, 'utf8') + getLogger('nextEditPrediction').info(`Successfully loaded debug.html from ${debugHtmlPath.fsPath}`) + + // Modify the HTML to add vscode API initialization + return htmlContent.replace( + '', + ` + + ` + ) + } catch (error) { + getLogger('nextEditPrediction').error(`Error loading debug.html: ${error}`) + return ` + + +

Error loading visualization

+

Failed to load debug.html file: ${error}

+ + + ` + } + } + + /** + * Clear the log file and update the panel + */ + private clearLogFile(): void { + try { + getLogger('nextEditPrediction').info(`Clearing log file: ${this.logFilePath}`) + + // Write an empty string to clear the file + fs.writeFileSync(this.logFilePath, '') + + // Clear the in-memory logs + this.requestLogs = [] + + // Update the view + if (this.isVisible && this.panel) { + this.updateRequestLogsView() + } + + getLogger('nextEditPrediction').info(`Log file cleared successfully`) + } catch (error) { + getLogger('nextEditPrediction').error(`Error clearing log file: ${error}`) + } + } + + /** + * Dispose of resources + */ + public dispose(): void { + if (this.panel) { + this.panel.dispose() + } + + if (this.fileWatcher) { + this.fileWatcher.dispose() + } + + if (this.fileReadTimeout) { + clearTimeout(this.fileReadTimeout) + } + + this.statusBarItem.dispose() + + for (const d of this.disposables) { + d.dispose() + } + this.disposables = [] + + NextEditPredictionPanel.instance = undefined + } +} diff --git a/packages/amazonq/src/extension.ts b/packages/amazonq/src/extension.ts index 1a9d3c5facc..9b83695205c 100644 --- a/packages/amazonq/src/extension.ts +++ b/packages/amazonq/src/extension.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Auth, AuthUtils, CredentialsStore, LoginManager, initializeAuth } from 'aws-core-vscode/auth' +import { AuthUtils, CredentialsStore, LoginManager, initializeAuth } from 'aws-core-vscode/auth' import { activate as activateCodeWhisperer, shutdown as shutdownCodeWhisperer } from 'aws-core-vscode/codewhisperer' import { makeEndpointsProvider, registerGenericCommands } from 'aws-core-vscode' import { CommonAuthWebview } from 'aws-core-vscode/login' @@ -44,8 +44,8 @@ import * as vscode from 'vscode' import { registerCommands } from './commands' import { focusAmazonQPanel } from 'aws-core-vscode/codewhispererChat' import { activate as activateAmazonqLsp } from './lsp/activation' -import { activate as activateInlineCompletion } from './app/inline/activation' import { hasGlibcPatch } from './lsp/client' +import { activateAutoDebug } from './lsp/chat/autoDebug/activation' export const amazonQContextPrefix = 'amazonq' @@ -122,20 +122,22 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is } // Configure proxy settings early - ProxyUtil.configureProxyForLanguageServer() + await ProxyUtil.configureProxyForLanguageServer() // This contains every lsp agnostic things (auth, security scan, code scan) await activateCodeWhisperer(extContext as ExtContext) - if ( - (Experiments.instance.get('amazonqLSP', true) || Auth.instance.isInternalAmazonUser()) && - (!isAmazonLinux2() || hasGlibcPatch()) - ) { - // start the Amazon Q LSP for internal users first - // for AL2, start LSP if glibc patch is found + + if (!isAmazonLinux2() || hasGlibcPatch()) { + // Activate Amazon Q LSP for everyone unless they're using AL2 without the glibc patch await activateAmazonqLsp(context) } - if (!Experiments.instance.get('amazonqLSPInline', false)) { - await activateInlineCompletion() + + // Activate AutoDebug feature at extension level + try { + const autoDebugFeature = await activateAutoDebug(context) + context.subscriptions.push(autoDebugFeature) + } catch (error) { + getLogger().error('Failed to activate AutoDebug feature at extension level: %s', error) } // Generic extension commands diff --git a/packages/amazonq/src/extensionNode.ts b/packages/amazonq/src/extensionNode.ts index 8224b9ce310..d42fafea058 100644 --- a/packages/amazonq/src/extensionNode.ts +++ b/packages/amazonq/src/extensionNode.ts @@ -25,7 +25,6 @@ import { DevOptions } from 'aws-core-vscode/dev' import { Auth, AuthUtils, getTelemetryMetadataForConn, isAnySsoConnection } from 'aws-core-vscode/auth' import api from './api' import { activate as activateCWChat } from './app/chat/activation' -import { activate as activateInlineChat } from './inlineChat/activation' import { beta } from 'aws-core-vscode/dev' import { activate as activateNotifications, NotificationsController } from 'aws-core-vscode/notifications' import { AuthState, AuthUtil } from 'aws-core-vscode/codewhisperer' @@ -73,7 +72,6 @@ async function activateAmazonQNode(context: vscode.ExtensionContext) { } activateAgents() await activateTransformationHub(extContext as ExtContext) - activateInlineChat(context) const authProvider = new CommonAuthViewProvider( context, diff --git a/packages/amazonq/src/inlineChat/activation.ts b/packages/amazonq/src/inlineChat/activation.ts index a42dfdb3e02..9f196f31ba3 100644 --- a/packages/amazonq/src/inlineChat/activation.ts +++ b/packages/amazonq/src/inlineChat/activation.ts @@ -5,8 +5,15 @@ import * as vscode from 'vscode' import { InlineChatController } from './controller/inlineChatController' import { registerInlineCommands } from './command/registerInlineCommands' +import { LanguageClient } from 'vscode-languageclient' +import { InlineChatTutorialAnnotation } from '../app/inline/tutorials/inlineChatTutorialAnnotation' -export function activate(context: vscode.ExtensionContext) { - const inlineChatController = new InlineChatController(context) +export function activate( + context: vscode.ExtensionContext, + client: LanguageClient, + encryptionKey: Buffer, + inlineChatTutorialAnnotation: InlineChatTutorialAnnotation +) { + const inlineChatController = new InlineChatController(context, client, encryptionKey, inlineChatTutorialAnnotation) registerInlineCommands(context, inlineChatController) } diff --git a/packages/amazonq/src/inlineChat/controller/inlineChatController.ts b/packages/amazonq/src/inlineChat/controller/inlineChatController.ts index 7ace8d0095e..7151a8f9723 100644 --- a/packages/amazonq/src/inlineChat/controller/inlineChatController.ts +++ b/packages/amazonq/src/inlineChat/controller/inlineChatController.ts @@ -14,6 +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 { codicon, getIcon, @@ -23,8 +24,9 @@ import { Timeout, textDocumentUtil, isSageMaker, + Experiments, } from 'aws-core-vscode/shared' -import { InlineLineAnnotationController } from '../decorations/inlineLineAnnotationController' +import { InlineChatTutorialAnnotation } from '../../app/inline/tutorials/inlineChatTutorialAnnotation' export class InlineChatController { private task: InlineTask | undefined @@ -32,15 +34,24 @@ export class InlineChatController { private readonly inlineChatProvider: InlineChatProvider private readonly codeLenseProvider: CodelensProvider private readonly referenceLogController = new ReferenceLogController() - private readonly inlineLineAnnotationController: InlineLineAnnotationController + private readonly inlineChatTutorialAnnotation: InlineChatTutorialAnnotation + private readonly computeDiffAndRenderOnEditor: (query: string) => Promise private userQuery: string | undefined private listeners: vscode.Disposable[] = [] - constructor(context: vscode.ExtensionContext) { - this.inlineChatProvider = new InlineChatProvider() + constructor( + context: vscode.ExtensionContext, + client: LanguageClient, + encryptionKey: Buffer, + inlineChatTutorialAnnotation: InlineChatTutorialAnnotation + ) { + this.inlineChatProvider = new InlineChatProvider(client, encryptionKey) this.inlineChatProvider.onErrorOccured(() => this.handleError()) this.codeLenseProvider = new CodelensProvider(context) - this.inlineLineAnnotationController = new InlineLineAnnotationController(context) + this.inlineChatTutorialAnnotation = inlineChatTutorialAnnotation + this.computeDiffAndRenderOnEditor = Experiments.instance.get('amazonqLSPInlineChat', false) + ? this.computeDiffAndRenderOnEditorLSP.bind(this) + : this.computeDiffAndRenderOnEditorLocal.bind(this) } public async createTask( @@ -138,7 +149,7 @@ export class InlineChatController { this.codeLenseProvider.updateLenses(task) if (task.state === TaskState.InProgress) { if (vscode.window.activeTextEditor) { - await this.inlineLineAnnotationController.hide(vscode.window.activeTextEditor) + await this.inlineChatTutorialAnnotation.hide(vscode.window.activeTextEditor) } } await this.refreshCodeLenses(task) @@ -164,7 +175,7 @@ export class InlineChatController { this.listeners = [] this.task = undefined - this.inlineLineAnnotationController.enable() + this.inlineChatTutorialAnnotation.enable() await setContext('amazonq.inline.codelensShortcutEnabled', undefined) } @@ -205,8 +216,8 @@ export class InlineChatController { this.userQuery = query await textDocumentUtil.addEofNewline(editor) this.task = await this.createTask(query, editor.document, editor.selection) - await this.inlineLineAnnotationController.disable(editor) - await this.computeDiffAndRenderOnEditor(query, editor.document).catch(async (err) => { + await this.inlineChatTutorialAnnotation.disable(editor) + await this.computeDiffAndRenderOnEditor(query).catch(async (err) => { getLogger().error('computeDiffAndRenderOnEditor error: %s', (err as Error)?.message) if (err instanceof Error) { void vscode.window.showErrorMessage(`Amazon Q: ${err.message}`) @@ -218,7 +229,46 @@ export class InlineChatController { }) } - private async computeDiffAndRenderOnEditor(query: string, document: vscode.TextDocument) { + private async computeDiffAndRenderOnEditorLSP(query: string) { + if (!this.task) { + return + } + + await this.updateTaskAndLenses(this.task, TaskState.InProgress) + getLogger().info(`inline chat query:\n${query}`) + const uuid = randomUUID() + const message: PromptMessage = { + message: query, + messageId: uuid, + command: undefined, + userIntent: undefined, + tabID: uuid, + } + + const response = await this.inlineChatProvider.processPromptMessageLSP(message) + + // TODO: add tests for this case. + if (!response.body) { + getLogger().warn('Empty body in inline chat response') + await this.handleError() + return + } + + // Update inline diff view + const textDiff = computeDiff(response.body, this.task, false) + const decorations = computeDecorations(this.task) + this.task.decorations = decorations + await this.applyDiff(this.task, textDiff ?? []) + this.decorator.applyDecorations(this.task) + + // Update Codelenses + await this.updateTaskAndLenses(this.task, TaskState.WaitingForDecision) + await setContext('amazonq.inline.codelensShortcutEnabled', true) + this.undoListener(this.task) + } + + // TODO: remove this implementation in favor of LSP + private async computeDiffAndRenderOnEditorLocal(query: string) { if (!this.task) { return } diff --git a/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts b/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts index e6534d65532..cfa3798945c 100644 --- a/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts +++ b/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts @@ -8,6 +8,8 @@ import { CodeWhispererStreamingServiceException, GenerateAssistantResponseCommandOutput, } from '@amzn/codewhisperer-streaming' +import { LanguageClient } from 'vscode-languageclient' +import { inlineChatRequestType } from '@aws/language-server-runtimes/protocol' import { AuthUtil, getSelectedCustomization } from 'aws-core-vscode/codewhisperer' import { ChatSessionStorage, @@ -25,6 +27,9 @@ import { codeWhispererClient } from 'aws-core-vscode/codewhisperer' import type { InlineChatEvent } from 'aws-core-vscode/codewhisperer' import { InlineTask } from '../controller/inlineTask' import { extractAuthFollowUp } from 'aws-core-vscode/amazonq' +import { InlineChatParams, InlineChatResult } from '@aws/language-server-runtimes-types' +import { decryptResponse, encryptRequest } from '../../lsp/encryption' +import { getCursorState } from '../../lsp/utils' export class InlineChatProvider { private readonly editorContextExtractor: EditorContextExtractor @@ -34,13 +39,49 @@ export class InlineChatProvider { private errorEmitter = new vscode.EventEmitter() public onErrorOccured = this.errorEmitter.event - public constructor() { + public constructor( + private readonly client: LanguageClient, + private readonly encryptionKey: Buffer + ) { this.editorContextExtractor = new EditorContextExtractor() this.userIntentRecognizer = new UserIntentRecognizer() this.sessionStorage = new ChatSessionStorage() this.triggerEventsStorage = new TriggerEventsStorage() } + private getCurrentEditorParams(prompt: string): InlineChatParams { + const editor = vscode.window.activeTextEditor + if (!editor) { + throw new ToolkitError('No active editor') + } + + const documentUri = editor.document.uri.toString() + const cursorState = getCursorState(editor.selections) + return { + prompt: { + prompt, + }, + cursorState, + textDocument: { + uri: documentUri, + }, + } + } + + public async processPromptMessageLSP(message: PromptMessage): Promise { + // TODO: handle partial responses. + getLogger().info('Making inline chat request with message %O', message) + const params = this.getCurrentEditorParams(message.message ?? '') + + const inlineChatRequest = await encryptRequest(params, this.encryptionKey) + const response = await this.client.sendRequest(inlineChatRequestType.method, inlineChatRequest) + const inlineChatResponse = await decryptResponse(response, this.encryptionKey) + this.client.info(`Logging response for inline chat ${JSON.stringify(inlineChatResponse)}`) + + return inlineChatResponse + } + + // TODO: remove in favor of LSP implementation. public async processPromptMessage(message: PromptMessage) { return this.editorContextExtractor .extractContextForTrigger('ChatMessage') diff --git a/packages/amazonq/src/lsp/auth.ts b/packages/amazonq/src/lsp/auth.ts index 0bfee98f2e2..f23183d25d7 100644 --- a/packages/amazonq/src/lsp/auth.ts +++ b/packages/amazonq/src/lsp/auth.ts @@ -5,6 +5,7 @@ import { bearerCredentialsUpdateRequestType, + iamCredentialsUpdateRequestType, ConnectionMetadata, NotificationType, RequestType, @@ -16,9 +17,9 @@ import * as crypto from 'crypto' import { LanguageClient } from 'vscode-languageclient' import { AuthUtil } from 'aws-core-vscode/codewhisperer' import { Writable } from 'stream' -import { onceChanged } from 'aws-core-vscode/utils' -import { getLogger, oneMinute } from 'aws-core-vscode/shared' -import { isSsoConnection } from 'aws-core-vscode/auth' +import { onceChanged, onceChangedWithComparator } from 'aws-core-vscode/utils' +import { getLogger, oneMinute, isSageMaker } from 'aws-core-vscode/shared' +import { isSsoConnection, isIamConnection, areCredentialsEqual } from 'aws-core-vscode/auth' export const encryptionKey = crypto.randomBytes(32) @@ -78,10 +79,16 @@ export class AmazonQLspAuth { */ async refreshConnection(force: boolean = false) { const activeConnection = this.authUtil.conn - if (this.authUtil.isConnectionValid() && isSsoConnection(activeConnection)) { - // send the token to the language server - const token = await this.authUtil.getBearerToken() - await (force ? this._updateBearerToken(token) : this.updateBearerToken(token)) + if (this.authUtil.isConnectionValid()) { + if (isSsoConnection(activeConnection)) { + // Existing SSO path + const token = await this.authUtil.getBearerToken() + await (force ? this._updateBearerToken(token) : this.updateBearerToken(token)) + } else if (isSageMaker() && isIamConnection(activeConnection)) { + // New SageMaker IAM path + const credentials = await this.authUtil.getCredentials() + await (force ? this._updateIamCredentials(credentials) : this.updateIamCredentials(credentials)) + } } } @@ -92,9 +99,7 @@ export class AmazonQLspAuth { public updateBearerToken = onceChanged(this._updateBearerToken.bind(this)) private async _updateBearerToken(token: string) { - const request = await this.createUpdateCredentialsRequest({ - token, - }) + const request = await this.createUpdateBearerCredentialsRequest(token) // "aws/credentials/token/update" // https://github.com/aws/language-servers/blob/44d81f0b5754747d77bda60b40cc70950413a737/core/aws-lsp-core/src/credentials/credentialsProvider.ts#L27 @@ -103,6 +108,29 @@ export class AmazonQLspAuth { this.client.info(`UpdateBearerToken: ${JSON.stringify(request)}`) } + 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'}, expiration=${credentials.expiration ? 'present' : 'missing'}` + ) + } + + const request = await this.createUpdateIamCredentialsRequest(credentials) + + // "aws/credentials/iam/update" + await this.client.sendRequest(iamCredentialsUpdateRequestType.method, request) + + this.client.info(`UpdateIamCredentials: ${JSON.stringify(request)}`) + getLogger().info(`[SageMaker Debug] IAM credentials update request sent successfully`) + } + public startTokenRefreshInterval(pollingTime: number = oneMinute / 2) { const interval = setInterval(async () => { await this.refreshConnection().catch((e) => this.logRefreshError(e)) @@ -110,8 +138,9 @@ export class AmazonQLspAuth { return interval } - private async createUpdateCredentialsRequest(data: any): Promise { - const payload = new TextEncoder().encode(JSON.stringify({ data })) + private async createUpdateBearerCredentialsRequest(token: string): Promise { + const bearerCredentials = { token } + const payload = new TextEncoder().encode(JSON.stringify({ data: bearerCredentials })) const jwt = await new jose.CompactEncrypt(payload) .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) @@ -127,4 +156,25 @@ export class AmazonQLspAuth { encrypted: true, } } + + private async createUpdateIamCredentialsRequest(credentials: any): Promise { + // Extract IAM credentials structure + const iamCredentials = { + accessKeyId: credentials.accessKeyId, + secretAccessKey: credentials.secretAccessKey, + sessionToken: credentials.sessionToken, + expiration: credentials.expiration, + } + const payload = new TextEncoder().encode(JSON.stringify({ data: iamCredentials })) + + const jwt = await new jose.CompactEncrypt(payload) + .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) + .encrypt(encryptionKey) + + return { + data: jwt, + // Omit metadata for IAM credentials since startUrl is undefined for non-SSO connections + encrypted: true, + } + } } diff --git a/packages/amazonq/src/lsp/chat/activation.ts b/packages/amazonq/src/lsp/chat/activation.ts index e10a7d2d438..1f443bed875 100644 --- a/packages/amazonq/src/lsp/chat/activation.ts +++ b/packages/amazonq/src/lsp/chat/activation.ts @@ -7,17 +7,25 @@ import { window } from 'vscode' import { LanguageClient } from 'vscode-languageclient' import { AmazonQChatViewProvider } from './webviewProvider' import { focusAmazonQPanel, registerCommands } from './commands' -import { registerLanguageServerEventListener, registerMessageListeners } from './messages' +import { + registerActiveEditorChangeListener, + registerLanguageServerEventListener, + registerMessageListeners, +} from './messages' import { Commands, getLogger, globals, undefinedIfEmpty } from 'aws-core-vscode/shared' import { activate as registerLegacyChatListeners } from '../../app/chat/activation' import { DefaultAmazonQAppInitContext } from 'aws-core-vscode/amazonq' import { AuthUtil, getSelectedCustomization } from 'aws-core-vscode/codewhisperer' import { pushConfigUpdate } from '../config' +import { AutoDebugLspClient } from './autoDebug/lsp/autoDebugLspClient' export async function activate(languageClient: LanguageClient, encryptionKey: Buffer, mynahUIPath: string) { const disposables = globals.context.subscriptions - const provider = new AmazonQChatViewProvider(mynahUIPath) + const provider = new AmazonQChatViewProvider(mynahUIPath, languageClient) + + // Set the chat view provider for AutoDebug to use + AutoDebugLspClient.setChatViewProvider(provider) disposables.push( window.registerWebviewViewProvider(AmazonQChatViewProvider.viewType, provider, { @@ -33,6 +41,7 @@ export async function activate(languageClient: LanguageClient, encryptionKey: Bu **/ registerCommands(provider) registerLanguageServerEventListener(languageClient, provider) + registerActiveEditorChangeListener(languageClient) provider.onDidResolveWebview(() => { const disposable = DefaultAmazonQAppInitContext.instance.getAppsToWebViewMessageListener().onMessage((msg) => { diff --git a/packages/amazonq/src/lsp/chat/autoDebug/activation.ts b/packages/amazonq/src/lsp/chat/autoDebug/activation.ts new file mode 100644 index 00000000000..f18c82c237a --- /dev/null +++ b/packages/amazonq/src/lsp/chat/autoDebug/activation.ts @@ -0,0 +1,78 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getLogger } from 'aws-core-vscode/shared' +import { AutoDebugCommands } from './commands' +import { AutoDebugCodeActionsProvider } from './codeActionsProvider' +import { AutoDebugController } from './controller' + +/** + * Auto Debug feature activation for Amazon Q + * This handles the complete lifecycle of the auto debug feature + */ +export class AutoDebugFeature implements vscode.Disposable { + private readonly logger = getLogger() + private readonly disposables: vscode.Disposable[] = [] + + private autoDebugCommands?: AutoDebugCommands + private codeActionsProvider?: AutoDebugCodeActionsProvider + private controller?: AutoDebugController + + constructor(private readonly context: vscode.ExtensionContext) {} + + /** + * Activate the auto debug feature + */ + async activate(): Promise { + try { + // Initialize the controller first + this.controller = new AutoDebugController() + + // Initialize commands and register them with the controller + this.autoDebugCommands = new AutoDebugCommands() + this.autoDebugCommands.registerCommands(this.context, this.controller) + + // Initialize code actions provider + this.codeActionsProvider = new AutoDebugCodeActionsProvider() + this.context.subscriptions.push(this.codeActionsProvider) + + // Add all to disposables + this.disposables.push(this.controller, this.autoDebugCommands, this.codeActionsProvider) + } catch (error) { + this.logger.error('AutoDebugFeature: Failed to activate auto debug feature: %s', error) + throw error + } + } + + /** + * Get the auto debug controller instance + */ + getController(): AutoDebugController | undefined { + return this.controller + } + + /** + * Dispose of all resources + */ + dispose(): void { + vscode.Disposable.from(...this.disposables).dispose() + } +} + +/** + * Factory function to activate auto debug feature with LSP client + * This is the main entry point for activating auto debug + */ +export async function activateAutoDebug( + context: vscode.ExtensionContext, + client?: any, + encryptionKey?: Buffer +): Promise { + const feature = new AutoDebugFeature(context) + await feature.activate() + + return feature +} diff --git a/packages/amazonq/src/lsp/chat/autoDebug/codeActionsProvider.ts b/packages/amazonq/src/lsp/chat/autoDebug/codeActionsProvider.ts new file mode 100644 index 00000000000..aed926eaacf --- /dev/null +++ b/packages/amazonq/src/lsp/chat/autoDebug/codeActionsProvider.ts @@ -0,0 +1,119 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' + +/** + * Provides code actions for Amazon Q Auto Debug features. + * Integrates with VS Code's quick fix system to offer debugging assistance. + */ +export class AutoDebugCodeActionsProvider implements vscode.CodeActionProvider, vscode.Disposable { + private readonly disposables: vscode.Disposable[] = [] + + public static readonly providedCodeActionKinds = [vscode.CodeActionKind.QuickFix, vscode.CodeActionKind.Refactor] + + constructor() { + this.registerProvider() + } + + private registerProvider(): void { + // Register for all file types + const selector: vscode.DocumentSelector = [{ scheme: 'file' }] + + this.disposables.push( + vscode.languages.registerCodeActionsProvider(selector, this, { + providedCodeActionKinds: AutoDebugCodeActionsProvider.providedCodeActionKinds, + }) + ) + } + + /** + * Provides code actions for the given document and range + */ + public provideCodeActions( + document: vscode.TextDocument, + range: vscode.Range | vscode.Selection, + context: vscode.CodeActionContext, + token: vscode.CancellationToken + ): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> { + if (token.isCancellationRequested) { + return [] + } + + const actions: vscode.CodeAction[] = [] + + // Get diagnostics for the current range + const diagnostics = context.diagnostics.filter( + (diagnostic) => diagnostic.range.intersection(range) !== undefined + ) + + if (diagnostics.length > 0) { + // Add "Fix with Amazon Q" action + actions.push(this.createFixWithQAction(document, range, diagnostics)) + + // Add "Fix All with Amazon Q" action + actions.push(this.createFixAllWithQAction(document)) + + // Add "Explain Problem" action + actions.push(this.createExplainProblemAction(document, range, diagnostics)) + } + return actions + } + + private createFixWithQAction( + document: vscode.TextDocument, + range: vscode.Range, + diagnostics: vscode.Diagnostic[] + ): vscode.CodeAction { + const action = new vscode.CodeAction( + `Amazon Q: Fix Problem (${diagnostics.length} issue${diagnostics.length !== 1 ? 's' : ''})`, + vscode.CodeActionKind.QuickFix + ) + + action.command = { + command: 'amazonq.01.fixWithQ', + title: 'Amazon Q: Fix Problem', + arguments: [range, diagnostics], + } + + action.diagnostics = diagnostics + action.isPreferred = true // Make this the preferred quick fix + + return action + } + + private createFixAllWithQAction(document: vscode.TextDocument): vscode.CodeAction { + const action = new vscode.CodeAction('Amazon Q: Fix All Errors', vscode.CodeActionKind.QuickFix) + + action.command = { + command: 'amazonq.02.fixAllWithQ', + title: 'Amazon Q: Fix All Errors', + } + + return action + } + + private createExplainProblemAction( + document: vscode.TextDocument, + range: vscode.Range, + diagnostics: vscode.Diagnostic[] + ): vscode.CodeAction { + const action = new vscode.CodeAction('Amazon Q: Explain Problem', vscode.CodeActionKind.QuickFix) + + action.command = { + command: 'amazonq.03.explainProblem', + title: 'Amazon Q: Explain Problem', + arguments: [range, diagnostics], + } + + action.diagnostics = diagnostics + + return action + } + + public dispose(): void { + vscode.Disposable.from(...this.disposables).dispose() + } +} diff --git a/packages/amazonq/src/lsp/chat/autoDebug/commands.ts b/packages/amazonq/src/lsp/chat/autoDebug/commands.ts new file mode 100644 index 00000000000..ecdbf80d1e0 --- /dev/null +++ b/packages/amazonq/src/lsp/chat/autoDebug/commands.ts @@ -0,0 +1,177 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +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 + * Handles all command registrations and implementations + */ +export class AutoDebugCommands implements vscode.Disposable { + private readonly logger = getLogger() + private readonly disposables: vscode.Disposable[] = [] + private controller!: AutoDebugController + + /** + * Register all auto debug commands + */ + registerCommands(context: vscode.ExtensionContext, controller: AutoDebugController): void { + this.controller = controller + this.disposables.push( + // Fix with Amazon Q command + Commands.register( + { + id: 'amazonq.01.fixWithQ', + name: 'Amazon Q: Fix Problem', + }, + async (range?: vscode.Range, diagnostics?: vscode.Diagnostic[]) => { + await this.fixWithAmazonQ(range, diagnostics) + } + ), + + // Fix All with Amazon Q command + Commands.register( + { + id: 'amazonq.02.fixAllWithQ', + name: 'Amazon Q: Fix All Errors', + }, + async () => { + await this.fixAllWithAmazonQ() + } + ), + + // Explain Problem with Amazon Q command + Commands.register( + { + id: 'amazonq.03.explainProblem', + name: 'Amazon Q: Explain Problem', + }, + async (range?: vscode.Range, diagnostics?: vscode.Diagnostic[]) => { + await this.explainProblem(range, diagnostics) + } + ) + ) + + // Add all disposables to context + context.subscriptions.push(...this.disposables) + } + + /** + * Generic error handling wrapper for command execution + */ + private async executeWithErrorHandling( + action: () => Promise, + errorMessage: string, + logContext: string + ): Promise { + try { + 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') + } + } + + /** + * Check if there's an active editor and log warning if not + */ + private checkActiveEditor(): vscode.TextEditor | undefined { + const editor = vscode.window.activeTextEditor + if (!editor) { + this.logger.warn('AutoDebugCommands: No active editor found') + } + return editor + } + + /** + * 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' + ) + } + + /** + * 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 + } + 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' + ) + } + + /** + * 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() + if (!editor) { + return + } + await this.controller.explainProblems(range, diagnostics) + autoDebugTelemetry.recordCommandSuccess('explainProblem', problemCount) + }, + 'Explain Problem', + 'explainProblem' + ) + } + + /** + * Dispose of all resources + */ + dispose(): void { + vscode.Disposable.from(...this.disposables).dispose() + } +} diff --git a/packages/amazonq/src/lsp/chat/autoDebug/controller.ts b/packages/amazonq/src/lsp/chat/autoDebug/controller.ts new file mode 100644 index 00000000000..66dcc83b21d --- /dev/null +++ b/packages/amazonq/src/lsp/chat/autoDebug/controller.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 { getLogger, randomUUID, messages } from 'aws-core-vscode/shared' +import { AutoDebugLspClient } from './lsp/autoDebugLspClient' +import { mapDiagnosticSeverity } from './shared/diagnosticUtils' +import { ErrorContextFormatter } from './diagnostics/errorContext' +import { Problem } from './diagnostics/problemDetector' +export interface AutoDebugConfig { + readonly enabled: boolean + readonly excludedSources: string[] + readonly severityFilter: ('error' | 'warning' | 'info' | 'hint')[] +} + +/** + * Simplified controller for Amazon Q Auto Debug system. + * Focuses on context menu and quick fix functionality without workspace-wide monitoring. + */ +export class AutoDebugController implements vscode.Disposable { + private readonly logger = getLogger() + private readonly lspClient: AutoDebugLspClient + private readonly errorFormatter: ErrorContextFormatter + private readonly disposables: vscode.Disposable[] = [] + + private config: AutoDebugConfig + + constructor(config?: Partial) { + this.config = { + enabled: true, + excludedSources: [], // No default exclusions - let users configure as needed + severityFilter: ['error'], // Only auto-fix errors, not warnings + ...config, + } + + this.lspClient = new AutoDebugLspClient() + this.errorFormatter = new ErrorContextFormatter() + } + + /** + * Extract common logic for getting problems from diagnostics + */ + private async getProblemsFromDiagnostics( + range?: vscode.Range, + diagnostics?: vscode.Diagnostic[] + ): Promise<{ editor: vscode.TextEditor; problems: Problem[] } | undefined> { + const editor = vscode.window.activeTextEditor + if (!editor) { + throw new Error('No active editor found') + } + + // Use provided diagnostics or get diagnostics for the range + let targetDiagnostics = diagnostics + if (!targetDiagnostics && range) { + const allDiagnostics = vscode.languages.getDiagnostics(editor.document.uri) + targetDiagnostics = allDiagnostics.filter((d) => d.range.intersection(range) !== undefined) + } + + if (!targetDiagnostics || targetDiagnostics.length === 0) { + return undefined + } + + // Convert diagnostics to problems + const problems = targetDiagnostics.map((diagnostic) => ({ + uri: editor.document.uri, + diagnostic, + severity: mapDiagnosticSeverity(diagnostic.severity), + source: diagnostic.source || 'unknown', + isNew: false, + })) + + return { editor, problems } + } + + /** + * Filter diagnostics to only errors and apply source filtering + */ + private filterErrorDiagnostics(diagnostics: vscode.Diagnostic[]): vscode.Diagnostic[] { + return diagnostics.filter((d) => { + if (d.severity !== vscode.DiagnosticSeverity.Error) { + return false + } + // Apply source filtering + if (this.config.excludedSources.length > 0 && d.source) { + return !this.config.excludedSources.includes(d.source) + } + return true + }) + } + + /** + * Fix specific problems in the code + */ + async fixSpecificProblems(range?: vscode.Range, diagnostics?: vscode.Diagnostic[]): Promise { + try { + const result = await this.getProblemsFromDiagnostics(range, diagnostics) + if (!result) { + return + } + const fixMessage = this.createFixMessage(result.editor.document.uri.fsPath, result.problems) + await this.sendMessageToChat(fixMessage) + } catch (error) { + this.logger.error('AutoDebugController: Error fixing specific problems: %s', error) + throw error + } + } + + /** + * Fix with Amazon Q - sends up to 15 error messages one time when user clicks the button + */ + public async fixAllProblemsInFile(maxProblems: number = 15): Promise { + try { + const editor = vscode.window.activeTextEditor + if (!editor) { + void messages.showMessage('warn', 'No active editor found') + 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 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 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 + } + } + + /** + * Explain problems using Amazon Q + */ + async explainProblems(range?: vscode.Range, diagnostics?: vscode.Diagnostic[]): Promise { + try { + const result = await this.getProblemsFromDiagnostics(range, diagnostics) + if (!result) { + return + } + const explainMessage = this.createExplainMessage(result.editor.document.uri.fsPath, result.problems) + await this.sendMessageToChat(explainMessage) + } catch (error) { + this.logger.error('AutoDebugController: Error explaining problems: %s', error) + throw error + } + } + + private createFixMessage(filePath: string, problems: Problem[]): string { + const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '' + const formattedProblems = this.errorFormatter.formatProblemsString(problems, workspaceRoot) + + return `Please help me fix the following errors in ${filePath}:${formattedProblems}` + } + + private createExplainMessage(filePath: string, problems: Problem[]): string { + const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '' + const formattedProblems = this.errorFormatter.formatProblemsString(problems, workspaceRoot) + + return `Please explain the following problems in ${filePath}. DO NOT edit files. ONLY provide explanation:${formattedProblems}` + } + + /** + * Sends message directly to language server bypassing webview connectors + * This ensures messages go through the proper LSP chat system + */ + private async sendMessageToChat(message: string): Promise { + const triggerID = randomUUID() + try { + const success = await this.lspClient.sendChatMessage({ + message: message, + triggerType: 'autoDebug', + eventId: triggerID, + }) + + if (success) { + this.logger.debug('AutoDebugController: Chat message sent successfully through LSP client') + } else { + this.logger.error('AutoDebugController: Failed to send chat message through LSP client') + throw new Error('Failed to send message through LSP client') + } + } catch (error) { + this.logger.error( + 'AutoDebugController: Error sending message through LSP client with triggerID %s: %s', + triggerID, + error + ) + } + } + + public dispose(): void { + vscode.Disposable.from(...this.disposables).dispose() + } +} diff --git a/packages/amazonq/src/lsp/chat/autoDebug/diagnostics/diagnosticsMonitor.ts b/packages/amazonq/src/lsp/chat/autoDebug/diagnostics/diagnosticsMonitor.ts new file mode 100644 index 00000000000..8f4000bf217 --- /dev/null +++ b/packages/amazonq/src/lsp/chat/autoDebug/diagnostics/diagnosticsMonitor.ts @@ -0,0 +1,22 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' + +export interface DiagnosticCollection { + readonly diagnostics: [vscode.Uri, vscode.Diagnostic[]][] + readonly timestamp: number +} + +export interface DiagnosticSnapshot { + readonly diagnostics: DiagnosticCollection + readonly captureTime: number + readonly id: string +} + +export interface FileDiagnostics { + readonly uri: vscode.Uri + readonly diagnostics: vscode.Diagnostic[] +} diff --git a/packages/amazonq/src/lsp/chat/autoDebug/diagnostics/errorContext.ts b/packages/amazonq/src/lsp/chat/autoDebug/diagnostics/errorContext.ts new file mode 100644 index 00000000000..dee7bc0565a --- /dev/null +++ b/packages/amazonq/src/lsp/chat/autoDebug/diagnostics/errorContext.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 * as path from 'path' +import { Problem } from './problemDetector' + +export interface ErrorContext { + readonly source: string + readonly severity: 'error' | 'warning' | 'info' | 'hint' + readonly location: { + readonly file: string + readonly line: number + readonly column: number + readonly range?: vscode.Range + } + readonly message: string + readonly code?: string | number + readonly relatedInformation?: vscode.DiagnosticRelatedInformation[] + readonly suggestedFixes?: vscode.CodeAction[] + readonly surroundingCode?: string +} + +export interface FormattedErrorReport { + readonly summary: string + readonly details: string + readonly contextualCode: string + readonly suggestions: string +} + +/** + * Formats diagnostic errors into contextual information for AI debugging assistance. + */ +export class ErrorContextFormatter { + /** + * Creates a problems string with Markdown formatting for better readability + */ + public formatProblemsString(problems: Problem[], cwd: string): string { + let result = '' + const fileGroups = this.groupProblemsByFile(problems) + + for (const [filePath, fileProblems] of fileGroups.entries()) { + if (fileProblems.length > 0) { + result += `\n\n**${path.relative(cwd, filePath)}**\n\n` + + // Group problems into a code block for better formatting + result += '```\n' + for (const problem of fileProblems) { + const line = problem.diagnostic.range.start.line + 1 + const source = problem.source ? `${problem.source}` : 'Unknown' + result += `[${source}] Line ${line}: ${problem.diagnostic.message}\n` + } + result += '```' + } + } + + return result.trim() + } + + private groupProblemsByFile(problems: Problem[]): Map { + const groups = new Map() + + for (const problem of problems) { + const filePath = problem.uri.fsPath + if (!groups.has(filePath)) { + groups.set(filePath, []) + } + groups.get(filePath)!.push(problem) + } + + return groups + } +} diff --git a/packages/amazonq/src/lsp/chat/autoDebug/diagnostics/problemDetector.ts b/packages/amazonq/src/lsp/chat/autoDebug/diagnostics/problemDetector.ts new file mode 100644 index 00000000000..44d00b55ca1 --- /dev/null +++ b/packages/amazonq/src/lsp/chat/autoDebug/diagnostics/problemDetector.ts @@ -0,0 +1,21 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' + +export interface Problem { + readonly uri: vscode.Uri + readonly diagnostic: vscode.Diagnostic + readonly severity: 'error' | 'warning' | 'info' | 'hint' + readonly source: string + readonly isNew: boolean +} + +export interface CategorizedProblems { + readonly errors: Problem[] + readonly warnings: Problem[] + readonly info: Problem[] + readonly hints: Problem[] +} diff --git a/packages/amazonq/src/lsp/chat/autoDebug/index.ts b/packages/amazonq/src/lsp/chat/autoDebug/index.ts new file mode 100644 index 00000000000..4819835066b --- /dev/null +++ b/packages/amazonq/src/lsp/chat/autoDebug/index.ts @@ -0,0 +1,18 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Auto Debug feature for Amazon Q + * + * This module provides auto debug functionality including: + * - Command registration for fixing problems with Amazon Q + * - Code actions provider for quick fixes + * - Integration with VSCode's diagnostic system + */ + +export { AutoDebugFeature, activateAutoDebug } from './activation' +export { AutoDebugCommands } from './commands' +export { AutoDebugCodeActionsProvider } from './codeActionsProvider' +export { AutoDebugController } from './controller' diff --git a/packages/amazonq/src/lsp/chat/autoDebug/lsp/autoDebugLspClient.ts b/packages/amazonq/src/lsp/chat/autoDebug/lsp/autoDebugLspClient.ts new file mode 100644 index 00000000000..2d2d0ca3664 --- /dev/null +++ b/packages/amazonq/src/lsp/chat/autoDebug/lsp/autoDebugLspClient.ts @@ -0,0 +1,75 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import { getLogger, placeholder } from 'aws-core-vscode/shared' +import { focusAmazonQPanel } from 'aws-core-vscode/codewhispererChat' + +export class AutoDebugLspClient { + private readonly logger = getLogger() + private static chatViewProvider: any // AmazonQChatViewProvider instance + + /** + * Sets the chat view provider instance + */ + public static setChatViewProvider(provider: any): void { + AutoDebugLspClient.chatViewProvider = provider + } + + public async sendChatMessage(params: { message: string; triggerType: string; eventId: string }): Promise { + try { + // Ensure the chat view provider and webview are available + await this.ensureWebviewReady() + + // Get the webview provider from the static reference + const amazonQChatViewProvider = AutoDebugLspClient.chatViewProvider + + if (!amazonQChatViewProvider?.webview) { + this.logger.error( + 'AutoDebugLspClient: Amazon Q Chat View Provider webview not available after initialization' + ) + return false + } + + // Focus Amazon Q panel first using the imported function + await focusAmazonQPanel.execute(placeholder, 'autoDebug') + + // Wait for panel to focus + await new Promise((resolve) => setTimeout(resolve, 200)) + await amazonQChatViewProvider.webview.postMessage({ + command: 'sendToPrompt', + params: { + selection: '', + triggerType: 'autoDebug', + prompt: { + prompt: params.message, // what gets sent to the user + escapedPrompt: params.message, // what gets sent to the backend + }, + autoSubmit: true, // Automatically submit the message + }, + }) + return true + } catch (error) { + this.logger.error('AutoDebugLspClient: Error sending message via webview: %s', error) + return false + } + } + + /** + * Ensures that the chat view provider and its webview are ready for use + */ + private async ensureWebviewReady(): Promise { + if (!AutoDebugLspClient.chatViewProvider) { + await focusAmazonQPanel.execute(placeholder, 'autoDebug') + // wait 1 second for focusAmazonQPanel to finish + await new Promise((resolve) => setTimeout(resolve, 500)) + } + + // Now ensure the webview is created + if (!AutoDebugLspClient.chatViewProvider.webview) { + await focusAmazonQPanel.execute(placeholder, 'autoDebug') + // wait 1 second for webview to be created + await new Promise((resolve) => setTimeout(resolve, 500)) + } + } +} diff --git a/packages/amazonq/src/lsp/chat/autoDebug/shared/diagnosticUtils.ts b/packages/amazonq/src/lsp/chat/autoDebug/shared/diagnosticUtils.ts new file mode 100644 index 00000000000..bf466168347 --- /dev/null +++ b/packages/amazonq/src/lsp/chat/autoDebug/shared/diagnosticUtils.ts @@ -0,0 +1,35 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { toIdeDiagnostics } from 'aws-core-vscode/codewhisperer' + +/** + * Maps VSCode DiagnosticSeverity to string representation + * Reuses the existing toIdeDiagnostics logic but returns lowercase format expected by Problem interface + */ +export function mapDiagnosticSeverity(severity: vscode.DiagnosticSeverity): 'error' | 'warning' | 'info' | 'hint' { + // Create a minimal diagnostic to use with toIdeDiagnostics + const tempDiagnostic: vscode.Diagnostic = { + range: new vscode.Range(0, 0, 0, 0), + message: '', + severity: severity, + } + + const ideDiagnostic = toIdeDiagnostics(tempDiagnostic) + // Convert uppercase severity to lowercase format expected by Problem interface + switch (ideDiagnostic.severity) { + case 'ERROR': + return 'error' + case 'WARNING': + return 'warning' + case 'INFORMATION': + return 'info' + case 'HINT': + return 'hint' + default: + return '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 115118a4ad2..6e4f928f5f1 100644 --- a/packages/amazonq/src/lsp/chat/commands.ts +++ b/packages/amazonq/src/lsp/chat/commands.ts @@ -6,9 +6,11 @@ import { Commands, globals } from 'aws-core-vscode/shared' import { window } from 'vscode' import { AmazonQChatViewProvider } from './webviewProvider' -import { CodeScanIssue } from 'aws-core-vscode/codewhisperer' -import { EditorContextExtractor } from 'aws-core-vscode/codewhispererChat' -import { DefaultAmazonQAppInitContext } from 'aws-core-vscode/amazonq' +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 { telemetry, AmazonqCodeReviewTool } from 'aws-core-vscode/telemetry' /** * TODO: Re-enable these once we can figure out which path they're going to live in @@ -20,52 +22,28 @@ export function registerCommands(provider: AmazonQChatViewProvider) { registerGenericCommand('aws.amazonq.refactorCode', 'Refactor', provider), registerGenericCommand('aws.amazonq.fixCode', 'Fix', provider), registerGenericCommand('aws.amazonq.optimizeCode', 'Optimize', provider), - Commands.register('aws.amazonq.generateUnitTests', async () => { - DefaultAmazonQAppInitContext.instance.getAppsToWebViewMessagePublisher().publish({ - sender: 'testChat', - command: 'test', - type: 'chatMessage', - }) - }), - Commands.register('aws.amazonq.explainIssue', async (issue: CodeScanIssue) => { - void focusAmazonQPanel().then(async () => { - const editorContextExtractor = new EditorContextExtractor() - const extractedContext = await editorContextExtractor.extractContextForTrigger('ContextMenu') - const selectedCode = - extractedContext?.activeFileContext?.fileText - ?.split('\n') - .slice(issue.startLine, issue.endLine) - .join('\n') ?? '' - - // The message that gets sent to the UI - const uiMessage = [ - 'Explain the ', - issue.title, - ' issue in the following code:', - '\n```\n', - selectedCode, - '\n```', - ].join('') - - // The message that gets sent to the backend - const contextMessage = `Explain the issue "${issue.title}" (${JSON.stringify( - issue - )}) and generate code demonstrating the fix` + registerGenericCommand('aws.amazonq.generateUnitTests', 'Generate Tests', provider), - void provider.webview?.postMessage({ - command: 'sendToPrompt', - params: { - selection: '', - triggerType: 'contextMenu', - prompt: { - prompt: uiMessage, // what gets sent to the user - escapedPrompt: contextMessage, // what gets sent to the backend - }, - autoSubmit: true, - }, - }) - }) - }), + Commands.register('aws.amazonq.explainIssue', (issue: CodeScanIssue, filePath: string) => + handleIssueCommand( + 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. You must start with the information stored in the recommendation.text field if it is present.', + provider, + 'explainIssue' + ) + ), + Commands.register('aws.amazonq.generateFix', (issue: CodeScanIssue, filePath: string) => + handleIssueCommand( + issue, + filePath, + 'Fix', + 'Generate a fix for the following code issue. You must not explain the issue, just generate and explain the fix. The user should have the option to accept or reject the fix before any code is changed.', + provider, + 'applyFix' + ) + ), Commands.register('aws.amazonq.sendToPrompt', (data) => { const triggerType = getCommandTriggerType(data) const selection = getSelectedText() @@ -84,10 +62,76 @@ export function registerCommands(provider: AmazonQChatViewProvider) { params: {}, }) }) - }) + }), + registerShellCommandShortCut('aws.amazonq.runCmdExecution', 'run-shell-command', provider), + registerShellCommandShortCut('aws.amazonq.rejectCmdExecution', 'reject-shell-command', provider), + registerShellCommandShortCut('aws.amazonq.stopCmdExecution', 'stop-shell-command', provider) ) } +async function handleIssueCommand( + issue: CodeScanIssue, + filePath: string, + action: string, + contextPrompt: string, + provider: AmazonQChatViewProvider, + metricName: string +) { + await focusAmazonQPanel() + + if (issue && filePath) { + await openFileWithSelection(issue, filePath) + } + + const lineRange = createLineRangeText(issue) + const visibleMessageInChat = `_${action} **${issue.title}** issue in **${path.basename(filePath)}** at \`${lineRange}\`_` + const contextMessage = `${contextPrompt} Code issue - ${JSON.stringify(issue)}` + + void provider.webview?.postMessage({ + command: 'sendToPrompt', + params: { + selection: '', + triggerType: 'contextMenu', + prompt: { + prompt: visibleMessageInChat, + escapedPrompt: contextMessage, + }, + autoSubmit: true, + }, + }) + + telemetry.amazonq_codeReviewTool.emit({ + findingId: issue.findingId, + detectorId: issue.detectorId, + ruleId: issue.ruleId, + credentialStartUrl: AuthUtil.instance.startUrl, + autoDetected: issue.autoDetected, + result: 'Succeeded', + reason: metricName, + } as AmazonqCodeReviewTool) +} + +async function openFileWithSelection(issue: CodeScanIssue, filePath: string) { + try { + const range = new vscode.Range(issue.startLine, 0, issue.endLine, 0) + const doc = await vscode.workspace.openTextDocument(filePath) + await vscode.window.showTextDocument(doc, { + selection: range, + viewColumn: vscode.ViewColumn.One, + preview: true, + }) + } catch (e) { + getLogger().error('openFileWithSelection: Failed to open file %s with selection: %O', filePath, e) + void vscode.window.showInformationMessage('Failed to display file with issue.') + } +} + +function createLineRangeText(issue: CodeScanIssue): string { + return issue.startLine === issue.endLine - 1 + ? `[${issue.startLine + 1}]` + : `[${issue.startLine + 1}, ${issue.endLine}]` +} + function getSelectedText(): string { const editor = window.activeTextEditor if (editor) { @@ -129,3 +173,14 @@ export async function focusAmazonQPanel() { await Commands.tryExecute('aws.amazonq.AmazonQChatView.focus') await Commands.tryExecute('aws.amazonq.AmazonCommonAuth.focus') } + +function registerShellCommandShortCut(commandName: string, buttonId: string, provider: AmazonQChatViewProvider) { + return Commands.register(commandName, async () => { + void focusAmazonQPanel().then(() => { + void provider.webview?.postMessage({ + command: 'aws/chat/executeShellCommandShortCut', + params: { id: buttonId }, + }) + }) + }) +} diff --git a/packages/amazonq/src/lsp/chat/messages.ts b/packages/amazonq/src/lsp/chat/messages.ts index bbac828e3df..e607643d561 100644 --- a/packages/amazonq/src/lsp/chat/messages.ts +++ b/packages/amazonq/src/lsp/chat/messages.ts @@ -16,6 +16,7 @@ import { ChatPromptOptionAcknowledgedMessage, STOP_CHAT_RESPONSE, StopChatResponseMessage, + OPEN_FILE_DIALOG, } from '@aws/chat-client-ui-types' import { ChatResult, @@ -48,6 +49,7 @@ import { LINK_CLICK_NOTIFICATION_METHOD, LinkClickParams, INFO_LINK_CLICK_NOTIFICATION_METHOD, + READY_NOTIFICATION_METHOD, buttonClickRequestType, ButtonClickResult, CancellationTokenSource, @@ -55,24 +57,63 @@ import { ChatUpdateParams, chatOptionsUpdateType, ChatOptionsUpdateParams, + listRulesRequestType, + ruleClickRequestType, + pinnedContextNotificationType, + activeEditorChangedNotificationType, + listAvailableModelsRequestType, + ShowOpenDialogRequestType, + ShowOpenDialogParams, + openFileDialogRequestType, + OpenFileDialogResult, } from '@aws/language-server-runtimes/protocol' import { v4 as uuidv4 } from 'uuid' import * as vscode from 'vscode' +import * as path from 'path' import { Disposable, LanguageClient, Position, TextDocumentIdentifier } from 'vscode-languageclient' -import * as jose from 'jose' import { AmazonQChatViewProvider } from './webviewProvider' -import { AuthUtil, ReferenceLogViewProvider } from 'aws-core-vscode/codewhisperer' -import { amazonQDiffScheme, AmazonQPromptSettings, messages, openUrl } from 'aws-core-vscode/shared' import { - DefaultAmazonQAppInitContext, - messageDispatcher, - EditorContentController, - ViewDiffMessage, - referenceLogText, -} from 'aws-core-vscode/amazonq' -import { telemetry, TelemetryBase } from 'aws-core-vscode/telemetry' + AggregatedCodeScanIssue, + AuthUtil, + CodeAnalysisScope, + CodeWhispererSettings, + initSecurityScanRender, + ReferenceLogViewProvider, + SecurityIssueTreeViewProvider, + CodeWhispererConstants, +} from 'aws-core-vscode/codewhisperer' +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' +import { getCursorState } from '../utils' 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) { + let debounceTimer: NodeJS.Timeout | undefined + vscode.window.onDidChangeActiveTextEditor((editor) => { + if (debounceTimer) { + clearTimeout(debounceTimer) + } + debounceTimer = setTimeout(() => { + let textDocument = undefined + let cursorState = undefined + if (editor) { + textDocument = { + uri: editor.document.uri.toString(), + } + cursorState = getCursorState(editor.selections) + } + languageClient.sendNotification(activeEditorChangedNotificationType.method, { + textDocument, + cursorState, + }) + }, 100) + }) +} export function registerLanguageServerEventListener(languageClient: LanguageClient, provider: AmazonQChatViewProvider) { languageClient.info( @@ -87,45 +128,30 @@ export function registerLanguageServerEventListener(languageClient: LanguageClie chatOptions.quickActions.quickActionsCommandGroups[0].groupName = 'Quick Actions' } - provider.onDidResolveWebview(() => { - void provider.webview?.postMessage({ - command: CHAT_OPTIONS, - params: chatOptions, - }) - }) - // This passes through metric data from LSP events to Toolkit telemetry with all fields from the LSP server languageClient.onTelemetry((e) => { const telemetryName: string = e.name - - if (telemetryName in telemetry) { - languageClient.info(`[Telemetry] Emitting ${telemetryName} telemetry: ${JSON.stringify(e.data)}`) - telemetry[telemetryName as keyof TelemetryBase].emit(e.data) + languageClient.info(`[VSCode Telemetry] Emitting ${telemetryName} telemetry: ${JSON.stringify(e.data)}`) + try { + // Flare is now the source of truth for metrics instead of depending on each IDE client and toolkit-common + const metric = (telemetry as any).getMetric(telemetryName) + metric?.emit(e.data) + } catch (error) { + languageClient.warn(`[VSCode Telemetry] Failed to emit ${telemetryName}: ${error}`) } }) } -function getCursorState(selection: readonly vscode.Selection[]) { - return selection.map((s) => ({ - range: { - start: { - line: s.start.line, - character: s.start.character, - }, - end: { - line: s.end.line, - character: s.end.character, - }, - }, - })) -} - export function registerMessageListeners( languageClient: LanguageClient, provider: AmazonQChatViewProvider, encryptionKey: Buffer ) { const chatStreamTokens = new Map() // tab id -> token + + // Keep track of pending chat options to send when webview UI is ready + const pendingChatOptions = languageClient.initializeResult?.awsServerCapabilities?.chatOptions + provider.webview?.onDidReceiveMessage(async (message) => { languageClient.info(`[VSCode Client] Received ${JSON.stringify(message)} from chat`) @@ -139,7 +165,33 @@ export function registerMessageListeners( } const webview = provider.webview + switch (message.command) { + // Handle "aws/chat/ready" event + case READY_NOTIFICATION_METHOD: + languageClient.info(`[VSCode Client] "aws/chat/ready" event is received, sending chat options`) + if (webview && pendingChatOptions) { + try { + await webview.postMessage({ + command: CHAT_OPTIONS, + params: pendingChatOptions, + }) + + // Display a more readable representation of quick actions + const quickActionCommands = + pendingChatOptions?.quickActions?.quickActionsCommandGroups?.[0]?.commands || [] + const quickActionsDisplay = quickActionCommands.map((cmd: any) => cmd.command).join(', ') + languageClient.info( + `[VSCode Client] Chat options flags: mcpServers=${pendingChatOptions?.mcpServers}, history=${pendingChatOptions?.history}, export=${pendingChatOptions?.export}, quickActions=[${quickActionsDisplay}]` + ) + 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}` + ) + } + } + break case COPY_TO_CLIPBOARD: languageClient.info('[VSCode Client] Copy to clipboard event received') try { @@ -225,21 +277,12 @@ export function registerMessageListeners( const cancellationToken = new CancellationTokenSource() chatStreamTokens.set(chatParams.tabId, cancellationToken) - const chatDisposable = languageClient.onProgress( - chatRequestType, - partialResultToken, - (partialResult) => { - // Store the latest partial result - if (typeof partialResult === 'string' && encryptionKey) { - void decodeRequest(partialResult, encryptionKey).then( - (decoded) => (lastPartialResult = decoded) - ) - } else { - lastPartialResult = partialResult as ChatResult + const chatDisposable = languageClient.onProgress(chatRequestType, partialResultToken, (partialResult) => + handlePartialResult(partialResult, encryptionKey, provider, chatParams.tabId).then( + (result) => { + lastPartialResult = result } - - void handlePartialResult(partialResult, encryptionKey, provider, chatParams.tabId) - } + ) ) const editor = @@ -251,6 +294,31 @@ export function registerMessageListeners( } const chatRequest = await encryptRequest(chatParams, encryptionKey) + + // Add detailed logging for SageMaker debugging + if (process.env.USE_IAM_AUTH === 'true') { + languageClient.info(`[SageMaker Debug] Making chat request with IAM auth`) + languageClient.info(`[SageMaker Debug] Chat request method: ${chatRequestType.method}`) + languageClient.info( + `[SageMaker Debug] Original chat params: ${JSON.stringify( + { + tabId: chatParams.tabId, + prompt: chatParams.prompt, + // Don't log full textDocument content, just metadata + textDocument: chatParams.textDocument + ? { uri: chatParams.textDocument.uri } + : undefined, + context: chatParams.context ? `${chatParams.context.length} context items` : undefined, + }, + undefined, + 2 + )}` + ) + languageClient.info( + `[SageMaker Debug] Environment context: USE_IAM_AUTH=${process.env.USE_IAM_AUTH}, AWS_REGION=${process.env.AWS_REGION}` + ) + } + try { const chatResult = await languageClient.sendRequest( chatRequestType.method, @@ -260,12 +328,33 @@ export function registerMessageListeners( }, cancellationToken.token ) + + // Add response content logging for SageMaker debugging + if (process.env.USE_IAM_AUTH === 'true') { + languageClient.info(`[SageMaker Debug] Chat response received - type: ${typeof chatResult}`) + if (typeof chatResult === 'string') { + languageClient.info( + `[SageMaker Debug] Chat response (string): ${chatResult.substring(0, 200)}...` + ) + } else if (chatResult && typeof chatResult === 'object') { + languageClient.info( + `[SageMaker Debug] Chat response (object keys): ${Object.keys(chatResult)}` + ) + if ('message' in chatResult) { + languageClient.info( + `[SageMaker Debug] Chat response message: ${JSON.stringify(chatResult.message).substring(0, 200)}...` + ) + } + } + } + await handleCompleteResult( chatResult, encryptionKey, provider, chatParams.tabId, - chatDisposable + chatDisposable, + languageClient ) } catch (e) { const errorMsg = `Error occurred during chat request: ${e}` @@ -281,13 +370,27 @@ export function registerMessageListeners( encryptionKey, provider, chatParams.tabId, - chatDisposable + chatDisposable, + languageClient ) } finally { chatStreamTokens.delete(chatParams.tabId) } break } + case OPEN_FILE_DIALOG: { + // openFileDialog is the event emitted from webView to open + // file system + const result = await languageClient.sendRequest( + openFileDialogRequestType.method, + message.params + ) + void provider.webview?.postMessage({ + command: openFileDialogRequestType.method, + params: result, + }) + break + } case quickActionRequestType.method: { const quickActionPartialResultToken = uuidv4() const quickActionDisposable = languageClient.onProgress( @@ -312,15 +415,53 @@ export function registerMessageListeners( encryptionKey, provider, message.params.tabId, - quickActionDisposable + quickActionDisposable, + languageClient ) break } + case listRulesRequestType.method: + case ruleClickRequestType.method: case listConversationsRequestType.method: case conversationClickRequestType.method: case listMcpServersRequestType.method: case mcpServerClickRequestType.method: case tabBarActionRequestType.method: + // handling for show_logs button + if (message.params.action === 'show_logs') { + languageClient.info('[VSCode Client] Received show_logs action, showing disclaimer') + + // Show warning message without buttons - just informational + void vscode.window.showWarningMessage( + 'Log files may contain sensitive information such as account IDs, resource names, and other data. Be careful when sharing these logs.' + ) + + // Get the log directory path + const logFolderPath = globals.context.logUri?.fsPath + const result = { ...message.params, success: false } + + if (logFolderPath) { + // Open the log directory in the OS file explorer directly + languageClient.info('[VSCode Client] Opening logs directory') + const path = require('path') + const logFilePath = path.join(logFolderPath, 'Amazon Q Logs.log') + await vscode.commands.executeCommand('revealFileInOS', vscode.Uri.file(logFilePath)) + result.success = true + } else { + // Fallback: show error if log path is not available + void vscode.window.showErrorMessage('Log location not available.') + languageClient.error('[VSCode Client] Log location not available') + } + + void webview?.postMessage({ + command: message.command, + params: result, + }) + + break + } + // eslint-disable-next-line no-fallthrough + case listAvailableModelsRequestType.method: await resolveChatResponse(message.command, message.params, languageClient, webview) break case followUpClickNotificationType.method: @@ -342,6 +483,11 @@ export function registerMessageListeners( } default: if (isServerEvent(message.command)) { + if (enterFocus(message.params)) { + await setContext('aws.amazonq.amazonqChatLSP.isFocus', true) + } else if (exitFocus(message.params)) { + await setContext('aws.amazonq.amazonqChatLSP.isFocus', false) + } languageClient.sendNotification(message.command, message.params) } break @@ -431,6 +577,24 @@ export function registerMessageListeners( } }) + languageClient.onRequest(ShowOpenDialogRequestType.method, async (params: ShowOpenDialogParams) => { + try { + const uris = await vscode.window.showOpenDialog({ + canSelectFiles: params.canSelectFiles ?? true, + canSelectFolders: params.canSelectFolders ?? false, + canSelectMany: params.canSelectMany ?? false, + filters: params.filters, + defaultUri: params.defaultUri ? vscode.Uri.parse(params.defaultUri, false) : undefined, + title: params.title, + }) + const urisString = uris?.map((uri) => uri.fsPath) + return { uris: urisString || [] } + } catch (err) { + languageClient.error(`[VSCode Client] Failed to open file dialog: ${(err as Error).message}`) + return { uris: [] } + } + }) + languageClient.onRequest( ShowDocumentRequest.method, async (params: ShowDocumentParams): Promise> => { @@ -471,33 +635,69 @@ export function registerMessageListeners( params: params, }) }) + languageClient.onNotification( + pinnedContextNotificationType.method, + (params: ContextCommandParams & { tabId: string; textDocument?: TextDocumentIdentifier }) => { + const editor = vscode.window.activeTextEditor + let textDocument = undefined + if (editor && isTextEditor(editor)) { + textDocument = { uri: vscode.workspace.asRelativePath(editor.document.uri) } + } + void provider.webview?.postMessage({ + command: pinnedContextNotificationType.method, + params: { ...params, textDocument }, + }) + } + ) 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) => { @@ -519,27 +719,12 @@ function isServerEvent(command: string) { return command.startsWith('aws/chat/') || command === 'telemetry/event' } -async function encryptRequest(params: T, encryptionKey: Buffer): Promise<{ message: string } | T> { - const payload = new TextEncoder().encode(JSON.stringify(params)) - - const encryptedMessage = await new jose.CompactEncrypt(payload) - .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) - .encrypt(encryptionKey) - - return { message: encryptedMessage } +function enterFocus(params: any) { + return params.name === 'enterFocus' } -async function decodeRequest(request: string, key: Buffer): Promise { - const result = await jose.jwtDecrypt(request, key, { - clockTolerance: 60, // Allow up to 60 seconds to account for clock differences - contentEncryptionAlgorithms: ['A256GCM'], - keyManagementAlgorithms: ['dir'], - }) - - if (!result.payload) { - throw new Error('JWT payload not found') - } - return result.payload as T +function exitFocus(params: any) { + return params.name === 'exitFocus' } /** @@ -551,10 +736,17 @@ async function handlePartialResult( provider: AmazonQChatViewProvider, tabId: string ) { - const decryptedMessage = - typeof partialResult === 'string' && encryptionKey - ? await decodeRequest(partialResult, encryptionKey) - : (partialResult as T) + const decryptedMessage = await decryptResponse(partialResult, encryptionKey) + + // This is to filter out the message containing findings from CodeReview tool to update CodeIssues panel + decryptedMessage.additionalMessages = decryptedMessage.additionalMessages?.filter( + (message) => + !( + message.messageId !== undefined && + (message.messageId.endsWith(CodeWhispererConstants.codeReviewFindingsSuffix) || + message.messageId.endsWith(CodeWhispererConstants.displayFindingsSuffix)) + ) + ) if (decryptedMessage.body !== undefined) { void provider.webview?.postMessage({ @@ -564,6 +756,7 @@ async function handlePartialResult( tabId: tabId, }) } + return decryptedMessage } /** @@ -575,10 +768,13 @@ async function handleCompleteResult( encryptionKey: Buffer | undefined, provider: AmazonQChatViewProvider, tabId: string, - disposable: Disposable + disposable: Disposable, + languageClient: LanguageClient ) { - const decryptedMessage = - typeof result === 'string' && encryptionKey ? await decodeRequest(result, encryptionKey) : (result as T) + const decryptedMessage = await decryptResponse(result, encryptionKey) + + await handleSecurityFindings(decryptedMessage, languageClient) + void provider.webview?.postMessage({ command: chatRequestType.method, params: decryptedMessage, @@ -592,6 +788,54 @@ async function handleCompleteResult( disposable.dispose() } +async function handleSecurityFindings( + decryptedMessage: { additionalMessages?: ChatMessage[] }, + languageClient: LanguageClient +): Promise { + if (decryptedMessage.additionalMessages === undefined || decryptedMessage.additionalMessages.length === 0) { + return + } + for (let i = decryptedMessage.additionalMessages.length - 1; i >= 0; i--) { + const message = decryptedMessage.additionalMessages[i] + if ( + message.messageId !== undefined && + (message.messageId.endsWith(CodeWhispererConstants.codeReviewFindingsSuffix) || + message.messageId.endsWith(CodeWhispererConstants.displayFindingsSuffix)) + ) { + if (message.body !== undefined) { + try { + const aggregatedCodeScanIssues: AggregatedCodeScanIssue[] = JSON.parse(message.body) + for (const aggregatedCodeScanIssue of aggregatedCodeScanIssues) { + const document = await vscode.workspace.openTextDocument(aggregatedCodeScanIssue.filePath) + for (const issue of aggregatedCodeScanIssue.issues) { + const isIssueTitleIgnored = CodeWhispererSettings.instance + .getIgnoredSecurityIssues() + .includes(issue.title) + const isSingleIssueIgnored = CommentUtils.detectCommentAboveLine( + document, + issue.startLine, + CodeWhispererConstants.amazonqIgnoreNextLine + ) + + issue.visible = !isIssueTitleIgnored && !isSingleIssueIgnored + } + } + initSecurityScanRender( + aggregatedCodeScanIssues, + undefined, + CodeAnalysisScope.AGENTIC, + message.messageId.endsWith(CodeWhispererConstants.codeReviewFindingsSuffix) + ) + SecurityIssueTreeViewProvider.focus() + } catch (e) { + languageClient.info('Failed to parse findings') + } + } + decryptedMessage.additionalMessages.splice(i, 1) + } + } +} + async function resolveChatResponse( requestMethod: string, params: any, diff --git a/packages/amazonq/src/lsp/chat/webviewProvider.ts b/packages/amazonq/src/lsp/chat/webviewProvider.ts index bb190b5eb67..109a6afd10f 100644 --- a/packages/amazonq/src/lsp/chat/webviewProvider.ts +++ b/packages/amazonq/src/lsp/chat/webviewProvider.ts @@ -13,6 +13,7 @@ import { Webview, } from 'vscode' import * as path from 'path' +import * as os from 'os' import { globals, isSageMaker, @@ -24,6 +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' export class AmazonQChatViewProvider implements WebviewViewProvider { public static readonly viewType = 'aws.amazonq.AmazonQChatView' @@ -36,7 +38,10 @@ export class AmazonQChatViewProvider implements WebviewViewProvider { connectorAdapterPath?: string uiPath?: string - constructor(private readonly mynahUIPath: string) {} + constructor( + private readonly mynahUIPath: string, + private readonly languageClient: LanguageClient + ) {} public async resolveWebviewView( webviewView: WebviewView, @@ -95,6 +100,8 @@ export class AmazonQChatViewProvider implements WebviewViewProvider { const pairProgrammingAcknowledged = !AmazonQPromptSettings.instance.isPromptEnabled('amazonQChatPairProgramming') const welcomeCount = globals.globalState.tryGet('aws.amazonq.welcomeChatShowCount', Number, 0) + const modelSelectionEnabled = + this.languageClient.initializeResult?.awsServerCapabilities?.chatOptions?.modelSelection ?? false // only show profile card when the two conditions // 1. profile count >= 2 @@ -143,14 +150,14 @@ export class AmazonQChatViewProvider implements WebviewViewProvider { const vscodeApi = acquireVsCodeApi() const hybridChatConnector = new HybridChatAdapter(${(await AuthUtil.instance.getChatAuthState()).amazonQ === 'connected'},${featureConfigData},${welcomeCount},${disclaimerAcknowledged},${regionProfileString},${disabledCommands},${isSMUS},${isSM},vscodeApi.postMessage) const commands = [hybridChatConnector.initialQuickActions[0]] - qChat = amazonQChat.createChat(vscodeApi, {disclaimerAcknowledged: ${disclaimerAcknowledged}, pairProgrammingAcknowledged: ${pairProgrammingAcknowledged}, agenticMode: true, quickActionCommands: commands}, hybridChatConnector, ${JSON.stringify(featureConfigData)}); + qChat = amazonQChat.createChat(vscodeApi, {os: "${os.platform()}", disclaimerAcknowledged: ${disclaimerAcknowledged}, pairProgrammingAcknowledged: ${pairProgrammingAcknowledged}, agenticMode: true, quickActionCommands: commands, modelSelectionEnabled: ${modelSelectionEnabled}}, hybridChatConnector, ${JSON.stringify(featureConfigData)}); } window.addEventListener('message', (event) => { /** * special handler that "simulates" reloading the webview when a profile changes. * required because chat-client relies on initializedResult from the lsp that * are only sent once - * + * * References: * closing tabs: https://github.com/aws/mynah-ui/blob/de736b52f369ba885cd19f33ac86c6f57b4a3134/docs/USAGE.md#removing-a-tab-programmatically- * opening tabs: https://github.com/aws/aws-toolkit-vscode/blob/c22efa03e73b241564c8051c35761eb8620edb83/packages/amazonq/test/e2e/amazonq/framework/framework.ts#L98 diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index 01dac742902..654b68fb914 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import vscode, { env, version } from 'vscode' +import vscode, { version } from 'vscode' import * as nls from 'vscode-nls' import { LanguageClient, LanguageClientOptions, RequestType, State } from 'vscode-languageclient' import { InlineCompletionManager } from '../app/inline/completion' @@ -12,13 +12,20 @@ import { CreateFilesParams, DeleteFilesParams, DidChangeWorkspaceFoldersParams, - DidSaveTextDocumentParams, GetConfigurationFromServerParams, RenameFilesParams, ResponseMessage, WorkspaceFolder, + ConnectionMetadata, } from '@aws/language-server-runtimes/protocol' -import { AuthUtil, CodeWhispererSettings, getSelectedCustomization } from 'aws-core-vscode/codewhisperer' +import { + AuthUtil, + CodeWhispererSettings, + FeatureConfigProvider, + getSelectedCustomization, + TelemetryHelper, + vsCodeState, +} from 'aws-core-vscode/codewhisperer' import { Settings, createServerOptions, @@ -32,22 +39,45 @@ import { getOptOutPreference, isAmazonLinux2, getClientId, + getClientName, extensionVersion, + isSageMaker, + DevSettings, } 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' import { telemetry } from 'aws-core-vscode/telemetry' +import { SessionManager } from '../app/inline/sessionManager' +import { LineTracker } from '../app/inline/stateTracker/lineTracker' +import { InlineTutorialAnnotation } from '../app/inline/tutorials/inlineTutorialAnnotation' +import { InlineChatTutorialAnnotation } from '../app/inline/tutorials/inlineChatTutorialAnnotation' +import { codeReviewInChat } from '../app/amazonqScan/models/constants' const localize = nls.loadMessageBundle() const logger = getLogger('amazonqLsp.lspClient') -export const glibcLinker: string = process.env.VSCODE_SERVER_CUSTOM_GLIBC_LINKER || '' -export const glibcPath: string = process.env.VSCODE_SERVER_CUSTOM_GLIBC_PATH || '' - export function hasGlibcPatch(): boolean { - return glibcLinker.length > 0 && glibcPath.length > 0 + // Skip GLIBC patching for SageMaker environments + if (isSageMaker()) { + getLogger('amazonqLsp').info('SageMaker environment detected in hasGlibcPatch, skipping GLIBC patching') + return false // Return false to ensure SageMaker doesn't try to use GLIBC patching + } + + // Check for environment variables (for CDM) + const glibcLinker = process.env.VSCODE_SERVER_CUSTOM_GLIBC_LINKER || '' + const glibcPath = process.env.VSCODE_SERVER_CUSTOM_GLIBC_PATH || '' + + if (glibcLinker.length > 0 && glibcPath.length > 0) { + getLogger('amazonqLsp').info('GLIBC patching environment variables detected') + return true + } + + // No environment variables, no patching needed + return false } export async function startLanguageServer( @@ -72,9 +102,24 @@ export async function startLanguageServer( const traceServerEnabled = Settings.instance.isSet(`${clientId}.trace.server`) let executable: string[] = [] // apply the GLIBC 2.28 path to node js runtime binary - if (isAmazonLinux2() && hasGlibcPatch()) { - executable = [glibcLinker, '--library-path', glibcPath, resourcePaths.node] - getLogger('amazonqLsp').info(`Patched node runtime with GLIBC to ${executable}`) + if (isSageMaker()) { + // SageMaker doesn't need GLIBC patching + getLogger('amazonqLsp').info('SageMaker environment detected, skipping GLIBC patching') + executable = [resourcePaths.node] + } else if (isAmazonLinux2() && hasGlibcPatch()) { + // Use environment variables if available (for CDM) + if (process.env.VSCODE_SERVER_CUSTOM_GLIBC_LINKER && process.env.VSCODE_SERVER_CUSTOM_GLIBC_PATH) { + executable = [ + process.env.VSCODE_SERVER_CUSTOM_GLIBC_LINKER, + '--library-path', + process.env.VSCODE_SERVER_CUSTOM_GLIBC_PATH, + resourcePaths.node, + ] + getLogger('amazonqLsp').info(`Patched node runtime with GLIBC using env vars to ${executable}`) + } else { + // No environment variables, use the node executable directly + executable = [resourcePaths.node] + } } else { executable = [resourcePaths.node] } @@ -90,6 +135,15 @@ export async function startLanguageServer( await validateNodeExe(executable, resourcePaths.lsp, argv, logger) + const endpointOverride = DevSettings.instance.get('codewhispererService', {}).endpoint ?? undefined + const textDocSection = { + inlineEditSupport: Experiments.instance.get('amazonqLSPNEP', true), + } as any + + if (endpointOverride) { + textDocSection.endpointOverride = endpointOverride + } + // Options to control the language client const clientOptions: LanguageClientOptions = { // Register the server for json documents @@ -112,7 +166,7 @@ export async function startLanguageServer( initializationOptions: { aws: { clientInfo: { - name: env.appName, + name: getClientName(), version: version, extension: { name: 'AmazonQ-For-VSCode', @@ -123,20 +177,34 @@ export async function startLanguageServer( awsClientCapabilities: { q: { developerProfiles: true, + pinnedContextEnabled: true, + imageContextEnabled: true, mcp: true, + shortcut: true, + reroute: true, + modelSelection: true, + workspaceFilePath: vscode.workspace.workspaceFile?.fsPath, + codeReviewInChat: codeReviewInChat, + // feature flag for displaying findings found not through CodeReview in the Code Issues Panel + displayFindings: true, }, window: { notifications: true, showSaveFileDialog: true, + showLogs: isSageMaker() ? false : true, + }, + textDocument: { + inlineCompletionWithReferences: textDocSection, }, }, contextConfiguration: { workspaceIdentifier: extensionContext.storageUri?.path, }, - logLevel: toAmazonQLSPLogLevel(globals.logOutputChannel.logLevel), + logLevel: isSageMaker() ? 'debug' : toAmazonQLSPLogLevel(globals.logOutputChannel.logLevel), }, credentials: { providesBearerToken: true, + providesIam: isSageMaker(), // Enable IAM credentials for SageMaker environments }, }, /** @@ -162,9 +230,35 @@ export async function startLanguageServer( toDispose.push(disposable) await client.onReady() + // Set up connection metadata handler + client.onRequest(notificationTypes.getConnectionMetadata.method, () => { + // For IAM auth, provide a default startUrl + if (process.env.USE_IAM_AUTH === 'true') { + getLogger().info( + `[SageMaker Debug] Connection metadata requested - returning hardcoded startUrl for IAM auth` + ) + return { + sso: { + // TODO P261194666 Replace with correct startUrl once identified + startUrl: 'https://amzn.awsapps.com/start', // Default for IAM auth + }, + } + } + + // For SSO auth, use the actual startUrl + getLogger().info( + `[SageMaker Debug] Connection metadata requested - returning actual startUrl for SSO auth: ${AuthUtil.instance.auth.startUrl}` + ) + return { + sso: { + startUrl: AuthUtil.instance.auth.startUrl, + }, + } + }) + const auth = await initializeAuth(client) - await onLanguageServerReady(auth, client, resourcePaths, toDispose) + await onLanguageServerReady(extensionContext, auth, client, resourcePaths, toDispose) return client } @@ -175,18 +269,105 @@ async function initializeAuth(client: LanguageClient): Promise { return auth } +// jscpd:ignore-start +async function initializeLanguageServerConfiguration(client: LanguageClient, context: string = 'startup') { + const logger = getLogger('amazonqLsp') + + if (AuthUtil.instance.isConnectionValid()) { + logger.info(`[${context}] Initializing language server configuration`) + // jscpd:ignore-end + + try { + // Send profile configuration + logger.debug(`[${context}] Sending profile configuration to language server`) + await sendProfileToLsp(client) + logger.debug(`[${context}] Profile configuration sent successfully`) + + // Send customization configuration + logger.debug(`[${context}] Sending customization configuration to language server`) + await pushConfigUpdate(client, { + type: 'customization', + customization: getSelectedCustomization(), + }) + logger.debug(`[${context}] Customization configuration sent successfully`) + + logger.info(`[${context}] Language server configuration completed successfully`) + } catch (error) { + logger.error(`[${context}] Failed to initialize language server configuration: ${error}`) + throw error + } + } else { + logger.warn( + `[${context}] Connection invalid, skipping language server configuration - this will cause authentication failures` + ) + const activeConnection = AuthUtil.instance.auth.activeConnection + const connectionState = activeConnection + ? AuthUtil.instance.auth.getConnectionState(activeConnection) + : 'no-connection' + logger.warn(`[${context}] Connection state: ${connectionState}`) + } +} + +async function sendProfileToLsp(client: LanguageClient) { + const logger = getLogger('amazonqLsp') + const profileArn = AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn + + logger.debug(`Sending profile to LSP: ${profileArn || 'undefined'}`) + + await pushConfigUpdate(client, { + type: 'profile', + profileArn: profileArn, + }) + + logger.debug(`Profile sent to LSP successfully`) +} + async function onLanguageServerReady( + extensionContext: vscode.ExtensionContext, auth: AmazonQLspAuth, client: LanguageClient, resourcePaths: AmazonQResourcePaths, toDispose: vscode.Disposable[] ) { - if (Experiments.instance.get('amazonqLSPInline', false)) { - const inlineManager = new InlineCompletionManager(client) + const sessionManager = new SessionManager() + + // keeps track of the line changes + const lineTracker = new LineTracker() + + // tutorial for inline suggestions + const inlineTutorialAnnotation = new InlineTutorialAnnotation(lineTracker, sessionManager) + + // tutorial for inline chat + const inlineChatTutorialAnnotation = new InlineChatTutorialAnnotation(inlineTutorialAnnotation) + + 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 () => { @@ -195,6 +376,8 @@ async function onLanguageServerReady( ) } + activateInlineChat(extensionContext, client, encryptionKey, inlineChatTutorialAnnotation) + if (Experiments.instance.get('amazonqChatLSP', true)) { await activate(client, encryptionKey, resourcePaths.ui) } @@ -204,16 +387,34 @@ async function onLanguageServerReady( // We manually push the cached values the first time since event handlers, which should push, may not have been setup yet. // Execution order is weird and should be fixed in the flare implementation. // TODO: Revisit if we need this if we setup the event handlers properly - if (AuthUtil.instance.isConnectionValid()) { - await sendProfileToLsp(client) - - await pushConfigUpdate(client, { - type: 'customization', - customization: getSelectedCustomization(), - }) - } + await initializeLanguageServerConfiguration(client, 'startup') toDispose.push( + Commands.register('aws.amazonq.refreshAnnotation', async (forceProceed: boolean) => { + telemetry.record({ + traceId: TelemetryHelper.instance.traceId, + }) + + const editor = vscode.window.activeTextEditor + if (editor) { + if (forceProceed) { + await inlineTutorialAnnotation.refresh(editor, 'codewhisperer', true) + } else { + await inlineTutorialAnnotation.refresh(editor, 'codewhisperer') + } + } + }), + Commands.register('aws.amazonq.dismissTutorial', async () => { + const editor = vscode.window.activeTextEditor + if (editor) { + inlineTutorialAnnotation.clear() + try { + telemetry.ui_click.emit({ elementId: `dismiss_${inlineTutorialAnnotation.currentState.id}` }) + } catch (_) {} + await inlineTutorialAnnotation.dismissTutorial() + getLogger().debug(`codewhisperer: user dismiss tutorial.`) + } + }), AuthUtil.instance.auth.onDidChangeActiveConnection(async () => { await auth.refreshConnection() }), @@ -251,13 +452,6 @@ async function onLanguageServerReady( }), } as RenameFilesParams) }), - vscode.workspace.onDidSaveTextDocument((e) => { - client.sendNotification('workspace/didSaveTextDocument', { - textDocument: { - uri: e.uri.fsPath, - }, - } as DidSaveTextDocumentParams) - }), vscode.workspace.onDidChangeWorkspaceFolders((e) => { client.sendNotification('workspace/didChangeWorkspaceFolder', { event: { @@ -280,13 +474,6 @@ async function onLanguageServerReady( // Set this inside onReady so that it only triggers on subsequent language server starts (not the first) onServerRestartHandler(client, auth) ) - - async function sendProfileToLsp(client: LanguageClient) { - await pushConfigUpdate(client, { - type: 'profile', - profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, - }) - } } /** @@ -306,8 +493,21 @@ function onServerRestartHandler(client: LanguageClient, auth: AmazonQLspAuth) { // TODO: Port this metric override to common definitions telemetry.languageServer_crash.emit({ id: 'AmazonQ' }) - // Need to set the auth token in the again - await auth.refreshConnection(true) + const logger = getLogger('amazonqLsp') + logger.info('[crash-recovery] Language server crash detected, reinitializing authentication') + + try { + // Send bearer token + logger.debug('[crash-recovery] Refreshing connection and sending bearer token') + await auth.refreshConnection(true) + logger.debug('[crash-recovery] Bearer token sent successfully') + + // Send profile and customization configuration + await initializeLanguageServerConfiguration(client, 'crash-recovery') + logger.info('[crash-recovery] Authentication reinitialized successfully') + } catch (error) { + logger.error(`[crash-recovery] Failed to reinitialize after crash: ${error}`) + } }) } diff --git a/packages/amazonq/src/lsp/config.ts b/packages/amazonq/src/lsp/config.ts index 1760fb51401..6b88eb98d21 100644 --- a/packages/amazonq/src/lsp/config.ts +++ b/packages/amazonq/src/lsp/config.ts @@ -3,15 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as vscode from 'vscode' -import { DevSettings, getServiceEnvVarConfig } from 'aws-core-vscode/shared' -import { LspConfig } from 'aws-core-vscode/amazonq' +import { DevSettings, getServiceEnvVarConfig, BaseLspInstaller, getLogger } from 'aws-core-vscode/shared' import { LanguageClient } from 'vscode-languageclient' import { DidChangeConfigurationNotification, updateConfigurationRequestType, } from '@aws/language-server-runtimes/protocol' -export interface ExtendedAmazonQLSPConfig extends LspConfig { +export interface ExtendedAmazonQLSPConfig extends BaseLspInstaller.LspConfig { ui?: string } @@ -69,23 +68,31 @@ export function toAmazonQLSPLogLevel(logLevel: vscode.LogLevel): LspLogLevel { * push the given config. */ export async function pushConfigUpdate(client: LanguageClient, config: QConfigs) { + const logger = getLogger('amazonqLsp') + switch (config.type) { case 'profile': + logger.debug(`Pushing profile configuration: ${config.profileArn || 'undefined'}`) await client.sendRequest(updateConfigurationRequestType.method, { section: 'aws.q', settings: { profileArn: config.profileArn }, }) + logger.debug(`Profile configuration pushed successfully`) break case 'customization': + logger.debug(`Pushing customization configuration: ${config.customization || 'undefined'}`) client.sendNotification(DidChangeConfigurationNotification.type.method, { section: 'aws.q', settings: { customization: config.customization }, }) + logger.debug(`Customization configuration pushed successfully`) break case 'logLevel': + logger.debug(`Pushing log level configuration`) client.sendNotification(DidChangeConfigurationNotification.type.method, { section: 'aws.logLevel', }) + logger.debug(`Log level configuration pushed successfully`) break } } diff --git a/packages/amazonq/src/lsp/encryption.ts b/packages/amazonq/src/lsp/encryption.ts new file mode 100644 index 00000000000..246c64f476b --- /dev/null +++ b/packages/amazonq/src/lsp/encryption.ts @@ -0,0 +1,34 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as jose from 'jose' + +export async function encryptRequest(params: T, encryptionKey: Buffer): Promise<{ message: string } | T> { + const payload = new TextEncoder().encode(JSON.stringify(params)) + + const encryptedMessage = await new jose.CompactEncrypt(payload) + .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) + .encrypt(encryptionKey) + + return { message: encryptedMessage } +} + +export async function decryptResponse(response: unknown, key: Buffer | undefined) { + // Note that casts are required since language client requests return 'unknown' type. + // If we can't decrypt, return original response casted. + if (typeof response !== 'string' || key === undefined) { + return response as T + } + + const result = await jose.jwtDecrypt(response, key, { + clockTolerance: 60, // Allow up to 60 seconds to account for clock differences + contentEncryptionAlgorithms: ['A256GCM'], + keyManagementAlgorithms: ['dir'], + }) + + if (!result.payload) { + throw new Error('JWT payload not found') + } + return result.payload as T +} diff --git a/packages/amazonq/src/lsp/utils.ts b/packages/amazonq/src/lsp/utils.ts new file mode 100644 index 00000000000..f5b010c536b --- /dev/null +++ b/packages/amazonq/src/lsp/utils.ts @@ -0,0 +1,26 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as vscode from 'vscode' +import { CursorState } from '@aws/language-server-runtimes-types' + +/** + * Convert from vscode selection type to the general CursorState expected by the AmazonQLSP. + * @param selection + * @returns + */ +export function getCursorState(selection: readonly vscode.Selection[]): CursorState[] { + return selection.map((s) => ({ + range: { + start: { + line: s.start.line, + character: s.start.character, + }, + end: { + line: s.end.line, + character: s.end.character, + }, + }, + })) +} diff --git a/packages/amazonq/src/util/timeoutUtil.ts b/packages/amazonq/src/util/timeoutUtil.ts new file mode 100644 index 00000000000..c42d1e3be01 --- /dev/null +++ b/packages/amazonq/src/util/timeoutUtil.ts @@ -0,0 +1,15 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +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 + }) +} diff --git a/packages/amazonq/test/e2e/amazonq/doc.test.ts b/packages/amazonq/test/e2e/amazonq/doc.test.ts deleted file mode 100644 index 20d281fe7b8..00000000000 --- a/packages/amazonq/test/e2e/amazonq/doc.test.ts +++ /dev/null @@ -1,492 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import vscode from 'vscode' -import assert from 'assert' -import { qTestingFramework } from './framework/framework' -import { getTestWindow, registerAuthHook, toTextEditor, using } from 'aws-core-vscode/test' -import { loginToIdC } from './utils/setup' -import { Messenger } from './framework/messenger' -import { FollowUpTypes } from 'aws-core-vscode/amazonq' -import { fs, i18n, sleep } from 'aws-core-vscode/shared' -import { - docGenerationProgressMessage, - DocGenerationStep, - docGenerationSuccessMessage, - docRejectConfirmation, - Mode, -} from 'aws-core-vscode/amazonqDoc' - -describe('Amazon Q Doc Generation', async function () { - let framework: qTestingFramework - let tab: Messenger - let workspaceUri: vscode.Uri - let rootReadmeFileUri: vscode.Uri - - type testProjectConfig = { - path: string - language: string - mockFile: string - mockContent: string - } - const testProjects: testProjectConfig[] = [ - { - path: 'ts-plain-sam-app', - language: 'TypeScript', - mockFile: 'bubbleSort.ts', - mockContent: ` - function bubbleSort(arr: number[]): number[] { - const n = arr.length; - for (let i = 0; i < n - 1; i++) { - for (let j = 0; j < n - i - 1; j++) { - if (arr[j] > arr[j + 1]) { - [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; - } - } - } - return arr; - }`, - }, - { - path: 'ruby-plain-sam-app', - language: 'Ruby', - mockFile: 'bubble_sort.rb', - mockContent: ` - def bubble_sort(arr) - n = arr.length - (n-1).times do |i| - (0..n-i-2).each do |j| - if arr[j] > arr[j+1] - arr[j], arr[j+1] = arr[j+1], arr[j] - end - end - end - arr - end`, - }, - { - path: 'js-plain-sam-app', - language: 'JavaScript', - mockFile: 'bubbleSort.js', - mockContent: ` - function bubbleSort(arr) { - const n = arr.length; - for (let i = 0; i < n - 1; i++) { - for (let j = 0; j < n - i - 1; j++) { - if (arr[j] > arr[j + 1]) { - [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; - } - } - } - return arr; - }`, - }, - { - path: 'java11-plain-maven-sam-app', - language: 'Java', - mockFile: 'BubbleSort.java', - mockContent: ` - public static void bubbleSort(int[] arr) { - int n = arr.length; - for (int i = 0; i < n - 1; i++) { - for (int j = 0; j < n - i - 1; j++) { - if (arr[j] > arr[j + 1]) { - int temp = arr[j]; - arr[j] = arr[j + 1]; - arr[j + 1] = temp; - } - } - } - }`, - }, - { - path: 'go1-plain-sam-app', - language: 'Go', - mockFile: 'bubble_sort.go', - mockContent: ` - func bubbleSort(arr []int) []int { - n := len(arr) - for i := 0; i < n-1; i++ { - for j := 0; j < n-i-1; j++ { - if arr[j] > arr[j+1] { - arr[j], arr[j+1] = arr[j+1], arr[j] - } - } - } - return arr - }`, - }, - { - path: 'python3.7-plain-sam-app', - language: 'Python', - mockFile: 'bubble_sort.py', - mockContent: ` - def bubble_sort(arr): - n = len(arr) - for i in range(n-1): - for j in range(0, n-i-1): - if arr[j] > arr[j+1]: - arr[j], arr[j+1] = arr[j+1], arr[j] - return arr`, - }, - ] - - const docUtils = { - async initializeDocOperation(operation: 'create' | 'update' | 'edit') { - console.log(`Initializing documentation ${operation} operation`) - - switch (operation) { - case 'create': - await tab.waitForButtons([FollowUpTypes.CreateDocumentation, FollowUpTypes.UpdateDocumentation]) - tab.clickButton(FollowUpTypes.CreateDocumentation) - await tab.waitForText(i18n('AWS.amazonq.doc.answer.createReadme')) - break - case 'update': - await tab.waitForButtons([FollowUpTypes.CreateDocumentation, FollowUpTypes.UpdateDocumentation]) - tab.clickButton(FollowUpTypes.UpdateDocumentation) - await tab.waitForButtons([FollowUpTypes.SynchronizeDocumentation, FollowUpTypes.EditDocumentation]) - tab.clickButton(FollowUpTypes.SynchronizeDocumentation) - await tab.waitForText(i18n('AWS.amazonq.doc.answer.updateReadme')) - break - case 'edit': - await tab.waitForButtons([FollowUpTypes.UpdateDocumentation]) - tab.clickButton(FollowUpTypes.UpdateDocumentation) - await tab.waitForButtons([FollowUpTypes.SynchronizeDocumentation, FollowUpTypes.EditDocumentation]) - tab.clickButton(FollowUpTypes.EditDocumentation) - await tab.waitForText(i18n('AWS.amazonq.doc.answer.updateReadme')) - break - } - }, - - async handleFolderSelection(testProject: testProjectConfig) { - console.table({ - 'Test in project': { - Path: testProject.path, - Language: testProject.language, - }, - }) - - const projectUri = vscode.Uri.joinPath(workspaceUri, testProject.path) - const readmeFileUri = vscode.Uri.joinPath(projectUri, 'README.md') - - // Cleanup existing README - await fs.delete(readmeFileUri, { force: true }) - - await tab.waitForButtons([FollowUpTypes.ProceedFolderSelection, FollowUpTypes.ChooseFolder]) - tab.clickButton(FollowUpTypes.ChooseFolder) - getTestWindow().onDidShowDialog((d) => d.selectItem(projectUri)) - - return readmeFileUri - }, - - async executeDocumentationFlow(operation: 'create' | 'update' | 'edit', msg?: string) { - const mode = { - create: Mode.CREATE, - update: Mode.SYNC, - edit: Mode.EDIT, - }[operation] - - console.log(`Executing documentation ${operation} flow`) - - await tab.waitForButtons([FollowUpTypes.ProceedFolderSelection]) - tab.clickButton(FollowUpTypes.ProceedFolderSelection) - - if (mode === Mode.EDIT && msg) { - tab.addChatMessage({ prompt: msg }) - } - await tab.waitForText(docGenerationProgressMessage(DocGenerationStep.SUMMARIZING_FILES, mode)) - await tab.waitForText(`${docGenerationSuccessMessage(mode)} ${i18n('AWS.amazonq.doc.answer.codeResult')}`) - await tab.waitForButtons([ - FollowUpTypes.AcceptChanges, - FollowUpTypes.MakeChanges, - FollowUpTypes.RejectChanges, - ]) - }, - - async verifyResult(action: FollowUpTypes, readmeFileUri?: vscode.Uri, shouldExist = true) { - tab.clickButton(action) - - if (action === FollowUpTypes.RejectChanges) { - await tab.waitForText(docRejectConfirmation) - assert.deepStrictEqual(tab.getChatItems().pop()?.body, docRejectConfirmation) - } - await tab.waitForButtons([FollowUpTypes.NewTask, FollowUpTypes.CloseSession]) - - if (readmeFileUri) { - const fileExists = await fs.exists(readmeFileUri) - console.log(`README file exists: ${fileExists}, Expected: ${shouldExist}`) - assert.strictEqual( - fileExists, - shouldExist, - shouldExist - ? 'README file was not saved to the appropriate folder' - : 'README file should not be saved to the folder' - ) - if (fileExists) { - await fs.delete(readmeFileUri, { force: true }) - } - } - }, - - async prepareMockFile(testProject: testProjectConfig) { - const folderUri = vscode.Uri.joinPath(workspaceUri, testProject.path) - const mockFileUri = vscode.Uri.joinPath(folderUri, testProject.mockFile) - await toTextEditor(testProject.mockContent, testProject.mockFile, folderUri.path) - return mockFileUri - }, - - getRandomTestProject() { - const randomIndex = Math.floor(Math.random() * testProjects.length) - return testProjects[randomIndex] - }, - async setupTest() { - tab = framework.createTab() - tab.addChatMessage({ command: '/doc' }) - tab = framework.getSelectedTab() - await tab.waitForChatFinishesLoading() - }, - } - /** - * Executes a test method with automatic retry capability for retryable errors. - * Uses Promise.race to detect errors during test execution without hanging. - */ - async function retryIfRequired(testMethod: () => Promise, maxAttempts: number = 3) { - const errorMessages = { - tooManyRequests: 'Too many requests', - unexpectedError: 'Encountered an unexpected error when processing the request', - } - const hasRetryableError = () => { - const lastTwoMessages = tab - .getChatItems() - .slice(-2) - .map((item) => item.body) - return lastTwoMessages.some( - (body) => body?.includes(errorMessages.unexpectedError) || body?.includes(errorMessages.tooManyRequests) - ) - } - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - console.log(`Attempt ${attempt}/${maxAttempts}`) - const errorDetectionPromise = new Promise((_, reject) => { - const errorCheckInterval = setInterval(() => { - if (hasRetryableError()) { - clearInterval(errorCheckInterval) - reject(new Error('Retryable error detected')) - } - }, 1000) - }) - try { - await Promise.race([testMethod(), errorDetectionPromise]) - return - } catch (error) { - if (attempt === maxAttempts) { - assert.fail(`Test failed after ${maxAttempts} attempts`) - } - console.log(`Attempt ${attempt} failed, retrying...`) - await sleep(1000 * attempt) - await docUtils.setupTest() - } - } - } - before(async function () { - /** - * The tests are getting throttled, only run them on stable for now - * - * TODO: Re-enable for all versions once the backend can handle them - */ - - const testVersion = process.env['VSCODE_TEST_VERSION'] - if (testVersion && testVersion !== 'stable') { - this.skip() - } - - await using(registerAuthHook('amazonq-test-account'), async () => { - await loginToIdC() - }) - }) - - beforeEach(() => { - registerAuthHook('amazonq-test-account') - framework = new qTestingFramework('doc', true, []) - tab = framework.createTab() - const wsFolders = vscode.workspace.workspaceFolders - if (!wsFolders?.length) { - assert.fail('Workspace folder not found') - } - workspaceUri = wsFolders[0].uri - rootReadmeFileUri = vscode.Uri.joinPath(workspaceUri, 'README.md') - }) - - afterEach(() => { - framework.removeTab(tab.tabID) - framework.dispose() - }) - - describe('Quick action availability', () => { - it('Shows /doc command when doc generation is enabled', async () => { - const command = tab.findCommand('/doc') - if (!command.length) { - assert.fail('Could not find command') - } - - if (command.length > 1) { - assert.fail('Found too many commands with the name /doc') - } - }) - - it('Hide /doc command when doc generation is NOT enabled', () => { - // The beforeEach registers a framework which accepts requests. If we don't dispose before building a new one we have duplicate messages - framework.dispose() - framework = new qTestingFramework('doc', false, []) - const tab = framework.createTab() - const command = tab.findCommand('/doc') - if (command.length > 0) { - assert.fail('Found command when it should not have been found') - } - }) - }) - - describe('/doc entry', () => { - beforeEach(async function () { - await docUtils.setupTest() - }) - - it('Display create and update options on initial load', async () => { - await tab.waitForButtons([FollowUpTypes.CreateDocumentation, FollowUpTypes.UpdateDocumentation]) - }) - it('Return to the select create or update documentation state when cancel button clicked', async () => { - await tab.waitForButtons([FollowUpTypes.CreateDocumentation, FollowUpTypes.UpdateDocumentation]) - tab.clickButton(FollowUpTypes.UpdateDocumentation) - await tab.waitForButtons([FollowUpTypes.SynchronizeDocumentation, FollowUpTypes.EditDocumentation]) - tab.clickButton(FollowUpTypes.SynchronizeDocumentation) - await tab.waitForButtons([ - FollowUpTypes.ProceedFolderSelection, - FollowUpTypes.ChooseFolder, - FollowUpTypes.CancelFolderSelection, - ]) - tab.clickButton(FollowUpTypes.CancelFolderSelection) - await tab.waitForChatFinishesLoading() - const followupButton = tab.getFollowUpButton(FollowUpTypes.CreateDocumentation) - if (!followupButton) { - assert.fail('Could not find follow up button for create or update readme') - } - }) - }) - - describe('README Creation', () => { - let testProject: testProjectConfig - beforeEach(async function () { - await docUtils.setupTest() - testProject = docUtils.getRandomTestProject() - }) - - it('Create and save README in root folder when accepted', async () => { - await retryIfRequired(async () => { - await docUtils.initializeDocOperation('create') - await docUtils.executeDocumentationFlow('create') - await docUtils.verifyResult(FollowUpTypes.AcceptChanges, rootReadmeFileUri, true) - }) - }) - it('Create and save README in subfolder when accepted', async () => { - await retryIfRequired(async () => { - await docUtils.initializeDocOperation('create') - const readmeFileUri = await docUtils.handleFolderSelection(testProject) - await docUtils.executeDocumentationFlow('create') - await docUtils.verifyResult(FollowUpTypes.AcceptChanges, readmeFileUri, true) - }) - }) - - it('Discard README in subfolder when rejected', async () => { - await retryIfRequired(async () => { - await docUtils.initializeDocOperation('create') - const readmeFileUri = await docUtils.handleFolderSelection(testProject) - await docUtils.executeDocumentationFlow('create') - await docUtils.verifyResult(FollowUpTypes.RejectChanges, readmeFileUri, false) - }) - }) - }) - - describe('README Editing', () => { - beforeEach(async function () { - await docUtils.setupTest() - }) - - it('Apply specific content changes when requested', async () => { - await retryIfRequired(async () => { - await docUtils.initializeDocOperation('edit') - await docUtils.executeDocumentationFlow('edit', 'remove the repository structure section') - await docUtils.verifyResult(FollowUpTypes.AcceptChanges, rootReadmeFileUri, true) - }) - }) - - it('Handle unrelated prompts with appropriate error message', async () => { - await retryIfRequired(async () => { - await docUtils.initializeDocOperation('edit') - await tab.waitForButtons([FollowUpTypes.ProceedFolderSelection]) - tab.clickButton(FollowUpTypes.ProceedFolderSelection) - tab.addChatMessage({ prompt: 'tell me about the weather' }) - await tab.waitForEvent(() => - tab - .getChatItems() - .some(({ body }) => body?.startsWith(i18n('AWS.amazonq.doc.error.promptUnrelated'))) - ) - await tab.waitForEvent(() => { - const store = tab.getStore() - return ( - !store.promptInputDisabledState && - store.promptInputPlaceholder === i18n('AWS.amazonq.doc.placeholder.editReadme') - ) - }) - }) - }) - }) - describe('README Updates', () => { - let testProject: testProjectConfig - let mockFileUri: vscode.Uri - - beforeEach(async function () { - await docUtils.setupTest() - testProject = docUtils.getRandomTestProject() - }) - afterEach(async function () { - // Clean up mock file - if (mockFileUri) { - await fs.delete(mockFileUri, { force: true }) - } - }) - - it('Update README with code change in subfolder', async () => { - mockFileUri = await docUtils.prepareMockFile(testProject) - await retryIfRequired(async () => { - await docUtils.initializeDocOperation('update') - const readmeFileUri = await docUtils.handleFolderSelection(testProject) - await docUtils.executeDocumentationFlow('update') - await docUtils.verifyResult(FollowUpTypes.AcceptChanges, readmeFileUri, true) - }) - }) - it('Update root README and incorporate additional changes', async () => { - // Cleanup any existing README - await fs.delete(rootReadmeFileUri, { force: true }) - mockFileUri = await docUtils.prepareMockFile(testProject) - await retryIfRequired(async () => { - await docUtils.initializeDocOperation('update') - await docUtils.executeDocumentationFlow('update') - tab.clickButton(FollowUpTypes.MakeChanges) - tab.addChatMessage({ prompt: 'remove the repository structure section' }) - - await tab.waitForText(docGenerationProgressMessage(DocGenerationStep.SUMMARIZING_FILES, Mode.SYNC)) - await tab.waitForText( - `${docGenerationSuccessMessage(Mode.SYNC)} ${i18n('AWS.amazonq.doc.answer.codeResult')}` - ) - await tab.waitForButtons([ - FollowUpTypes.AcceptChanges, - FollowUpTypes.MakeChanges, - FollowUpTypes.RejectChanges, - ]) - - await docUtils.verifyResult(FollowUpTypes.AcceptChanges, rootReadmeFileUri, true) - }) - }) - }) -}) diff --git a/packages/amazonq/test/e2e/amazonq/explore.test.ts b/packages/amazonq/test/e2e/amazonq/explore.test.ts deleted file mode 100644 index 970d93d00bb..00000000000 --- a/packages/amazonq/test/e2e/amazonq/explore.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -/*! - * 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 { qTestingFramework } from './framework/framework' -import { Messenger } from './framework/messenger' - -describe('Amazon Q Explore page', function () { - let framework: qTestingFramework - let tab: Messenger - - beforeEach(() => { - framework = new qTestingFramework('agentWalkthrough', true, [], 0) - const welcomeTab = framework.getTabs()[0] - welcomeTab.clickInBodyButton('explore') - - // Find the new explore tab - const exploreTab = framework.findTab('Explore') - if (!exploreTab) { - assert.fail('Explore tab not found') - } - tab = exploreTab - }) - - afterEach(() => { - framework.removeTab(tab.tabID) - framework.dispose() - sinon.restore() - }) - - // TODO refactor page objects so we can associate clicking user guides with actual urls - // TODO test that clicking quick start changes the tab title, etc - it('should have correct button IDs', async () => { - const features = ['featuredev', 'testgen', 'doc', 'review', 'gumby'] - - for (const [index, feature] of features.entries()) { - const buttons = (tab.getStore().chatItems ?? [])[index].buttons ?? [] - assert.deepStrictEqual(buttons[0].id, `user-guide-${feature}`) - assert.deepStrictEqual(buttons[1].id, `quick-start-${feature}`) - } - }) -}) diff --git a/packages/amazonq/test/e2e/amazonq/featureDev.test.ts b/packages/amazonq/test/e2e/amazonq/featureDev.test.ts deleted file mode 100644 index 87099e2a2d0..00000000000 --- a/packages/amazonq/test/e2e/amazonq/featureDev.test.ts +++ /dev/null @@ -1,345 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import { qTestingFramework } from './framework/framework' -import sinon from 'sinon' -import { registerAuthHook, using } from 'aws-core-vscode/test' -import { loginToIdC } from './utils/setup' -import { Messenger } from './framework/messenger' -import { FollowUpTypes } from 'aws-core-vscode/amazonq' -import { sleep } from 'aws-core-vscode/shared' - -describe('Amazon Q Feature Dev', function () { - let framework: qTestingFramework - let tab: Messenger - - const prompt = 'Add current timestamp into blank.txt' - const iteratePrompt = `Add a new section in readme to explain your change` - const fileLevelAcceptPrompt = `${prompt} and ${iteratePrompt}` - const informationCard = - 'After you provide a task, I will:\n1. Generate code based on your description and the code in your workspace\n2. Provide a list of suggestions for you to review and add to your workspace\n3. If needed, iterate based on your feedback\nTo learn more, visit the [user guide](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/software-dev.html)' - const tooManyRequestsWaitTime = 100000 - - async function waitForText(text: string) { - await tab.waitForText(text, { - waitIntervalInMs: 250, - waitTimeoutInMs: 2000, - }) - } - - async function iterate(prompt: string) { - tab.addChatMessage({ prompt }) - - await retryIfRequired( - async () => { - // Wait for a backend response - await tab.waitForChatFinishesLoading() - }, - () => {} - ) - } - - async function clickActionButton(filePath: string, actionName: string) { - tab.clickFileActionButton(filePath, actionName) - await tab.waitForEvent(() => !tab.hasAction(filePath, actionName), { - waitIntervalInMs: 500, - waitTimeoutInMs: 600000, - }) - } - - /** - * Wait for the original request to finish. - * If the response has a retry button or encountered a guardrails error, continue retrying - * - * This allows the e2e tests to recover from potential one off backend problems/random guardrails - */ - async function retryIfRequired(waitUntilReady: () => Promise, request?: () => void) { - await waitUntilReady() - - const findAnotherTopic = 'find another topic to discuss' - const tooManyRequests = 'Too many requests' - const failureState = (message: string) => { - return ( - tab.getChatItems().pop()?.body?.includes(message) || - tab.getChatItems().slice(-2).shift()?.body?.includes(message) - ) - } - while ( - tab.hasButton(FollowUpTypes.Retry) || - (request && (failureState(findAnotherTopic) || failureState(tooManyRequests))) - ) { - if (tab.hasButton(FollowUpTypes.Retry)) { - console.log('Retrying request') - tab.clickButton(FollowUpTypes.Retry) - await waitUntilReady() - } else if (failureState(tooManyRequests)) { - // 3 versions of the e2e tests are running at the same time in the ci so we occassionally need to wait before continuing - request && request() - await sleep(tooManyRequestsWaitTime) - } else { - // We've hit guardrails, re-make the request and wait again - request && request() - await waitUntilReady() - } - } - - // The backend never recovered - if (tab.hasButton(FollowUpTypes.SendFeedback)) { - assert.fail('Encountered an error when attempting to call the feature dev backend. Could not continue') - } - } - - before(async function () { - /** - * The tests are getting throttled, only run them on stable for now - * - * TODO: Re-enable for all versions once the backend can handle them - */ - const testVersion = process.env['VSCODE_TEST_VERSION'] - if (testVersion && testVersion !== 'stable') { - this.skip() - } - - await using(registerAuthHook('amazonq-test-account'), async () => { - await loginToIdC() - }) - }) - - beforeEach(() => { - registerAuthHook('amazonq-test-account') - framework = new qTestingFramework('featuredev', true, []) - tab = framework.createTab() - }) - - afterEach(() => { - framework.removeTab(tab.tabID) - framework.dispose() - sinon.restore() - }) - - describe('Quick action availability', () => { - it('Shows /dev when feature dev is enabled', async () => { - const command = tab.findCommand('/dev') - if (!command) { - assert.fail('Could not find command') - } - - if (command.length > 1) { - assert.fail('Found too many commands with the name /dev') - } - }) - - it('Does NOT show /dev when feature dev is NOT enabled', () => { - // The beforeEach registers a framework which accepts requests. If we don't dispose before building a new one we have duplicate messages - framework.dispose() - framework = new qTestingFramework('featuredev', false, []) - const tab = framework.createTab() - const command = tab.findCommand('/dev') - if (command.length > 0) { - assert.fail('Found command when it should not have been found') - } - }) - }) - - describe('/dev entry', () => { - before(async () => { - tab = framework.createTab() - tab.addChatMessage({ command: '/dev' }) // This would create a new tab for feature dev. - tab = framework.getSelectedTab() - }) - - it('should display information card', async () => { - await retryIfRequired( - async () => { - await tab.waitForChatFinishesLoading() - }, - () => { - const lastChatItems = tab.getChatItems().pop() - assert.deepStrictEqual(lastChatItems?.body, informationCard) - } - ) - }) - }) - - describe('/dev {msg} entry', async () => { - beforeEach(async function () { - const isMultiIterationTestsEnabled = process.env['AMAZONQ_FEATUREDEV_ITERATION_TEST'] // Controls whether to enable multiple iteration testing for Amazon Q feature development - if (!isMultiIterationTestsEnabled) { - this.skip() - } else { - this.timeout(900000) // Code Gen with multi-iterations requires longer than default timeout(5 mins). - } - tab = framework.createTab() - tab.addChatMessage({ command: '/dev', prompt }) - tab = framework.getSelectedTab() - await retryIfRequired( - async () => { - await tab.waitForChatFinishesLoading() - }, - () => {} - ) - }) - - afterEach(async function () { - // currentTest.state is undefined if a beforeEach fails - if ( - this.currentTest?.state === undefined || - this.currentTest?.isFailed() || - this.currentTest?.isPending() - ) { - // Since the tests are long running this may help in diagnosing the issue - console.log('Current chat items at failure') - console.log(JSON.stringify(tab.getChatItems(), undefined, 4)) - } - }) - - it('Clicks accept code and click new task', async () => { - await retryIfRequired(async () => { - await Promise.any([ - tab.waitForButtons([FollowUpTypes.InsertCode, FollowUpTypes.ProvideFeedbackAndRegenerateCode]), - tab.waitForButtons([FollowUpTypes.Retry]), - ]) - }) - tab.clickButton(FollowUpTypes.InsertCode) - await tab.waitForButtons([FollowUpTypes.NewTask, FollowUpTypes.CloseSession]) - tab.clickButton(FollowUpTypes.NewTask) - await waitForText('What new task would you like to work on?') - assert.deepStrictEqual(tab.getChatItems().pop()?.body, 'What new task would you like to work on?') - }) - - it('Iterates on codegen', async () => { - await retryIfRequired(async () => { - await Promise.any([ - tab.waitForButtons([FollowUpTypes.InsertCode, FollowUpTypes.ProvideFeedbackAndRegenerateCode]), - tab.waitForButtons([FollowUpTypes.Retry]), - ]) - }) - tab.clickButton(FollowUpTypes.ProvideFeedbackAndRegenerateCode) - await tab.waitForChatFinishesLoading() - await iterate(iteratePrompt) - tab.clickButton(FollowUpTypes.InsertCode) - await tab.waitForButtons([FollowUpTypes.NewTask, FollowUpTypes.CloseSession]) - }) - }) - - describe('file-level accepts', async () => { - beforeEach(async function () { - tab = framework.createTab() - tab.addChatMessage({ command: '/dev', prompt: fileLevelAcceptPrompt }) - tab = framework.getSelectedTab() - await retryIfRequired( - async () => { - await tab.waitForChatFinishesLoading() - }, - () => { - tab.addChatMessage({ prompt }) - } - ) - await retryIfRequired(async () => { - await Promise.any([ - tab.waitForButtons([FollowUpTypes.InsertCode, FollowUpTypes.ProvideFeedbackAndRegenerateCode]), - tab.waitForButtons([FollowUpTypes.Retry]), - ]) - }) - }) - - describe('fileList', async () => { - it('has both accept-change and reject-change action buttons for file', async () => { - const filePath = tab.getFilePaths()[0] - assert.ok(tab.getActionsByFilePath(filePath).length === 2) - assert.ok(tab.hasAction(filePath, 'accept-change')) - assert.ok(tab.hasAction(filePath, 'reject-change')) - }) - - it('has only revert-rejection action button for rejected file', async () => { - const filePath = tab.getFilePaths()[0] - await clickActionButton(filePath, 'reject-change') - - assert.ok(tab.getActionsByFilePath(filePath).length === 1) - assert.ok(tab.hasAction(filePath, 'revert-rejection')) - }) - - it('does not have any of the action buttons for accepted file', async () => { - const filePath = tab.getFilePaths()[0] - await clickActionButton(filePath, 'accept-change') - - assert.ok(tab.getActionsByFilePath(filePath).length === 0) - }) - - it('disables all action buttons when new task is clicked', async () => { - tab.clickButton(FollowUpTypes.InsertCode) - await tab.waitForButtons([FollowUpTypes.NewTask, FollowUpTypes.CloseSession]) - tab.clickButton(FollowUpTypes.NewTask) - await waitForText('What new task would you like to work on?') - - const filePaths = tab.getFilePaths() - for (const filePath of filePaths) { - assert.ok(tab.getActionsByFilePath(filePath).length === 0) - } - }) - - it('disables all action buttons when close session is clicked', async () => { - tab.clickButton(FollowUpTypes.InsertCode) - await tab.waitForButtons([FollowUpTypes.NewTask, FollowUpTypes.CloseSession]) - tab.clickButton(FollowUpTypes.CloseSession) - await waitForText( - "Okay, I've ended this chat session. You can open a new tab to chat or start another workflow." - ) - - const filePaths = tab.getFilePaths() - for (const filePath of filePaths) { - assert.ok(tab.getActionsByFilePath(filePath).length === 0) - } - }) - }) - - describe('accept button', async () => { - describe('button text', async () => { - it('shows "Accept all changes" when no files are accepted or rejected, and "Accept remaining changes" otherwise', async () => { - let insertCodeButton = tab.getFollowUpButton(FollowUpTypes.InsertCode) - assert.ok(insertCodeButton.pillText === 'Accept all changes') - - const filePath = tab.getFilePaths()[0] - await clickActionButton(filePath, 'reject-change') - - insertCodeButton = tab.getFollowUpButton(FollowUpTypes.InsertCode) - assert.ok(insertCodeButton.pillText === 'Accept remaining changes') - - await clickActionButton(filePath, 'revert-rejection') - - insertCodeButton = tab.getFollowUpButton(FollowUpTypes.InsertCode) - assert.ok(insertCodeButton.pillText === 'Accept all changes') - - await clickActionButton(filePath, 'accept-change') - - insertCodeButton = tab.getFollowUpButton(FollowUpTypes.InsertCode) - assert.ok(insertCodeButton.pillText === 'Accept remaining changes') - }) - - it('shows "Continue" when all files are either accepted or rejected, with at least one of them rejected', async () => { - const filePaths = tab.getFilePaths() - for (const filePath of filePaths) { - await clickActionButton(filePath, 'reject-change') - } - - const insertCodeButton = tab.getFollowUpButton(FollowUpTypes.InsertCode) - assert.ok(insertCodeButton.pillText === 'Continue') - }) - }) - - it('disappears and automatically moves on to the next step when all changes are accepted', async () => { - const filePaths = tab.getFilePaths() - for (const filePath of filePaths) { - await clickActionButton(filePath, 'accept-change') - } - await tab.waitForButtons([FollowUpTypes.NewTask, FollowUpTypes.CloseSession]) - - assert.ok(tab.hasButton(FollowUpTypes.InsertCode) === false) - assert.ok(tab.hasButton(FollowUpTypes.ProvideFeedbackAndRegenerateCode) === false) - }) - }) - }) -}) diff --git a/packages/amazonq/test/e2e/amazonq/testGen.test.ts b/packages/amazonq/test/e2e/amazonq/testGen.test.ts deleted file mode 100644 index 21db83fd6e8..00000000000 --- a/packages/amazonq/test/e2e/amazonq/testGen.test.ts +++ /dev/null @@ -1,209 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import vscode from 'vscode' -import { qTestingFramework } from './framework/framework' -import sinon from 'sinon' -import { Messenger } from './framework/messenger' -import { FollowUpTypes } from 'aws-core-vscode/amazonq' -import { registerAuthHook, using, TestFolder, closeAllEditors, getTestWorkspaceFolder } from 'aws-core-vscode/test' -import { loginToIdC } from './utils/setup' -import { waitUntil, workspaceUtils } from 'aws-core-vscode/shared' -import * as path from 'path' - -describe('Amazon Q Test Generation', function () { - let framework: qTestingFramework - let tab: Messenger - - const testFiles = [ - { - language: 'python', - filePath: 'testGenFolder/src/main/math.py', - testFilePath: 'testGenFolder/src/test/test_math.py', - }, - { - language: 'java', - filePath: 'testGenFolder/src/main/Math.java', - testFilePath: 'testGenFolder/src/test/MathTest.java', - }, - ] - - // handles opening the file since /test must be called on an active file - async function setupTestDocument(filePath: string, language: string) { - const document = await waitUntil(async () => { - const doc = await workspaceUtils.openTextDocument(filePath) - return doc - }, {}) - - if (!document) { - assert.fail(`Failed to open ${language} file`) - } - - await waitUntil(async () => { - await vscode.window.showTextDocument(document, { preview: false }) - }, {}) - - const activeEditor = vscode.window.activeTextEditor - if (!activeEditor || activeEditor.document.uri.fsPath !== document.uri.fsPath) { - assert.fail(`Failed to make ${language} file active`) - } - } - - async function waitForChatItems(index: number) { - await tab.waitForEvent(() => tab.getChatItems().length > index, { - waitTimeoutInMs: 5000, - waitIntervalInMs: 1000, - }) - } - - // clears test file to a blank file - // not cleaning up test file may possibly cause bloat in CI since testFixtures does not get reset - async function cleanupTestFile(testFilePath: string) { - const workspaceFolder = getTestWorkspaceFolder() - const absoluteTestFilePath = path.join(workspaceFolder, testFilePath) - const testFileUri = vscode.Uri.file(absoluteTestFilePath) - await vscode.workspace.fs.writeFile(testFileUri, Buffer.from('', 'utf-8')) - } - - before(async function () { - await using(registerAuthHook('amazonq-test-account'), async () => { - await loginToIdC() - }) - }) - - beforeEach(async () => { - registerAuthHook('amazonq-test-account') - framework = new qTestingFramework('testgen', true, []) - tab = framework.createTab() - }) - - afterEach(async () => { - // Close all editors to prevent conflicts with subsequent tests trying to open the same file - await closeAllEditors() - framework.removeTab(tab.tabID) - framework.dispose() - sinon.restore() - }) - - describe('Quick action availability', () => { - it('Shows /test when test generation is enabled', async () => { - const command = tab.findCommand('/test') - if (!command.length) { - assert.fail('Could not find command') - } - if (command.length > 1) { - assert.fail('Found too many commands with the name /test') - } - }) - - it('Does NOT show /test when test generation is NOT enabled', () => { - // The beforeEach registers a framework which accepts requests. If we don't dispose before building a new one we have duplicate messages - framework.dispose() - framework = new qTestingFramework('testgen', false, []) - const tab = framework.createTab() - const command = tab.findCommand('/test') - if (command.length > 0) { - assert.fail('Found command when it should not have been found') - } - }) - }) - - describe('/test entry', () => { - describe('External file out of project', async () => { - let testFolder: TestFolder - let fileName: string - - beforeEach(async () => { - testFolder = await TestFolder.create() - fileName = 'math.py' - const filePath = await testFolder.write(fileName, 'def add(a, b): return a + b') - - const document = await vscode.workspace.openTextDocument(filePath) - await vscode.window.showTextDocument(document, { preview: false }) - }) - - it('/test for external file redirects to chat', async () => { - tab.addChatMessage({ command: '/test' }) - await tab.waitForChatFinishesLoading() - - await waitForChatItems(3) - const externalFileMessage = tab.getChatItems()[3] - - assert.deepStrictEqual(externalFileMessage.type, 'answer') - assert.deepStrictEqual( - externalFileMessage.body, - `I can't generate tests for ${fileName} because the file is outside of workspace scope.
I can still provide examples, instructions and code suggestions.` - ) - }) - }) - - for (const { language, filePath, testFilePath } of testFiles) { - describe(`/test on ${language} file`, () => { - beforeEach(async () => { - await waitUntil(async () => await setupTestDocument(filePath, language), {}) - - tab.addChatMessage({ command: '/test' }) - await tab.waitForChatFinishesLoading() - - await tab.waitForButtons([FollowUpTypes.ViewDiff]) - tab.clickButton(FollowUpTypes.ViewDiff) - await tab.waitForChatFinishesLoading() - }) - - describe('View diff of test file', async () => { - it('Clicks on view diff', async () => { - const chatItems = tab.getChatItems() - const viewDiffMessage = chatItems[5] - - assert.deepStrictEqual(viewDiffMessage.type, 'answer') - const expectedEnding = - 'Please see the unit tests generated below. Click “View diff” to review the changes in the code editor.' - assert.strictEqual( - viewDiffMessage.body?.includes(expectedEnding), - true, - `View diff message does not contain phrase: ${expectedEnding}` - ) - }) - }) - - describe('Accept unit tests', async () => { - afterEach(async () => { - // this e2e test generates unit tests, so we want to clean them up after this test is done - await waitUntil(async () => { - await cleanupTestFile(testFilePath) - }, {}) - }) - - it('Clicks on accept', async () => { - await tab.waitForButtons([FollowUpTypes.AcceptCode, FollowUpTypes.RejectCode]) - tab.clickButton(FollowUpTypes.AcceptCode) - await tab.waitForChatFinishesLoading() - - await waitForChatItems(7) - const acceptedMessage = tab.getChatItems()[7] - - assert.deepStrictEqual(acceptedMessage?.type, 'answer-part') - assert.deepStrictEqual(acceptedMessage?.followUp?.options?.[0].pillText, 'Accepted') - }) - }) - - describe('Reject unit tests', async () => { - it('Clicks on reject', async () => { - await tab.waitForButtons([FollowUpTypes.AcceptCode, FollowUpTypes.RejectCode]) - tab.clickButton(FollowUpTypes.RejectCode) - await tab.waitForChatFinishesLoading() - - await waitForChatItems(7) - const rejectedMessage = tab.getChatItems()[7] - - assert.deepStrictEqual(rejectedMessage?.type, 'answer-part') - assert.deepStrictEqual(rejectedMessage?.followUp?.options?.[0].pillText, 'Rejected') - }) - }) - }) - } - }) -}) diff --git a/packages/amazonq/test/e2e/amazonq/transformByQ.test.ts b/packages/amazonq/test/e2e/amazonq/transformByQ.test.ts index 4493a7c2387..7a9273a1e84 100644 --- a/packages/amazonq/test/e2e/amazonq/transformByQ.test.ts +++ b/packages/amazonq/test/e2e/amazonq/transformByQ.test.ts @@ -129,8 +129,6 @@ describe('Amazon Q Code Transformation', function () { waitIntervalInMs: 1000, }) - // TO-DO: add this back when releasing CSB - /* const customDependencyVersionPrompt = tab.getChatItems().pop() assert.strictEqual( customDependencyVersionPrompt?.body?.includes('You can optionally upload a YAML file'), @@ -139,11 +137,10 @@ describe('Amazon Q Code Transformation', function () { tab.clickCustomFormButton({ id: 'gumbyTransformFormContinue' }) // 2 additional chat messages get sent after Continue button clicked; wait for both of them - await tab.waitForEvent(() => tab.getChatItems().length > 13, { + await tab.waitForEvent(() => tab.getChatItems().length > 10, { waitTimeoutInMs: 5000, waitIntervalInMs: 1000, }) - */ const sourceJdkPathPrompt = tab.getChatItems().pop() assert.strictEqual(sourceJdkPathPrompt?.body?.includes('Enter the path to JDK 8'), true) @@ -151,7 +148,7 @@ describe('Amazon Q Code Transformation', function () { tab.addChatMessage({ prompt: '/dummy/path/to/jdk8' }) // 2 additional chat messages get sent after JDK path submitted; wait for both of them - await tab.waitForEvent(() => tab.getChatItems().length > 10, { + await tab.waitForEvent(() => tab.getChatItems().length > 12, { waitTimeoutInMs: 5000, waitIntervalInMs: 1000, }) @@ -173,7 +170,7 @@ describe('Amazon Q Code Transformation', function () { text: 'View summary', }) - await tab.waitForEvent(() => tab.getChatItems().length > 11, { + await tab.waitForEvent(() => tab.getChatItems().length > 13, { waitTimeoutInMs: 5000, waitIntervalInMs: 1000, }) diff --git a/packages/amazonq/test/e2e/lsp/amazonqLsp.test.ts b/packages/amazonq/test/e2e/lsp/amazonqLsp.test.ts index d3e90ec4e8e..f4a60ff282b 100644 --- a/packages/amazonq/test/e2e/lsp/amazonqLsp.test.ts +++ b/packages/amazonq/test/e2e/lsp/amazonqLsp.test.ts @@ -6,13 +6,13 @@ import { AmazonQLspInstaller } from '../../../src/lsp/lspInstaller' import { defaultAmazonQLspConfig } from '../../../src/lsp/config' import { createLspInstallerTests } from './lspInstallerUtil' -import { LspConfig } from 'aws-core-vscode/amazonq' +import { BaseLspInstaller } from 'aws-core-vscode/shared' describe('AmazonQLSP', () => { createLspInstallerTests({ suiteName: 'AmazonQLSPInstaller', lspConfig: defaultAmazonQLspConfig, - createInstaller: (lspConfig?: LspConfig) => new AmazonQLspInstaller(lspConfig), + createInstaller: (lspConfig?: BaseLspInstaller.LspConfig) => new AmazonQLspInstaller(lspConfig), targetContents: [ { bytes: 0, diff --git a/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts b/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts index c7ca7a4ff9b..d4251959756 100644 --- a/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts +++ b/packages/amazonq/test/e2e/lsp/lspInstallerUtil.ts @@ -18,7 +18,6 @@ import { } from 'aws-core-vscode/shared' import * as semver from 'semver' import { assertTelemetry } from 'aws-core-vscode/test' -import { LspConfig, LspController } from 'aws-core-vscode/amazonq' import { LanguageServerSetup } from 'aws-core-vscode/telemetry' function createVersion(version: string, contents: TargetContent[]) { @@ -44,8 +43,8 @@ export function createLspInstallerTests({ resetEnv, }: { suiteName: string - lspConfig: LspConfig - createInstaller: (lspConfig?: LspConfig) => BaseLspInstaller.BaseLspInstaller + lspConfig: BaseLspInstaller.LspConfig + createInstaller: (lspConfig?: BaseLspInstaller.LspConfig) => BaseLspInstaller.BaseLspInstaller targetContents: TargetContent[] setEnv: (path: string) => void resetEnv: () => void @@ -60,8 +59,6 @@ export function createLspInstallerTests({ installer = createInstaller() tempDir = await makeTemporaryToolkitFolder() sandbox.stub(LanguageServerResolver.prototype, 'defaultDownloadFolder').returns(tempDir) - // Called on extension activation and can contaminate telemetry. - sandbox.stub(LspController.prototype, 'trySetupLsp') }) afterEach(async () => { diff --git a/packages/amazonq/test/e2e/lsp/workspaceContextLsp.test.ts b/packages/amazonq/test/e2e/lsp/workspaceContextLsp.test.ts deleted file mode 100644 index 75d57949c0b..00000000000 --- a/packages/amazonq/test/e2e/lsp/workspaceContextLsp.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as os from 'os' -import { createLspInstallerTests } from './lspInstallerUtil' -import { defaultAmazonQWorkspaceLspConfig, LspClient, LspConfig, WorkspaceLspInstaller } from 'aws-core-vscode/amazonq' -import assert from 'assert' - -describe('AmazonQWorkspaceLSP', () => { - createLspInstallerTests({ - suiteName: 'AmazonQWorkspaceLSPInstaller', - lspConfig: defaultAmazonQWorkspaceLspConfig, - createInstaller: (lspConfig?: LspConfig) => new WorkspaceLspInstaller.WorkspaceLspInstaller(lspConfig), - targetContents: [ - { - bytes: 0, - filename: `qserver-${os.platform()}-${os.arch()}.zip`, - hashes: [], - url: 'http://fakeurl', - }, - ], - setEnv: (path: string) => { - process.env.__AMAZONQWORKSPACELSP_PATH = path - }, - resetEnv: () => { - delete process.env.__AMAZONQWORKSPACELSP_PATH - }, - }) - - it('activates', async () => { - const ok = await LspClient.instance.waitUntilReady() - if (!ok) { - assert.fail('Workspace context language server failed to become ready') - } - const serverUsage = await LspClient.instance.getLspServerUsage() - if (!serverUsage) { - assert.fail('Unable to verify that the workspace context language server has been activated') - } - }) -}) 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 d2182329e45..417c8be1426 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts @@ -3,18 +3,32 @@ * SPDX-License-Identifier: Apache-2.0 */ import sinon from 'sinon' -import { CancellationToken, commands, languages, Position } from 'vscode' +import { + CancellationToken, + commands, + InlineCompletionItem, + languages, + Position, + window, + Range, + InlineCompletionTriggerKind, +} from 'vscode' import assert from 'assert' import { LanguageClient } from 'vscode-languageclient' +import { StringValue } from 'vscode-languageserver-types' import { AmazonQInlineCompletionItemProvider, InlineCompletionManager } from '../../../../../src/app/inline/completion' import { RecommendationService } from '../../../../../src/app/inline/recommendationService' import { SessionManager } from '../../../../../src/app/inline/sessionManager' -import { createMockDocument, createMockTextEditor } from 'aws-core-vscode/test' +import { createMockDocument, createMockTextEditor, getTestWindow, installFakeClock } from 'aws-core-vscode/test' import { + noInlineSuggestionsMsg, ReferenceHoverProvider, - ReferenceInlineProvider, ReferenceLogViewProvider, + vsCodeState, } from 'aws-core-vscode/codewhisperer' +import { LineTracker } from '../../../../../src/app/inline/stateTracker/lineTracker' +import { InlineTutorialAnnotation } from '../../../../../src/app/inline/tutorials/inlineTutorialAnnotation' +import { DocumentEventListener } from '../../../../../src/app/inline/documentEventListener' describe('InlineCompletionManager', () => { let manager: InlineCompletionManager @@ -32,7 +46,7 @@ describe('InlineCompletionManager', () => { let hoverReferenceStub: sinon.SinonStub const mockDocument = createMockDocument() const mockEditor = createMockTextEditor() - const mockPosition = { line: 0, character: 0 } as Position + const mockPosition = new Position(0, 0) const mockContext = { triggerKind: 1, selectedCompletionInfo: undefined } const mockToken = { isCancellationRequested: false } as CancellationToken const fakeReferences = [ @@ -52,6 +66,11 @@ describe('InlineCompletionManager', () => { insertText: 'test', references: fakeReferences, }, + { + itemId: 'test-item2', + insertText: 'import math\ndef two_sum(nums, target):\n', + references: fakeReferences, + }, ] beforeEach(() => { @@ -72,7 +91,10 @@ describe('InlineCompletionManager', () => { sendNotification: sendNotificationStub, } as unknown as LanguageClient - manager = new InlineCompletionManager(languageClient) + const sessionManager = new SessionManager() + const lineTracker = new LineTracker() + const inlineTutorialAnnotation = new InlineTutorialAnnotation(lineTracker, sessionManager) + manager = new InlineCompletionManager(languageClient, sessionManager, lineTracker, inlineTutorialAnnotation) getActiveSessionStub = sandbox.stub(manager['sessionManager'], 'getActiveSession') getActiveRecommendationStub = sandbox.stub(manager['sessionManager'], 'getActiveRecommendation') getReferenceStub = sandbox.stub(ReferenceLogViewProvider, 'getReferenceLog') @@ -213,46 +235,6 @@ describe('InlineCompletionManager', () => { assert(registerProviderStub.calledTwice) // Once in constructor, once after rejection }) }) - - describe('previous command', () => { - it('should register and handle previous command correctly', async () => { - const prevCommandCall = registerCommandStub - .getCalls() - .find((call) => call.args[0] === 'editor.action.inlineSuggest.showPrevious') - - assert(prevCommandCall, 'Previous command should be registered') - - if (prevCommandCall) { - const handler = prevCommandCall.args[1] - await handler() - - assert(executeCommandStub.calledWith('editor.action.inlineSuggest.hide')) - assert(disposableStub.calledOnce) - assert(registerProviderStub.calledTwice) - assert(executeCommandStub.calledWith('editor.action.inlineSuggest.trigger')) - } - }) - }) - - describe('next command', () => { - it('should register and handle next command correctly', async () => { - const nextCommandCall = registerCommandStub - .getCalls() - .find((call) => call.args[0] === 'editor.action.inlineSuggest.showNext') - - assert(nextCommandCall, 'Next command should be registered') - - if (nextCommandCall) { - const handler = nextCommandCall.args[1] - await handler() - - assert(executeCommandStub.calledWith('editor.action.inlineSuggest.hide')) - assert(disposableStub.calledOnce) - assert(registerProviderStub.calledTwice) - assert(executeCommandStub.calledWith('editor.action.inlineSuggest.trigger')) - } - }) - }) }) describe('AmazonQInlineCompletionItemProvider', () => { @@ -261,15 +243,20 @@ describe('InlineCompletionManager', () => { let provider: AmazonQInlineCompletionItemProvider let getAllRecommendationsStub: sinon.SinonStub let recommendationService: RecommendationService - let setInlineReferenceStub: sinon.SinonStub + let inlineTutorialAnnotation: InlineTutorialAnnotation + let documentEventListener: DocumentEventListener beforeEach(() => { + const lineTracker = new LineTracker() + inlineTutorialAnnotation = new InlineTutorialAnnotation(lineTracker, mockSessionManager) recommendationService = new RecommendationService(mockSessionManager) - setInlineReferenceStub = sandbox.stub(ReferenceInlineProvider.instance, 'setInlineReference') - + documentEventListener = new DocumentEventListener() + vsCodeState.isRecommendationsActive = false mockSessionManager = { getActiveSession: getActiveSessionStub, getActiveRecommendation: getActiveRecommendationStub, + clear: () => {}, + updateCodeReferenceAndImports: () => {}, } as unknown as SessionManager getActiveSessionStub.returns({ @@ -281,12 +268,15 @@ describe('InlineCompletionManager', () => { getActiveRecommendationStub.returns(mockSuggestions) getAllRecommendationsStub = sandbox.stub(recommendationService, 'getAllRecommendations') getAllRecommendationsStub.resolves() + sandbox.stub(window, 'activeTextEditor').value(createMockTextEditor()) }), - it('should call recommendation service to get new suggestions for new sessions', async () => { + it('should call recommendation service to get new suggestions(matching typeahead) for new sessions', async () => { provider = new AmazonQInlineCompletionItemProvider( languageClient, recommendationService, - mockSessionManager + mockSessionManager, + inlineTutorialAnnotation, + documentEventListener ) const items = await provider.provideInlineCompletionItems( mockDocument, @@ -295,41 +285,140 @@ describe('InlineCompletionManager', () => { mockToken ) assert(getAllRecommendationsStub.calledOnce) - assert.deepStrictEqual(items, mockSuggestions) + assert.deepStrictEqual(items, [mockSuggestions[1]]) }), - it('should not call recommendation service for existing sessions', async () => { + it('should handle reference if there is any', async () => { provider = new AmazonQInlineCompletionItemProvider( languageClient, recommendationService, mockSessionManager, - false + inlineTutorialAnnotation, + documentEventListener ) - const items = await provider.provideInlineCompletionItems( + await provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken) + }), + it('should add a range to the completion item when missing', async function () { + provider = new AmazonQInlineCompletionItemProvider( + languageClient, + recommendationService, + mockSessionManager, + inlineTutorialAnnotation, + documentEventListener + ) + getActiveRecommendationStub.returns([ + { + insertText: 'testText', + itemId: 'itemId', + }, + { + insertText: 'testText2', + itemId: 'itemId2', + range: undefined, + }, + ]) + const cursorPosition = new Position(5, 6) + const result = await provider.provideInlineCompletionItems( + mockDocument, + cursorPosition, + mockContext, + mockToken + ) + + for (const item of result) { + assert.deepStrictEqual(item.range, new Range(cursorPosition, cursorPosition)) + } + }), + it('should handle StringValue instead of strings', async function () { + provider = new AmazonQInlineCompletionItemProvider( + languageClient, + recommendationService, + mockSessionManager, + inlineTutorialAnnotation, + documentEventListener + ) + const expectedText = `${mockSuggestions[1].insertText}this is my text` + getActiveRecommendationStub.returns([ + { + insertText: { + kind: 'snippet', + value: `${mockSuggestions[1].insertText}this is my text`, + } satisfies StringValue, + itemId: 'itemId', + }, + ]) + const result = await provider.provideInlineCompletionItems( mockDocument, mockPosition, mockContext, mockToken ) - assert(getAllRecommendationsStub.notCalled) - assert.deepStrictEqual(items, mockSuggestions) + + assert.strictEqual(result[0].insertText, expectedText) }), - it('should handle reference if there is any', async () => { + it('shows message to user when manual invoke fails to produce results', async function () { provider = new AmazonQInlineCompletionItemProvider( languageClient, recommendationService, mockSessionManager, - false + inlineTutorialAnnotation, + documentEventListener ) - await provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken) - assert(setInlineReferenceStub.calledOnce) - assert( - setInlineReferenceStub.calledWithExactly( - mockPosition.line, - mockSuggestions[0].insertText, - fakeReferences - ) + getActiveRecommendationStub.returns([]) + const messageShown = new Promise((resolve) => + getTestWindow().onDidShowMessage((e) => { + assert.strictEqual(e.message, noInlineSuggestionsMsg) + resolve(true) + }) + ) + await provider.provideInlineCompletionItems( + mockDocument, + mockPosition, + { triggerKind: InlineCompletionTriggerKind.Invoke, selectedCompletionInfo: undefined }, + mockToken + ) + await messageShown + }) + describe.skip('debounce behavior', function () { + let clock: ReturnType + + beforeEach(function () { + clock = installFakeClock() + }) + + after(function () { + clock.uninstall() + }) + + it.skip('should only trigger once on rapid events', async () => { + provider = new AmazonQInlineCompletionItemProvider( + languageClient, + recommendationService, + mockSessionManager, + inlineTutorialAnnotation, + documentEventListener ) + const p1 = provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken) + const p2 = provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken) + const p3 = provider.provideInlineCompletionItems( + mockDocument, + new Position(1, 26), + mockContext, + mockToken + ) + + await clock.tickAsync(1000) + + // All promises should be the same object when debounced properly. + assert.strictEqual(p1, p2) + assert.strictEqual(p1, p3) + await p1 + await p2 + const r3 = await p3 + + // calls the function with the latest provided args. + assert.deepStrictEqual((r3 as InlineCompletionItem[])[0].range?.end, new Position(1, 26)) }) + }) }) }) }) diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/inlineTracker.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/inlineTracker.test.ts new file mode 100644 index 00000000000..6b9490c72a5 --- /dev/null +++ b/packages/amazonq/test/unit/amazonq/apps/inline/inlineTracker.test.ts @@ -0,0 +1,299 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { LineSelection, LineTracker, AuthUtil } from 'aws-core-vscode/codewhisperer' +import sinon from 'sinon' +import { Disposable, TextEditor, Position, Range, Selection } from 'vscode' +import { toTextEditor } from 'aws-core-vscode/test' +import assert from 'assert' +import { waitUntil } from 'aws-core-vscode/shared' + +describe('LineTracker class', function () { + let sut: LineTracker + let disposable: Disposable + let editor: TextEditor + let sandbox: sinon.SinonSandbox + let counts = { + editor: 0, + selection: 0, + content: 0, + } + + beforeEach(async function () { + sut = new LineTracker() + sandbox = sinon.createSandbox() + counts = { + editor: 0, + selection: 0, + content: 0, + } + disposable = sut.onDidChangeActiveLines((e) => { + if (e.reason === 'content') { + counts.content++ + } else if (e.reason === 'selection') { + counts.selection++ + } else if (e.reason === 'editor') { + counts.editor++ + } + }) + + sandbox.stub(AuthUtil.instance, 'isConnected').returns(true) + sandbox.stub(AuthUtil.instance, 'isConnectionExpired').returns(false) + }) + + afterEach(function () { + disposable.dispose() + sut.dispose() + sandbox.restore() + }) + + function assertEmptyCounts() { + assert.deepStrictEqual(counts, { + editor: 0, + selection: 0, + content: 0, + }) + } + + it('ready will emit onReady event', async function () { + let messageReceived = 0 + disposable = sut.onReady((_) => { + messageReceived++ + }) + + assert.strictEqual(sut.isReady, false) + sut.ready() + + await waitUntil( + async () => { + if (messageReceived !== 0) { + return + } + }, + { interval: 1000 } + ) + + assert.strictEqual(sut.isReady, true) + assert.strictEqual(messageReceived, 1) + }) + + describe('includes', function () { + // util function to help set up LineTracker.selections + async function setEditorSelection(selections: LineSelection[]): Promise { + const editor = await toTextEditor('\n\n\n\n\n\n\n\n\n\n', 'foo.py', undefined, { + preview: false, + }) + + const vscodeSelections = selections.map((s) => { + return new Selection(new Position(s.anchor, 0), new Position(s.active, 0)) + }) + + await sut.onTextEditorSelectionChanged({ + textEditor: editor, + selections: vscodeSelections, + kind: undefined, + }) + + assert.deepStrictEqual(sut.selections, selections) + return editor + } + + it('exact match when array of selections are provided', async function () { + const selections = [ + { + anchor: 1, + active: 1, + }, + { + anchor: 3, + active: 3, + }, + ] + + editor = await setEditorSelection(selections) + assert.deepStrictEqual(sut.selections, selections) + + let actual = sut.includes([ + { active: 1, anchor: 1 }, + { active: 3, anchor: 3 }, + ]) + assert.strictEqual(actual, true) + + actual = sut.includes([ + { active: 2, anchor: 2 }, + { active: 4, anchor: 4 }, + ]) + assert.strictEqual(actual, false) + + // both active && anchor have to be the same + actual = sut.includes([ + { active: 1, anchor: 0 }, + { active: 3, anchor: 0 }, + ]) + assert.strictEqual(actual, false) + + // different length would simply return false + actual = sut.includes([ + { active: 1, anchor: 1 }, + { active: 3, anchor: 3 }, + { active: 5, anchor: 5 }, + ]) + assert.strictEqual(actual, false) + }) + + it('match active line if line number and activeOnly option are provided', async function () { + const selections = [ + { + anchor: 1, + active: 1, + }, + { + anchor: 3, + active: 3, + }, + ] + + editor = await setEditorSelection(selections) + assert.deepStrictEqual(sut.selections, selections) + + let actual = sut.includes(1, { activeOnly: true }) + assert.strictEqual(actual, true) + + actual = sut.includes(2, { activeOnly: true }) + assert.strictEqual(actual, false) + }) + + it('range match if line number and activeOnly is set to false', async function () { + const selections = [ + { + anchor: 0, + active: 2, + }, + { + anchor: 4, + active: 6, + }, + ] + + editor = await setEditorSelection(selections) + assert.deepStrictEqual(sut.selections, selections) + + for (const line of [0, 1, 2]) { + const actual = sut.includes(line, { activeOnly: false }) + assert.strictEqual(actual, true) + } + + for (const line of [4, 5, 6]) { + const actual = sut.includes(line, { activeOnly: false }) + assert.strictEqual(actual, true) + } + + let actual = sut.includes(3, { activeOnly: false }) + assert.strictEqual(actual, false) + + actual = sut.includes(7, { activeOnly: false }) + assert.strictEqual(actual, false) + }) + }) + + describe('onContentChanged', function () { + it('should fire lineChangedEvent and set current line selection', async function () { + editor = await toTextEditor('\n\n\n\n\n', 'foo.py', undefined, { preview: false }) + editor.selection = new Selection(new Position(5, 0), new Position(5, 0)) + assertEmptyCounts() + + sut.onContentChanged({ + document: editor.document, + contentChanges: [{ text: 'a', range: new Range(0, 0, 0, 0), rangeOffset: 0, rangeLength: 0 }], + reason: undefined, + }) + + assert.deepStrictEqual(counts, { ...counts, content: 1 }) + assert.deepStrictEqual(sut.selections, [ + { + anchor: 5, + active: 5, + }, + ]) + }) + }) + + describe('onTextEditorSelectionChanged', function () { + it('should fire lineChangedEvent if selection changes and set current line selection', async function () { + editor = await toTextEditor('\n\n\n\n\n', 'foo.py', undefined, { preview: false }) + editor.selection = new Selection(new Position(3, 0), new Position(3, 0)) + assertEmptyCounts() + + await sut.onTextEditorSelectionChanged({ + textEditor: editor, + selections: [new Selection(new Position(3, 0), new Position(3, 0))], + kind: undefined, + }) + + assert.deepStrictEqual(counts, { ...counts, selection: 1 }) + assert.deepStrictEqual(sut.selections, [ + { + anchor: 3, + active: 3, + }, + ]) + + // if selection is included in the existing selections, won't emit an event + await sut.onTextEditorSelectionChanged({ + textEditor: editor, + selections: [new Selection(new Position(3, 0), new Position(3, 0))], + kind: undefined, + }) + + assert.deepStrictEqual(counts, { ...counts, selection: 1 }) + assert.deepStrictEqual(sut.selections, [ + { + anchor: 3, + active: 3, + }, + ]) + }) + + it('should not fire lineChangedEvent if uri scheme is debug || output', async function () { + // if the editor is not a text editor, won't emit an event and selection will be set to undefined + async function assertLineChanged(schema: string) { + const anotherEditor = await toTextEditor('', 'bar.log', undefined, { preview: false }) + const uri = anotherEditor.document.uri + sandbox.stub(uri, 'scheme').get(() => schema) + + await sut.onTextEditorSelectionChanged({ + textEditor: anotherEditor, + selections: [new Selection(new Position(3, 0), new Position(3, 0))], + kind: undefined, + }) + + assert.deepStrictEqual(counts, { ...counts }) + } + + await assertLineChanged('debug') + await assertLineChanged('output') + }) + }) + + describe('onActiveTextEditorChanged', function () { + it('shoudl fire lineChangedEvent', async function () { + const selections: Selection[] = [new Selection(0, 0, 1, 1)] + + editor = { selections: selections } as any + + assertEmptyCounts() + + await sut.onActiveTextEditorChanged(editor) + + assert.deepStrictEqual(counts, { ...counts, editor: 1 }) + assert.deepStrictEqual(sut.selections, [ + { + anchor: 0, + active: 1, + }, + ]) + }) + }) +}) 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 b3628e22c35..a051ef94abb 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts @@ -5,20 +5,38 @@ import sinon from 'sinon' import { LanguageClient } from 'vscode-languageclient' -import { Position, CancellationToken, InlineCompletionItem } from 'vscode' +import { Position, CancellationToken, InlineCompletionItem, InlineCompletionTriggerKind } from 'vscode' import assert from 'assert' import { RecommendationService } from '../../../../../src/app/inline/recommendationService' import { SessionManager } from '../../../../../src/app/inline/sessionManager' import { createMockDocument } from 'aws-core-vscode/test' +// Import CursorUpdateManager directly instead of the interface +import { CursorUpdateManager } from '../../../../../src/app/inline/cursorUpdateManager' +import { CodeWhispererStatusBarManager } from 'aws-core-vscode/codewhisperer' +import { globals } from 'aws-core-vscode/shared' +import { DocumentEventListener } from '../../../../../src/app/inline/documentEventListener' +import { EditSuggestionState } from '../../../../../src/app/inline/editSuggestionState' + +const completionApi = 'aws/textDocument/inlineCompletionWithReferences' +const editApi = 'aws/textDocument/editCompletion' describe('RecommendationService', () => { let languageClient: LanguageClient let sendRequestStub: sinon.SinonStub let sandbox: sinon.SinonSandbox + let sessionManager: SessionManager + let service: RecommendationService + let cursorUpdateManager: CursorUpdateManager + let statusBarStub: any + let clockStub: sinon.SinonFakeTimers const mockDocument = createMockDocument() const mockPosition = { line: 0, character: 0 } as Position - const mockContext = { triggerKind: 1, selectedCompletionInfo: undefined } + const mockContext = { triggerKind: InlineCompletionTriggerKind.Automatic, selectedCompletionInfo: undefined } const mockToken = { isCancellationRequested: false } as CancellationToken + const mockDocumentEventListener = { + isLastEventDeletion: (filepath: string) => false, + getLastDocumentChangeEvent: (filepath: string) => undefined, + } as DocumentEventListener const mockInlineCompletionItemOne = { insertText: 'ItemOne', } as InlineCompletionItem @@ -27,17 +45,59 @@ describe('RecommendationService', () => { insertText: 'ItemTwo', } as InlineCompletionItem const mockPartialResultToken = 'some-random-token' - const sessionManager = new SessionManager() - const service = new RecommendationService(sessionManager) - beforeEach(() => { + beforeEach(async () => { sandbox = sinon.createSandbox() + // Create a fake clock for testing time-based functionality + clockStub = sandbox.useFakeTimers({ + now: 1000, + shouldAdvanceTime: true, + }) + + // Stub globals.clock + sandbox.stub(globals, 'clock').value({ + Date: { + now: () => clockStub.now, + }, + setTimeout: clockStub.setTimeout.bind(clockStub), + clearTimeout: clockStub.clearTimeout.bind(clockStub), + setInterval: clockStub.setInterval.bind(clockStub), + clearInterval: clockStub.clearInterval.bind(clockStub), + }) + sendRequestStub = sandbox.stub() languageClient = { sendRequest: sendRequestStub, + warn: sandbox.stub(), } as unknown as LanguageClient + + sessionManager = new SessionManager() + + // Create cursor update manager mock + cursorUpdateManager = { + recordCompletionRequest: sandbox.stub(), + logger: { debug: sandbox.stub(), warn: sandbox.stub(), error: sandbox.stub() }, + updateIntervalMs: 250, + isActive: false, + lastRequestTime: 0, + dispose: sandbox.stub(), + start: sandbox.stub(), + stop: sandbox.stub(), + updatePosition: sandbox.stub(), + } as unknown as CursorUpdateManager + + // Create status bar stub + statusBarStub = { + setLoading: sandbox.stub().resolves(), + refreshStatusBar: sandbox.stub().resolves(), + } + + sandbox.stub(CodeWhispererStatusBarManager, 'instance').get(() => statusBarStub) + + // Create the service without cursor update recorder initially + service = new RecommendationService(sessionManager) }) afterEach(() => { @@ -45,8 +105,33 @@ describe('RecommendationService', () => { sessionManager.clear() }) + describe('constructor', () => { + it('should initialize with optional cursorUpdateRecorder', () => { + const serviceWithRecorder = new RecommendationService(sessionManager, cursorUpdateManager) + + // Verify the service was created with the recorder + assert.strictEqual(serviceWithRecorder['cursorUpdateRecorder'], cursorUpdateManager) + }) + }) + + describe('setCursorUpdateRecorder', () => { + it('should set the cursor update recorder', () => { + // Initially the recorder should be undefined + assert.strictEqual(service['cursorUpdateRecorder'], undefined) + + // Set the recorder + service.setCursorUpdateRecorder(cursorUpdateManager) + + // Verify it was set correctly + assert.strictEqual(service['cursorUpdateRecorder'], cursorUpdateManager) + }) + }) + describe('getAllRecommendations', () => { it('should handle single request with no partial result token', async () => { + // Mock EditSuggestionState to return false (no edit suggestion active) + sandbox.stub(EditSuggestionState, 'isEditSuggestionActive').returns(false) + const mockFirstResult = { sessionId: 'test-session', items: [mockInlineCompletionItemOne], @@ -55,17 +140,33 @@ describe('RecommendationService', () => { sendRequestStub.resolves(mockFirstResult) - await service.getAllRecommendations(languageClient, mockDocument, mockPosition, mockContext, mockToken) + await service.getAllRecommendations( + languageClient, + mockDocument, + mockPosition, + mockContext, + mockToken, + true, + mockDocumentEventListener + ) // Verify sendRequest was called with correct parameters - assert(sendRequestStub.calledOnce) - const requestArgs = sendRequestStub.firstCall.args[1] + const cs = sendRequestStub.getCalls() + const completionCalls = cs.filter((c) => c.firstArg === completionApi) + const editCalls = cs.filter((c) => c.firstArg === editApi) + assert.strictEqual(cs.length, 2) + assert.strictEqual(completionCalls.length, 1) + assert.strictEqual(editCalls.length, 1) + + const requestArgs = completionCalls[0].args[1] assert.deepStrictEqual(requestArgs, { textDocument: { uri: 'file:///test.py', }, position: mockPosition, context: mockContext, + documentChangeParams: undefined, + openTabFilepaths: [], }) // Verify session management @@ -74,6 +175,9 @@ describe('RecommendationService', () => { }) it('should handle multiple request with partial result token', async () => { + // Mock EditSuggestionState to return false (no edit suggestion active) + sandbox.stub(EditSuggestionState, 'isEditSuggestionActive').returns(false) + const mockFirstResult = { sessionId: 'test-session', items: [mockInlineCompletionItemOne], @@ -89,31 +193,208 @@ describe('RecommendationService', () => { sendRequestStub.onFirstCall().resolves(mockFirstResult) sendRequestStub.onSecondCall().resolves(mockSecondResult) - await service.getAllRecommendations(languageClient, mockDocument, mockPosition, mockContext, mockToken) + await service.getAllRecommendations( + languageClient, + mockDocument, + mockPosition, + mockContext, + mockToken, + true, + mockDocumentEventListener + ) // Verify sendRequest was called with correct parameters - assert(sendRequestStub.calledTwice) - const firstRequestArgs = sendRequestStub.firstCall.args[1] + const cs = sendRequestStub.getCalls() + const completionCalls = cs.filter((c) => c.firstArg === completionApi) + const editCalls = cs.filter((c) => c.firstArg === editApi) + assert.strictEqual(cs.length, 3) + assert.strictEqual(completionCalls.length, 2) + assert.strictEqual(editCalls.length, 1) + + const firstRequestArgs = completionCalls[0].args[1] const expectedRequestArgs = { textDocument: { uri: 'file:///test.py', }, position: mockPosition, context: mockContext, + documentChangeParams: undefined, + openTabFilepaths: [], } - const secondRequestArgs = sendRequestStub.secondCall.args[1] + const secondRequestArgs = completionCalls[1].args[1] assert.deepStrictEqual(firstRequestArgs, expectedRequestArgs) assert.deepStrictEqual(secondRequestArgs, { ...expectedRequestArgs, partialResultToken: mockPartialResultToken, }) + }) - // Verify session management - const items = sessionManager.getActiveRecommendation() - assert.deepStrictEqual(items, [mockInlineCompletionItemOne, { insertText: '1' } as InlineCompletionItem]) - sessionManager.incrementActiveIndex() - const items2 = sessionManager.getActiveRecommendation() - assert.deepStrictEqual(items2, [mockInlineCompletionItemTwo, { insertText: '1' } as InlineCompletionItem]) + it('should record completion request when cursorUpdateRecorder is set', async () => { + // Set the cursor update recorder + service.setCursorUpdateRecorder(cursorUpdateManager) + + const mockFirstResult = { + sessionId: 'test-session', + items: [mockInlineCompletionItemOne], + partialResultToken: undefined, + } + + sendRequestStub.resolves(mockFirstResult) + + await service.getAllRecommendations( + languageClient, + mockDocument, + mockPosition, + mockContext, + mockToken, + true, + mockDocumentEventListener + ) + + // Verify recordCompletionRequest was called + // eslint-disable-next-line @typescript-eslint/unbound-method + sinon.assert.calledOnce(cursorUpdateManager.recordCompletionRequest as sinon.SinonStub) + }) + + it('should not show UI indicators when showUi option is false', async () => { + // Call with showUi: false option + await service.getAllRecommendations( + languageClient, + mockDocument, + mockPosition, + mockContext, + mockToken, + true, + mockDocumentEventListener, + { + showUi: false, + emitTelemetry: true, + } + ) + + // Verify UI methods were not called + sinon.assert.notCalled(statusBarStub.setLoading) + sinon.assert.notCalled(statusBarStub.refreshStatusBar) + }) + + it('should show UI indicators when showUi option is true (default)', async () => { + // Call with default options (showUi: true) + await service.getAllRecommendations( + languageClient, + mockDocument, + mockPosition, + mockContext, + mockToken, + true, + mockDocumentEventListener + ) + + // Verify UI methods were called + sinon.assert.calledOnce(statusBarStub.setLoading) + sinon.assert.calledOnce(statusBarStub.refreshStatusBar) + }) + + it('should handle errors gracefully', async () => { + // Set the cursor update recorder + service.setCursorUpdateRecorder(cursorUpdateManager) + + // Make the request throw an error + const testError = new Error('Test error') + sendRequestStub.rejects(testError) + + // Set up UI options + const options = { showUi: true } + + // Temporarily replace console.error with a no-op function to prevent test failure + const originalConsoleError = console.error + console.error = () => {} + + try { + // Call the method and expect it to handle the error + const result = await service.getAllRecommendations( + languageClient, + mockDocument, + mockPosition, + mockContext, + mockToken, + true, + mockDocumentEventListener, + options + ) + + // Assert that error handling was done correctly + assert.deepStrictEqual(result, []) + + // Verify the UI indicators were hidden even when an error occurs + sinon.assert.calledOnce(statusBarStub.refreshStatusBar) + } finally { + // Restore the original console.error function + console.error = originalConsoleError + } + }) + + it('should not make completion request when edit suggestion is active', async () => { + // Mock EditSuggestionState to return true (edit suggestion is active) + sandbox.stub(EditSuggestionState, 'isEditSuggestionActive').returns(true) + + const mockResult = { + sessionId: 'test-session', + items: [mockInlineCompletionItemOne], + partialResultToken: undefined, + } + + sendRequestStub.resolves(mockResult) + + await service.getAllRecommendations( + languageClient, + mockDocument, + mockPosition, + mockContext, + mockToken, + true, + mockDocumentEventListener + ) + + // Verify sendRequest was called only for edit API, not completion API + const cs = sendRequestStub.getCalls() + const completionCalls = cs.filter((c) => c.firstArg === completionApi) + const editCalls = cs.filter((c) => c.firstArg === editApi) + + assert.strictEqual(cs.length, 1) // Only edit call + assert.strictEqual(completionCalls.length, 0) // No completion calls + assert.strictEqual(editCalls.length, 1) // One edit call + }) + + it('should make completion request when edit suggestion is not active', async () => { + // Mock EditSuggestionState to return false (no edit suggestion active) + sandbox.stub(EditSuggestionState, 'isEditSuggestionActive').returns(false) + + const mockResult = { + sessionId: 'test-session', + items: [mockInlineCompletionItemOne], + partialResultToken: undefined, + } + + sendRequestStub.resolves(mockResult) + + await service.getAllRecommendations( + languageClient, + mockDocument, + mockPosition, + mockContext, + mockToken, + true, + mockDocumentEventListener + ) + + // Verify sendRequest was called for both APIs + const cs = sendRequestStub.getCalls() + const completionCalls = cs.filter((c) => c.firstArg === completionApi) + const editCalls = cs.filter((c) => c.firstArg === editApi) + + assert.strictEqual(cs.length, 2) // Both calls + assert.strictEqual(completionCalls.length, 1) // One completion call + assert.strictEqual(editCalls.length, 1) // One edit call }) }) }) diff --git a/packages/amazonq/test/unit/amazonq/lsp/client.test.ts b/packages/amazonq/test/unit/amazonq/lsp/client.test.ts new file mode 100644 index 00000000000..7c99c47e0ea --- /dev/null +++ b/packages/amazonq/test/unit/amazonq/lsp/client.test.ts @@ -0,0 +1,268 @@ +/*! + * 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 { LanguageClient } from 'vscode-languageclient' +import { AuthUtil } from 'aws-core-vscode/codewhisperer' +import { AmazonQLspAuth } from '../../../../src/lsp/auth' + +// These tests verify the behavior of the authentication functions +// Since the actual functions are module-level and use real dependencies, +// we test the expected behavior through mock implementations + +describe('Language Server Client Authentication', function () { + let sandbox: sinon.SinonSandbox + let mockClient: any + let mockAuth: any + let authUtilStub: sinon.SinonStub + let loggerStub: any + let getLoggerStub: sinon.SinonStub + let pushConfigUpdateStub: sinon.SinonStub + + beforeEach(() => { + sandbox = sinon.createSandbox() + + // Mock LanguageClient + mockClient = { + sendRequest: sandbox.stub().resolves(), + sendNotification: sandbox.stub(), + onDidChangeState: sandbox.stub(), + } + + // Mock AmazonQLspAuth + mockAuth = { + refreshConnection: sandbox.stub().resolves(), + } + + // Mock AuthUtil + authUtilStub = sandbox.stub(AuthUtil, 'instance').get(() => ({ + isConnectionValid: sandbox.stub().returns(true), + regionProfileManager: { + activeRegionProfile: { arn: 'test-profile-arn' }, + }, + auth: { + getConnectionState: sandbox.stub().returns('valid'), + activeConnection: { id: 'test-connection' }, + }, + })) + + // Create logger stub + loggerStub = { + info: sandbox.stub(), + debug: sandbox.stub(), + warn: sandbox.stub(), + error: sandbox.stub(), + } + + // Clear all relevant module caches + const sharedModuleId = require.resolve('aws-core-vscode/shared') + const configModuleId = require.resolve('../../../../src/lsp/config') + delete require.cache[sharedModuleId] + delete require.cache[configModuleId] + + // jscpd:ignore-start + // Create getLogger stub + getLoggerStub = sandbox.stub().returns(loggerStub) + + // Create a mock shared module with stubbed getLogger + const mockSharedModule = { + getLogger: getLoggerStub, + } + + // Override the require cache with our mock + require.cache[sharedModuleId] = { + id: sharedModuleId, + filename: sharedModuleId, + loaded: true, + parent: undefined, + children: [], + exports: mockSharedModule, + paths: [], + } as any + // jscpd:ignore-end + + // Mock pushConfigUpdate + pushConfigUpdateStub = sandbox.stub().resolves() + const mockConfigModule = { + pushConfigUpdate: pushConfigUpdateStub, + } + + require.cache[configModuleId] = { + id: configModuleId, + filename: configModuleId, + loaded: true, + parent: undefined, + children: [], + exports: mockConfigModule, + paths: [], + } as any + }) + + afterEach(() => { + sandbox.restore() + }) + + 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 { getLogger } = require('aws-core-vscode/shared') + const { pushConfigUpdate } = require('../../../../src/lsp/config') + const logger = getLogger('amazonqLsp') + + if (AuthUtil.instance.isConnectionValid()) { + logger.info(`[${context}] Initializing language server configuration`) + + // Send profile configuration + logger.debug(`[${context}] Sending profile configuration to language server`) + await pushConfigUpdate(client, { + type: 'profile', + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, + }) + logger.debug(`[${context}] Profile configuration sent successfully`) + + // Send customization configuration + logger.debug(`[${context}] Sending customization configuration to language server`) + await pushConfigUpdate(client, { + type: 'customization', + customization: 'test-customization', + }) + logger.debug(`[${context}] Customization configuration sent successfully`) + + logger.info(`[${context}] Language server configuration completed successfully`) + } else { + logger.warn(`[${context}] Connection invalid, skipping configuration`) + } + } + + await mockInitializeFunction(mockClient as any, 'startup') + + // Verify logging + assert(loggerStub.info.calledWith('[startup] Initializing language server configuration')) + assert(loggerStub.debug.calledWith('[startup] Sending profile configuration to language server')) + assert(loggerStub.debug.calledWith('[startup] Profile configuration sent successfully')) + assert(loggerStub.debug.calledWith('[startup] Sending customization configuration to language server')) + assert(loggerStub.debug.calledWith('[startup] Customization configuration sent successfully')) + assert(loggerStub.info.calledWith('[startup] Language server configuration completed successfully')) + + // Verify pushConfigUpdate was called twice + assert.strictEqual(pushConfigUpdateStub.callCount, 2) + + // Verify profile configuration + assert( + pushConfigUpdateStub.calledWith(mockClient, { + type: 'profile', + profileArn: 'test-profile-arn', + }) + ) + + // Verify customization configuration + assert( + pushConfigUpdateStub.calledWith(mockClient, { + type: 'customization', + customization: 'test-customization', + }) + ) + }) + + it('should log warning when connection is invalid', async function () { + // Mock invalid connection + authUtilStub.get(() => ({ + isConnectionValid: sandbox.stub().returns(false), + auth: { + getConnectionState: sandbox.stub().returns('invalid'), + activeConnection: { id: 'test-connection' }, + }, + })) + + const mockInitializeFunction = async (client: LanguageClient, context: string) => { + const { getLogger } = require('aws-core-vscode/shared') + const logger = getLogger('amazonqLsp') + + // jscpd:ignore-start + if (AuthUtil.instance.isConnectionValid()) { + // Should not reach here + } else { + logger.warn( + `[${context}] Connection invalid, skipping language server configuration - this will cause authentication failures` + ) + const activeConnection = AuthUtil.instance.auth.activeConnection + const connectionState = activeConnection + ? AuthUtil.instance.auth.getConnectionState(activeConnection) + : 'no-connection' + logger.warn(`[${context}] Connection state: ${connectionState}`) + // jscpd:ignore-end + } + } + + await mockInitializeFunction(mockClient as any, 'crash-recovery') + + // Verify warning logs + assert( + loggerStub.warn.calledWith( + '[crash-recovery] Connection invalid, skipping language server configuration - this will cause authentication failures' + ) + ) + assert(loggerStub.warn.calledWith('[crash-recovery] Connection state: invalid')) + + // Verify pushConfigUpdate was not called + assert.strictEqual(pushConfigUpdateStub.callCount, 0) + }) + }) + + describe('crash recovery handler behavior', function () { + it('should reinitialize authentication after crash', async function () { + const mockCrashHandler = async (client: LanguageClient, auth: AmazonQLspAuth) => { + const { getLogger } = require('aws-core-vscode/shared') + const { pushConfigUpdate } = require('../../../../src/lsp/config') + const logger = getLogger('amazonqLsp') + + logger.info('[crash-recovery] Language server crash detected, reinitializing authentication') + + try { + logger.debug('[crash-recovery] Refreshing connection and sending bearer token') + await auth.refreshConnection(true) + logger.debug('[crash-recovery] Bearer token sent successfully') + + // Mock the configuration initialization + if (AuthUtil.instance.isConnectionValid()) { + await pushConfigUpdate(client, { + type: 'profile', + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, + }) + } + + logger.info('[crash-recovery] Authentication reinitialized successfully') + } catch (error) { + logger.error(`[crash-recovery] Failed to reinitialize after crash: ${error}`) + } + } + + await mockCrashHandler(mockClient as any, mockAuth as any) + + // Verify crash recovery logging + assert( + loggerStub.info.calledWith( + '[crash-recovery] Language server crash detected, reinitializing authentication' + ) + ) + assert(loggerStub.debug.calledWith('[crash-recovery] Refreshing connection and sending bearer token')) + assert(loggerStub.debug.calledWith('[crash-recovery] Bearer token sent successfully')) + assert(loggerStub.info.calledWith('[crash-recovery] Authentication reinitialized successfully')) + + // Verify auth.refreshConnection was called + assert(mockAuth.refreshConnection.calledWith(true)) + + // Verify profile configuration was sent + assert( + pushConfigUpdateStub.calledWith(mockClient, { + type: 'profile', + profileArn: 'test-profile-arn', + }) + ) + }) + }) +}) diff --git a/packages/amazonq/test/unit/amazonq/lsp/config.test.ts b/packages/amazonq/test/unit/amazonq/lsp/config.test.ts index 0327395fe1a..c31e873e181 100644 --- a/packages/amazonq/test/unit/amazonq/lsp/config.test.ts +++ b/packages/amazonq/test/unit/amazonq/lsp/config.test.ts @@ -7,97 +7,221 @@ import assert from 'assert' import { DevSettings } from 'aws-core-vscode/shared' import sinon from 'sinon' import { defaultAmazonQLspConfig, ExtendedAmazonQLSPConfig, getAmazonQLspConfig } from '../../../../src/lsp/config' -import { defaultAmazonQWorkspaceLspConfig, getAmazonQWorkspaceLspConfig, LspConfig } from 'aws-core-vscode/amazonq' - -for (const [name, config, defaultConfig, setEnv, resetEnv] of [ - [ - 'getAmazonQLspConfig', - getAmazonQLspConfig, - defaultAmazonQLspConfig, - (envConfig: ExtendedAmazonQLSPConfig) => { - process.env.__AMAZONQLSP_MANIFEST_URL = envConfig.manifestUrl - process.env.__AMAZONQLSP_SUPPORTED_VERSIONS = envConfig.supportedVersions - process.env.__AMAZONQLSP_ID = envConfig.id - process.env.__AMAZONQLSP_PATH = envConfig.path - process.env.__AMAZONQLSP_UI = envConfig.ui - }, - () => { - delete process.env.__AMAZONQLSP_MANIFEST_URL - delete process.env.__AMAZONQLSP_SUPPORTED_VERSIONS - delete process.env.__AMAZONQLSP_ID - delete process.env.__AMAZONQLSP_PATH - delete process.env.__AMAZONQLSP_UI - }, - ], - [ - 'getAmazonQWorkspaceLspConfig', - getAmazonQWorkspaceLspConfig, - defaultAmazonQWorkspaceLspConfig, - (envConfig: LspConfig) => { - process.env.__AMAZONQWORKSPACELSP_MANIFEST_URL = envConfig.manifestUrl - process.env.__AMAZONQWORKSPACELSP_SUPPORTED_VERSIONS = envConfig.supportedVersions - process.env.__AMAZONQWORKSPACELSP_ID = envConfig.id - process.env.__AMAZONQWORKSPACELSP_PATH = envConfig.path - }, - () => { - delete process.env.__AMAZONQWORKSPACELSP_MANIFEST_URL - delete process.env.__AMAZONQWORKSPACELSP_SUPPORTED_VERSIONS - delete process.env.__AMAZONQWORKSPACELSP_ID - delete process.env.__AMAZONQWORKSPACELSP_PATH - }, - ], -] as const) { - describe(name, () => { - let sandbox: sinon.SinonSandbox - let serviceConfigStub: sinon.SinonStub - const settingConfig: LspConfig = { - manifestUrl: 'https://custom.url/manifest.json', - supportedVersions: '4.0.0', - id: 'AmazonQSetting', - suppressPromptPrefix: config().suppressPromptPrefix, - path: '/custom/path', - ...(name === 'getAmazonQLspConfig' && { ui: '/chat/client/location' }), - } - beforeEach(() => { - sandbox = sinon.createSandbox() +describe('getAmazonQLspConfig', () => { + let sandbox: sinon.SinonSandbox + let serviceConfigStub: sinon.SinonStub + const settingConfig: ExtendedAmazonQLSPConfig = { + manifestUrl: 'https://custom.url/manifest.json', + supportedVersions: '4.0.0', + id: 'AmazonQSetting', + suppressPromptPrefix: getAmazonQLspConfig().suppressPromptPrefix, + path: '/custom/path', + ui: '/chat/client/location', + } - serviceConfigStub = sandbox.stub() - sandbox.stub(DevSettings, 'instance').get(() => ({ - getServiceConfig: serviceConfigStub, - })) - }) + beforeEach(() => { + sandbox = sinon.createSandbox() - afterEach(() => { - sandbox.restore() - resetEnv() - }) + serviceConfigStub = sandbox.stub() + sandbox.stub(DevSettings, 'instance').get(() => ({ + getServiceConfig: serviceConfigStub, + })) + }) + + afterEach(() => { + sandbox.restore() + resetEnv() + }) + + it('uses default config', () => { + serviceConfigStub.returns({}) + assert.deepStrictEqual(getAmazonQLspConfig(), defaultAmazonQLspConfig) + }) - it('uses default config', () => { - serviceConfigStub.returns({}) - assert.deepStrictEqual(config(), defaultConfig) + it('overrides path', () => { + const path = '/custom/path/to/lsp' + serviceConfigStub.returns({ path }) + + assert.deepStrictEqual(getAmazonQLspConfig(), { + ...defaultAmazonQLspConfig, + path, }) + }) + + it('overrides default settings', () => { + serviceConfigStub.returns(settingConfig) + + assert.deepStrictEqual(getAmazonQLspConfig(), settingConfig) + }) + + it('environment variable takes precedence over settings', () => { + setEnv(settingConfig) + serviceConfigStub.returns({}) + assert.deepStrictEqual(getAmazonQLspConfig(), settingConfig) + }) - it('overrides path', () => { - const path = '/custom/path/to/lsp' - serviceConfigStub.returns({ path }) + function setEnv(envConfig: ExtendedAmazonQLSPConfig) { + process.env.__AMAZONQLSP_MANIFEST_URL = envConfig.manifestUrl + process.env.__AMAZONQLSP_SUPPORTED_VERSIONS = envConfig.supportedVersions + process.env.__AMAZONQLSP_ID = envConfig.id + process.env.__AMAZONQLSP_PATH = envConfig.path + process.env.__AMAZONQLSP_UI = envConfig.ui + } - assert.deepStrictEqual(config(), { - ...defaultConfig, - path, + function resetEnv() { + delete process.env.__AMAZONQLSP_MANIFEST_URL + delete process.env.__AMAZONQLSP_SUPPORTED_VERSIONS + delete process.env.__AMAZONQLSP_ID + delete process.env.__AMAZONQLSP_PATH + delete process.env.__AMAZONQLSP_UI + } +}) + +describe('pushConfigUpdate', () => { + let sandbox: sinon.SinonSandbox + let mockClient: any + let loggerStub: any + let getLoggerStub: sinon.SinonStub + let pushConfigUpdate: any + + beforeEach(() => { + sandbox = sinon.createSandbox() + + // Mock LanguageClient + mockClient = { + sendRequest: sandbox.stub().resolves(), + sendNotification: sandbox.stub(), + } + + // Create logger stub + loggerStub = { + debug: sandbox.stub(), + } + + // Clear all relevant module caches + const configModuleId = require.resolve('../../../../src/lsp/config') + const sharedModuleId = require.resolve('aws-core-vscode/shared') + delete require.cache[configModuleId] + delete require.cache[sharedModuleId] + + // jscpd:ignore-start + // Create getLogger stub and store reference for test verification + getLoggerStub = sandbox.stub().returns(loggerStub) + + // Create a mock shared module with stubbed getLogger + const mockSharedModule = { + getLogger: getLoggerStub, + } + + // Override the require cache with our mock + require.cache[sharedModuleId] = { + id: sharedModuleId, + filename: sharedModuleId, + loaded: true, + parent: undefined, + children: [], + exports: mockSharedModule, + paths: [], + } as any + + // Now require the module - it should use our mocked getLogger + // jscpd:ignore-end + const configModule = require('../../../../src/lsp/config') + pushConfigUpdate = configModule.pushConfigUpdate + }) + + afterEach(() => { + sandbox.restore() + }) + + it('should send profile configuration with logging', async () => { + const config = { + type: 'profile' as const, + profileArn: 'test-profile-arn', + } + + await pushConfigUpdate(mockClient, config) + + // Verify logging + assert(loggerStub.debug.calledWith('Pushing profile configuration: test-profile-arn')) + assert(loggerStub.debug.calledWith('Profile configuration pushed successfully')) + + // Verify client call + assert(mockClient.sendRequest.calledOnce) + assert( + mockClient.sendRequest.calledWith(sinon.match.string, { + section: 'aws.q', + settings: { profileArn: 'test-profile-arn' }, }) - }) + ) + }) - it('overrides default settings', () => { - serviceConfigStub.returns(settingConfig) + it('should send customization configuration with logging', async () => { + const config = { + type: 'customization' as const, + customization: 'test-customization-arn', + } - assert.deepStrictEqual(config(), settingConfig) - }) + await pushConfigUpdate(mockClient, config) - it('environment variable takes precedence over settings', () => { - setEnv(settingConfig) - serviceConfigStub.returns({}) - assert.deepStrictEqual(config(), settingConfig) - }) + // Verify logging + assert(loggerStub.debug.calledWith('Pushing customization configuration: test-customization-arn')) + assert(loggerStub.debug.calledWith('Customization configuration pushed successfully')) + + // Verify client call + assert(mockClient.sendNotification.calledOnce) + assert( + mockClient.sendNotification.calledWith(sinon.match.string, { + section: 'aws.q', + settings: { customization: 'test-customization-arn' }, + }) + ) + }) + + it('should handle undefined profile ARN', async () => { + const config = { + type: 'profile' as const, + profileArn: undefined, + } + + await pushConfigUpdate(mockClient, config) + + // Verify logging with undefined + assert(loggerStub.debug.calledWith('Pushing profile configuration: undefined')) + assert(loggerStub.debug.calledWith('Profile configuration pushed successfully')) + }) + + it('should handle undefined customization ARN', async () => { + const config = { + type: 'customization' as const, + customization: undefined, + } + + await pushConfigUpdate(mockClient, config) + + // Verify logging with undefined + assert(loggerStub.debug.calledWith('Pushing customization configuration: undefined')) + assert(loggerStub.debug.calledWith('Customization configuration pushed successfully')) + }) + + it('should send logLevel configuration with logging', async () => { + const config = { + type: 'logLevel' as const, + } + + await pushConfigUpdate(mockClient, config) + + // Verify logging + assert(loggerStub.debug.calledWith('Pushing log level configuration')) + assert(loggerStub.debug.calledWith('Log level configuration pushed successfully')) + + // Verify client call + assert(mockClient.sendNotification.calledOnce) + assert( + mockClient.sendNotification.calledWith(sinon.match.string, { + section: 'aws.logLevel', + }) + ) }) -} +}) diff --git a/packages/amazonq/test/unit/amazonq/lsp/encryption.test.ts b/packages/amazonq/test/unit/amazonq/lsp/encryption.test.ts new file mode 100644 index 00000000000..06a901edde6 --- /dev/null +++ b/packages/amazonq/test/unit/amazonq/lsp/encryption.test.ts @@ -0,0 +1,27 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import { decryptResponse, encryptRequest } from '../../../../src/lsp/encryption' +import { encryptionKey } from '../../../../src/lsp/auth' + +describe('LSP encryption', function () { + it('encrypt and decrypt invert eachother with same key', async function () { + const key = encryptionKey + const request = { + id: 0, + name: 'my Request', + isRealRequest: false, + metadata: { + tags: ['tag1', 'tag2'], + }, + } + const encryptedPayload = await encryptRequest(request, key) + const message = (encryptedPayload as { message: string }).message + const decrypted = await decryptResponse(message, key) + + assert.deepStrictEqual(decrypted, request) + }) +}) diff --git a/packages/amazonq/test/unit/amazonq/lsp/lspClient.test.ts b/packages/amazonq/test/unit/amazonq/lsp/lspClient.test.ts deleted file mode 100644 index 369cda5402d..00000000000 --- a/packages/amazonq/test/unit/amazonq/lsp/lspClient.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -/*! - * 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 { globals, getNodeExecutableName } from 'aws-core-vscode/shared' -import { LspClient, lspClient as lspClientModule } from 'aws-core-vscode/amazonq' - -describe('Amazon Q LSP client', function () { - let lspClient: LspClient - let encryptFunc: sinon.SinonSpy - - beforeEach(async function () { - sinon.stub(globals, 'isWeb').returns(false) - lspClient = new LspClient() - encryptFunc = sinon.spy(lspClient, 'encrypt') - }) - - it('encrypts payload of query ', async () => { - await lspClient.queryVectorIndex('mock_input') - assert.ok(encryptFunc.calledOnce) - assert.ok(encryptFunc.calledWith(JSON.stringify({ query: 'mock_input' }))) - const value = await encryptFunc.returnValues[0] - // verifies JWT encryption header - assert.ok(value.startsWith(`eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0`)) - }) - - it('encrypts payload of index files ', async () => { - await lspClient.buildIndex(['fileA'], 'path', 'all') - assert.ok(encryptFunc.calledOnce) - assert.ok( - encryptFunc.calledWith( - JSON.stringify({ - filePaths: ['fileA'], - projectRoot: 'path', - config: 'all', - language: '', - }) - ) - ) - const value = await encryptFunc.returnValues[0] - // verifies JWT encryption header - assert.ok(value.startsWith(`eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0`)) - }) - - it('encrypt removes readable information', async () => { - const sample = 'hello' - const encryptedSample = await lspClient.encrypt(sample) - assert.ok(!encryptedSample.includes('hello')) - }) - - it('validates node executable + lsp bundle', async () => { - await assert.rejects(async () => { - await lspClientModule.activate(globals.context, { - // Mimic the `LspResolution` type. - node: 'node.bogus.exe', - lsp: 'fake/lsp.js', - }) - }, /.*failed to run basic .*node.*exitcode.*node\.bogus\.exe.*/) - await assert.rejects(async () => { - await lspClientModule.activate(globals.context, { - node: getNodeExecutableName(), - lsp: 'fake/lsp.js', - }) - }, /.*failed to run .*exitcode.*node.*lsp\.js/) - }) - - afterEach(() => { - sinon.restore() - }) -}) diff --git a/packages/amazonq/test/unit/amazonqFeatureDev/session/chatSessionStorage.test.ts b/packages/amazonq/test/unit/amazonqFeatureDev/session/chatSessionStorage.test.ts deleted file mode 100644 index 4c6073114f8..00000000000 --- a/packages/amazonq/test/unit/amazonqFeatureDev/session/chatSessionStorage.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import * as assert from 'assert' -import { FeatureDevChatSessionStorage } from 'aws-core-vscode/amazonqFeatureDev' -import { Messenger } from 'aws-core-vscode/amazonq' -import { createMessenger } from 'aws-core-vscode/test' - -describe('chatSession', () => { - const tabID = '1234' - let chatStorage: FeatureDevChatSessionStorage - let messenger: Messenger - - beforeEach(() => { - messenger = createMessenger() - chatStorage = new FeatureDevChatSessionStorage(messenger) - }) - - it('locks getSession', async () => { - const results = await Promise.allSettled([chatStorage.getSession(tabID), chatStorage.getSession(tabID)]) - assert.equal(results.length, 2) - assert.deepStrictEqual(results[0], results[1]) - }) -}) diff --git a/packages/amazonq/test/unit/amazonqFeatureDev/session/session.test.ts b/packages/amazonq/test/unit/amazonqFeatureDev/session/session.test.ts deleted file mode 100644 index 39c38de555f..00000000000 --- a/packages/amazonq/test/unit/amazonqFeatureDev/session/session.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -/*! - * 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 sinon from 'sinon' - -import { - ControllerSetup, - createController, - createMessenger, - createSession, - generateVirtualMemoryUri, - sessionRegisterProvider, - sessionWriteFile, - assertTelemetry, -} from 'aws-core-vscode/test' -import { FeatureDevClient, featureDevScheme, FeatureDevCodeGenState } from 'aws-core-vscode/amazonqFeatureDev' -import { Messenger, CurrentWsFolders } from 'aws-core-vscode/amazonq' -import path from 'path' -import { fs } from 'aws-core-vscode/shared' - -describe('session', () => { - const conversationID = '12345' - let messenger: Messenger - - beforeEach(() => { - messenger = createMessenger() - }) - - afterEach(() => { - sinon.restore() - }) - - describe('preloader', () => { - it('emits start chat telemetry', async () => { - const session = await createSession({ messenger, conversationID, scheme: featureDevScheme }) - session.latestMessage = 'implement twosum in typescript' - - await session.preloader() - - assertTelemetry('amazonq_startConversationInvoke', { - amazonqConversationId: conversationID, - }) - }) - }) - describe('insertChanges', async () => { - afterEach(() => { - sinon.restore() - }) - - let workspaceFolderUriFsPath: string - const notRejectedFileName = 'notRejectedFile.js' - const notRejectedFileContent = 'notrejectedFileContent' - let uri: vscode.Uri - let encodedContent: Uint8Array - - async function createCodeGenState() { - const controllerSetup: ControllerSetup = await createController() - - const uploadID = '789' - const tabID = '123' - const workspaceFolders = [controllerSetup.workspaceFolder] as CurrentWsFolders - workspaceFolderUriFsPath = controllerSetup.workspaceFolder.uri.fsPath - uri = generateVirtualMemoryUri(uploadID, notRejectedFileName, featureDevScheme) - - const testConfig = { - conversationId: conversationID, - proxyClient: {} as unknown as FeatureDevClient, - workspaceRoots: [''], - uploadId: uploadID, - workspaceFolders, - } - - const codeGenState = new FeatureDevCodeGenState( - testConfig, - [ - { - zipFilePath: notRejectedFileName, - relativePath: notRejectedFileName, - fileContent: notRejectedFileContent, - rejected: false, - virtualMemoryUri: uri, - workspaceFolder: controllerSetup.workspaceFolder, - changeApplied: false, - }, - { - zipFilePath: 'rejectedFile.js', - relativePath: 'rejectedFile.js', - fileContent: 'rejectedFileContent', - rejected: true, - virtualMemoryUri: generateVirtualMemoryUri(uploadID, 'rejectedFile.js', featureDevScheme), - workspaceFolder: controllerSetup.workspaceFolder, - changeApplied: false, - }, - ], - [], - [], - tabID, - 0, - {} - ) - const session = await createSession({ - messenger, - sessionState: codeGenState, - conversationID, - scheme: featureDevScheme, - }) - encodedContent = new TextEncoder().encode(notRejectedFileContent) - await sessionRegisterProvider(session, uri, encodedContent) - return session - } - it('only insert non rejected files', async () => { - const fsSpyWriteFile = sinon.spy(fs, 'writeFile') - const session = await createCodeGenState() - sinon.stub(session, 'sendLinesOfCodeAcceptedTelemetry').resolves() - await sessionWriteFile(session, uri, encodedContent) - await session.insertChanges() - - const absolutePath = path.join(workspaceFolderUriFsPath, notRejectedFileName) - - assert.ok(fsSpyWriteFile.calledOnce) - assert.ok(fsSpyWriteFile.calledWith(absolutePath, notRejectedFileContent)) - }) - }) -}) diff --git a/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts b/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts deleted file mode 100644 index 574d0a25a19..00000000000 --- a/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts +++ /dev/null @@ -1,172 +0,0 @@ -/*! - * 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 { - prepareRepoData, - PrepareRepoDataOptions, - TelemetryHelper, - maxRepoSizeBytes, -} from 'aws-core-vscode/amazonqFeatureDev' -import { assertTelemetry, getWorkspaceFolder, TestFolder } from 'aws-core-vscode/test' -import { fs, AmazonqCreateUpload, ZipStream, ContentLengthError } from 'aws-core-vscode/shared' -import { MetricName, Span } from 'aws-core-vscode/telemetry' -import sinon from 'sinon' -import { CodeWhispererSettings } from 'aws-core-vscode/codewhisperer' -import { CurrentWsFolders } from 'aws-core-vscode/amazonq' -import path from 'path' - -const testDevfilePrepareRepo = async (devfileEnabled: boolean) => { - const files: Record = { - 'file.md': 'test content', - // only include when execution is enabled - 'devfile.yaml': 'test', - // .git folder is always dropped (because of vscode global exclude rules) - '.git/ref': '####', - // .gitignore should always be included - '.gitignore': 'node_models/*', - // non code files only when dev execution is enabled - 'abc.jar': 'jar-content', - 'data/logo.ico': 'binary-content', - } - const folder = await TestFolder.create() - - for (const [fileName, content] of Object.entries(files)) { - await folder.write(fileName, content) - } - - const expectedFiles = !devfileEnabled - ? ['file.md', '.gitignore'] - : ['devfile.yaml', 'file.md', '.gitignore', 'abc.jar', 'data/logo.ico'] - - const workspace = getWorkspaceFolder(folder.path) - sinon - .stub(CodeWhispererSettings.instance, 'getAutoBuildSetting') - .returns(devfileEnabled ? { [workspace.uri.fsPath]: true } : {}) - - await testPrepareRepoData([workspace], expectedFiles, { telemetry: new TelemetryHelper() }) -} - -const testPrepareRepoData = async ( - workspaces: vscode.WorkspaceFolder[], - expectedFiles: string[], - prepareRepoDataOptions: PrepareRepoDataOptions, - expectedTelemetryMetrics?: Array<{ metricName: MetricName; value: any }> -) => { - expectedFiles.sort((a, b) => a.localeCompare(b)) - const result = await prepareRepoData( - workspaces.map((ws) => ws.uri.fsPath), - workspaces as CurrentWsFolders, - { - record: () => {}, - } as unknown as Span, - prepareRepoDataOptions - ) - - assert.strictEqual(Buffer.isBuffer(result.zipFileBuffer), true) - // checksum is not the same across different test executions because some unique random folder names are generated - assert.strictEqual(result.zipFileChecksum.length, 44) - - if (expectedTelemetryMetrics) { - for (const metric of expectedTelemetryMetrics) { - assertTelemetry(metric.metricName, metric.value) - } - } - - // Unzip the buffer and compare the entry names - const zipEntries = await ZipStream.unzip(result.zipFileBuffer) - const actualZipEntries = zipEntries.map((entry) => entry.filename) - actualZipEntries.sort((a, b) => a.localeCompare(b)) - assert.deepStrictEqual(actualZipEntries, expectedFiles) -} - -describe('file utils', () => { - describe('prepareRepoData', function () { - const defaultPrepareRepoDataOptions: PrepareRepoDataOptions = { telemetry: new TelemetryHelper() } - - afterEach(() => { - sinon.restore() - }) - - it('returns files in the workspace as a zip', async function () { - const folder = await TestFolder.create() - await folder.write('file1.md', 'test content') - await folder.write('file2.md', 'test content') - await folder.write('docs/infra.svg', 'test content') - const workspace = getWorkspaceFolder(folder.path) - - await testPrepareRepoData([workspace], ['file1.md', 'file2.md'], defaultPrepareRepoDataOptions) - }) - - it('infrastructure diagram is included', async function () { - const folder = await TestFolder.create() - await folder.write('file1.md', 'test content') - await folder.write('file2.svg', 'test content') - await folder.write('docs/infra.svg', 'test content') - const workspace = getWorkspaceFolder(folder.path) - - await testPrepareRepoData([workspace], ['file1.md', 'docs/infra.svg'], { - telemetry: new TelemetryHelper(), - isIncludeInfraDiagram: true, - }) - }) - - it('prepareRepoData ignores denied file extensions', async function () { - const folder = await TestFolder.create() - await folder.write('file.mp4', 'test content') - const workspace = getWorkspaceFolder(folder.path) - - await testPrepareRepoData([workspace], [], defaultPrepareRepoDataOptions, [ - { metricName: 'amazonq_bundleExtensionIgnored', value: { filenameExt: 'mp4', count: 1 } }, - ]) - }) - - it('should ignore devfile.yaml when setting is disabled', async function () { - await testDevfilePrepareRepo(false) - }) - - it('should include devfile.yaml when setting is enabled', async function () { - await testDevfilePrepareRepo(true) - }) - - // Test the logic that allows the customer to modify root source folder - it('prepareRepoData throws a ContentLengthError code when repo is too big', async function () { - const folder = await TestFolder.create() - await folder.write('file.md', 'test content') - const workspace = getWorkspaceFolder(folder.path) - - sinon.stub(fs, 'stat').resolves({ size: 2 * maxRepoSizeBytes } as vscode.FileStat) - await assert.rejects( - () => - prepareRepoData( - [workspace.uri.fsPath], - [workspace], - { - record: () => {}, - } as unknown as Span, - defaultPrepareRepoDataOptions - ), - ContentLengthError - ) - }) - - it('prepareRepoData properly handles multi-root workspaces', async function () { - const folder = await TestFolder.create() - const testFilePath = 'innerFolder/file.md' - await folder.write(testFilePath, 'test content') - - // Add a folder and its subfolder to the workspace - const workspace1 = getWorkspaceFolder(folder.path) - const workspace2 = getWorkspaceFolder(folder.path + '/innerFolder') - const folderName = path.basename(folder.path) - - await testPrepareRepoData( - [workspace1, workspace2], - [`${folderName}_${workspace1.name}/${testFilePath}`], - defaultPrepareRepoDataOptions - ) - }) - }) -}) diff --git a/packages/amazonq/test/unit/app/inline/EditRendering/diffUtils.test.ts b/packages/amazonq/test/unit/app/inline/EditRendering/diffUtils.test.ts new file mode 100644 index 00000000000..dee096d7f57 --- /dev/null +++ b/packages/amazonq/test/unit/app/inline/EditRendering/diffUtils.test.ts @@ -0,0 +1,83 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as assert from 'assert' +import { applyUnifiedDiff } from '../../../../../src/app/inline/EditRendering/diffUtils' + +describe('diffUtils', function () { + describe('applyUnifiedDiff', function () { + it('should correctly apply a unified diff to original text', function () { + // Original code + const originalCode = 'function add(a, b) {\n return a + b;\n}' + + // Unified diff that adds a comment and modifies the return statement + const unifiedDiff = + '--- a/file.js\n' + + '+++ b/file.js\n' + + '@@ -1,3 +1,4 @@\n' + + ' function add(a, b) {\n' + + '+ // Add two numbers\n' + + '- return a + b;\n' + + '+ return a + b; // Return the sum\n' + + ' }' + + // Expected result after applying the diff + const expectedResult = 'function add(a, b) {\n // Add two numbers\n return a + b; // Return the sum\n}' + + // Apply the diff + const appliedCode = applyUnifiedDiff(originalCode, unifiedDiff) + + // Verify the result + assert.strictEqual(appliedCode, expectedResult) + }) + }) + + describe('applyUnifiedDiff with complex changes', function () { + it('should handle multiple hunks in a diff', function () { + // Original code with multiple functions + const originalCode = + 'function add(a, b) {\n' + + ' return a + b;\n' + + '}\n' + + '\n' + + 'function subtract(a, b) {\n' + + ' return a - b;\n' + + '}' + + // Unified diff that modifies both functions + const unifiedDiff = + '--- a/file.js\n' + + '+++ b/file.js\n' + + '@@ -1,3 +1,4 @@\n' + + ' function add(a, b) {\n' + + '+ // Addition function\n' + + ' return a + b;\n' + + ' }\n' + + '@@ -5,3 +6,4 @@\n' + + ' function subtract(a, b) {\n' + + '+ // Subtraction function\n' + + ' return a - b;\n' + + ' }' + + // Expected result after applying the diff + const expectedResult = + 'function add(a, b) {\n' + + ' // Addition function\n' + + ' return a + b;\n' + + '}\n' + + '\n' + + 'function subtract(a, b) {\n' + + ' // Subtraction function\n' + + ' return a - b;\n' + + '}' + + // Apply the diff + const appliedCode = applyUnifiedDiff(originalCode, unifiedDiff) + + // Verify the result + assert.strictEqual(appliedCode, expectedResult) + }) + }) +}) diff --git a/packages/amazonq/test/unit/app/inline/EditRendering/displayImage.test.ts b/packages/amazonq/test/unit/app/inline/EditRendering/displayImage.test.ts new file mode 100644 index 00000000000..28155811f50 --- /dev/null +++ b/packages/amazonq/test/unit/app/inline/EditRendering/displayImage.test.ts @@ -0,0 +1,417 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as sinon from 'sinon' +import assert from 'assert' +import { EditDecorationManager, displaySvgDecoration } from '../../../../../src/app/inline/EditRendering/displayImage' +import { EditSuggestionState } from '../../../../../src/app/inline/editSuggestionState' + +// Shared helper function to create common stubs +function createCommonStubs(sandbox: sinon.SinonSandbox) { + const documentStub = { + getText: sandbox.stub().returns('Original code content'), + uri: vscode.Uri.file('/test/file.ts'), + lineAt: sandbox.stub().returns({ + text: 'Line text content', + range: new vscode.Range(0, 0, 0, 18), + rangeIncludingLineBreak: new vscode.Range(0, 0, 0, 19), + firstNonWhitespaceCharacterIndex: 0, + isEmptyOrWhitespace: false, + }), + } as unknown as sinon.SinonStubbedInstance + + const editorStub = { + document: documentStub, + setDecorations: sandbox.stub(), + } as unknown as sinon.SinonStubbedInstance + + return { documentStub, editorStub } +} + +describe('EditDecorationManager', function () { + let sandbox: sinon.SinonSandbox + let editorStub: sinon.SinonStubbedInstance + let documentStub: sinon.SinonStubbedInstance + let windowStub: sinon.SinonStubbedInstance + let commandsStub: sinon.SinonStubbedInstance + let decorationTypeStub: sinon.SinonStubbedInstance + let manager: EditDecorationManager + + beforeEach(function () { + sandbox = sinon.createSandbox() + + // Create stubs for vscode objects + decorationTypeStub = { + dispose: sandbox.stub(), + } as unknown as sinon.SinonStubbedInstance + + const commonStubs = createCommonStubs(sandbox) + documentStub = commonStubs.documentStub + editorStub = commonStubs.editorStub + + // Add additional properties needed for this test suite - extend the stub objects + Object.assign(documentStub, { lineCount: 5 }) + Object.assign(editorStub, { edit: sandbox.stub().resolves(true) }) + + windowStub = sandbox.stub(vscode.window) + windowStub.createTextEditorDecorationType.returns(decorationTypeStub as any) + + commandsStub = sandbox.stub(vscode.commands) + commandsStub.registerCommand.returns({ dispose: sandbox.stub() }) + + // Create a new instance of EditDecorationManager for each test + manager = new EditDecorationManager() + }) + + afterEach(function () { + sandbox.restore() + }) + + it('should display SVG decorations in the editor', async function () { + // Create a fake SVG image URI + const svgUri = vscode.Uri.parse('file:///path/to/image.svg') + + // Create accept and reject handlers + const acceptHandler = sandbox.stub() + const rejectHandler = sandbox.stub() + + // Reset the setDecorations stub to clear any previous calls + editorStub.setDecorations.reset() + + // Call displayEditSuggestion + await manager.displayEditSuggestion( + editorStub as unknown as vscode.TextEditor, + svgUri, + 0, + acceptHandler, + rejectHandler, + 'Original code', + 'New code', + [{ line: 0, start: 0, end: 0 }] + ) + + // Verify decorations were set (we expect 4 calls because clearDecorations is called first) + assert.strictEqual(editorStub.setDecorations.callCount, 4) + + // Verify the third call is for the image decoration (after clearDecorations) + const imageCall = editorStub.setDecorations.getCall(2) + assert.strictEqual(imageCall.args[0], manager['imageDecorationType']) + assert.strictEqual(imageCall.args[1].length, 1) + + // Verify the fourth call is for the removed code decoration + const removedCodeCall = editorStub.setDecorations.getCall(3) + assert.strictEqual(removedCodeCall.args[0], manager['removedCodeDecorationType']) + }) + + // Helper function to setup edit suggestion test + async function setupEditSuggestionTest() { + // Create a fake SVG image URI + const svgUri = vscode.Uri.parse('file:///path/to/image.svg') + + // Create accept and reject handlers + const acceptHandler = sandbox.stub() + const rejectHandler = sandbox.stub() + + // Display the edit suggestion + await manager.displayEditSuggestion( + editorStub as unknown as vscode.TextEditor, + svgUri, + 0, + acceptHandler, + rejectHandler, + 'Original code', + 'New code', + [{ line: 0, start: 0, end: 0 }] + ) + + return { acceptHandler, rejectHandler } + } + + it('should trigger accept handler when command is executed', async function () { + const { acceptHandler, rejectHandler } = await setupEditSuggestionTest() + + // Find the command handler that was registered for accept + const acceptCommandArgs = commandsStub.registerCommand.args.find( + (args) => args[0] === 'aws.amazonq.inline.acceptEdit' + ) + + // Execute the accept command handler if found + if (acceptCommandArgs && acceptCommandArgs[1]) { + const acceptCommandHandler = acceptCommandArgs[1] + acceptCommandHandler() + + // Verify the accept handler was called + sinon.assert.calledOnce(acceptHandler) + sinon.assert.notCalled(rejectHandler) + } else { + assert.fail('Accept command handler not found') + } + }) + + it('should trigger reject handler when command is executed', async function () { + const { acceptHandler, rejectHandler } = await setupEditSuggestionTest() + + // Find the command handler that was registered for reject + const rejectCommandArgs = commandsStub.registerCommand.args.find( + (args) => args[0] === 'aws.amazonq.inline.rejectEdit' + ) + + // Execute the reject command handler if found + if (rejectCommandArgs && rejectCommandArgs[1]) { + const rejectCommandHandler = rejectCommandArgs[1] + rejectCommandHandler() + + // Verify the reject handler was called + sinon.assert.calledOnce(rejectHandler) + sinon.assert.notCalled(acceptHandler) + } else { + assert.fail('Reject command handler not found') + } + }) + + it('should clear decorations when requested', async function () { + // Reset the setDecorations stub to clear any previous calls + editorStub.setDecorations.reset() + + // Call clearDecorations + await manager.clearDecorations(editorStub as unknown as vscode.TextEditor) + + // Verify decorations were cleared + assert.strictEqual(editorStub.setDecorations.callCount, 2) + + // Verify both decoration types were cleared + sinon.assert.calledWith(editorStub.setDecorations.firstCall, manager['imageDecorationType'], []) + sinon.assert.calledWith(editorStub.setDecorations.secondCall, manager['removedCodeDecorationType'], []) + }) +}) + +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) + }) +}) + +describe('displaySvgDecoration cursor distance auto-reject', function () { + let sandbox: sinon.SinonSandbox + let editorStub: sinon.SinonStubbedInstance + let windowStub: sinon.SinonStub + let commandsStub: sinon.SinonStub + let editSuggestionStateStub: sinon.SinonStub + let onDidChangeTextEditorSelectionStub: sinon.SinonStub + let selectionChangeListener: (e: vscode.TextEditorSelectionChangeEvent) => void + + // Helper function to setup displaySvgDecoration + async function setupDisplaySvgDecoration(startLine: number) { + return await displaySvgDecoration( + editorStub as unknown as vscode.TextEditor, + vscode.Uri.parse('data:image/svg+xml;base64,test'), + startLine, + 'new code', + [], + {} as any, + {} as any, + { itemId: 'test', insertText: 'patch' } as any + ) + } + + // Helper function to create selection change event + function createSelectionChangeEvent(line: number): vscode.TextEditorSelectionChangeEvent { + const position = new vscode.Position(line, 0) + const selection = new vscode.Selection(position, position) + return { + textEditor: editorStub, + selections: [selection], + kind: vscode.TextEditorSelectionChangeKind.Mouse, + } as vscode.TextEditorSelectionChangeEvent + } + + beforeEach(function () { + sandbox = sinon.createSandbox() + + const commonStubs = createCommonStubs(sandbox) + editorStub = commonStubs.editorStub + + // Mock vscode.window.onDidChangeTextEditorSelection + onDidChangeTextEditorSelectionStub = sandbox.stub() + onDidChangeTextEditorSelectionStub.returns({ dispose: sandbox.stub() }) + windowStub = sandbox.stub(vscode.window, 'onDidChangeTextEditorSelection') + windowStub.callsFake((callback) => { + selectionChangeListener = callback + return { dispose: sandbox.stub() } + }) + + // Mock vscode.commands.executeCommand + commandsStub = sandbox.stub(vscode.commands, 'executeCommand') + + // Mock EditSuggestionState + editSuggestionStateStub = sandbox.stub(EditSuggestionState, 'isEditSuggestionActive') + editSuggestionStateStub.returns(true) + + // Mock other required dependencies + sandbox.stub(vscode.workspace, 'onDidChangeTextDocument').returns({ dispose: sandbox.stub() }) + }) + + afterEach(function () { + sandbox.restore() + }) + + 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) + + selectionChangeListener(createSelectionChangeEvent(startLine + 24)) + + sinon.assert.notCalled(commandsStub) + }) + + 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) + + selectionChangeListener(createSelectionChangeEvent(startLine + 25)) + + sinon.assert.notCalled(commandsStub) + }) + + 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) + + selectionChangeListener(createSelectionChangeEvent(startLine + 26)) + + sinon.assert.calledOnceWithExactly(commandsStub, 'aws.amazonq.inline.rejectEdit') + }) + + 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) + + selectionChangeListener(createSelectionChangeEvent(startLine - 26)) + + sinon.assert.calledOnceWithExactly(commandsStub, 'aws.amazonq.inline.rejectEdit') + }) + + 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) + + selectionChangeListener(createSelectionChangeEvent(0)) + + sinon.assert.notCalled(commandsStub) + }) + + 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 + await setupDisplaySvgDecoration(startLine) + + selectionChangeListener(createSelectionChangeEvent(startLine + 100)) + + sinon.assert.notCalled(commandsStub) + }) +}) diff --git a/packages/amazonq/test/unit/app/inline/EditRendering/imageRenderer.test.ts b/packages/amazonq/test/unit/app/inline/EditRendering/imageRenderer.test.ts new file mode 100644 index 00000000000..e1c32778d83 --- /dev/null +++ b/packages/amazonq/test/unit/app/inline/EditRendering/imageRenderer.test.ts @@ -0,0 +1,269 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +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 { SvgGenerationService } from '../../../../../src/app/inline/EditRendering/svgGenerator' +import { InlineCompletionItemWithReferences } from '@aws/language-server-runtimes/protocol' + +describe('showEdits', function () { + let sandbox: sinon.SinonSandbox + let editorStub: sinon.SinonStubbedInstance + let documentStub: sinon.SinonStubbedInstance + let svgGenerationServiceStub: sinon.SinonStubbedInstance + let displaySvgDecorationStub: sinon.SinonStub + let loggerStub: sinon.SinonStubbedInstance + let getLoggerStub: sinon.SinonStub + let showEdits: any // Will be dynamically imported + let languageClientStub: any + let sessionStub: any + let itemStub: InlineCompletionItemWithReferences + + // Helper function to create mock SVG result + function createMockSvgResult(overrides: Partial = {}) { + return { + svgImage: vscode.Uri.file('/path/to/generated.svg'), + startLine: 5, + newCode: 'console.log("Hello World");', + originalCodeHighlightRange: [{ line: 5, start: 0, end: 10 }], + ...overrides, + } + } + + beforeEach(function () { + sandbox = sinon.createSandbox() + + // Create logger stub + loggerStub = { + error: sandbox.stub(), + info: sandbox.stub(), + debug: sandbox.stub(), + warn: sandbox.stub(), + } + + // Clear all relevant module caches + const moduleId = require.resolve('../../../../../src/app/inline/EditRendering/imageRenderer') + const sharedModuleId = require.resolve('aws-core-vscode/shared') + delete require.cache[moduleId] + delete require.cache[sharedModuleId] + + // jscpd:ignore-start + // Create getLogger stub and store reference for test verification + getLoggerStub = sandbox.stub().returns(loggerStub) + + // Create a mock shared module with stubbed getLogger + const mockSharedModule = { + getLogger: getLoggerStub, + } + + // Override the require cache with our mock + require.cache[sharedModuleId] = { + id: sharedModuleId, + filename: sharedModuleId, + loaded: true, + parent: undefined, + children: [], + exports: mockSharedModule, + paths: [], + } as any + + // Now require the module - it should use our mocked getLogger + // jscpd:ignore-end + const imageRendererModule = require('../../../../../src/app/inline/EditRendering/imageRenderer') + showEdits = imageRendererModule.showEdits + + // Create document stub + documentStub = { + uri: { + fsPath: '/path/to/test/file.ts', + }, + getText: sandbox.stub().returns('Original code content'), + lineCount: 5, + } as unknown as sinon.SinonStubbedInstance + + // Create editor stub + editorStub = { + document: documentStub, + setDecorations: sandbox.stub(), + edit: sandbox.stub().resolves(true), + } as unknown as sinon.SinonStubbedInstance + + // Create SVG generation service stub + svgGenerationServiceStub = { + generateDiffSvg: sandbox.stub(), + } as unknown as sinon.SinonStubbedInstance + + // Stub the SvgGenerationService constructor + sandbox + .stub(SvgGenerationService.prototype, 'generateDiffSvg') + .callsFake(svgGenerationServiceStub.generateDiffSvg) + + // Create display SVG decoration stub + displaySvgDecorationStub = sandbox.stub() + sandbox.replace( + require('../../../../../src/app/inline/EditRendering/displayImage'), + 'displaySvgDecoration', + displaySvgDecorationStub + ) + + // Create language client stub + languageClientStub = {} as any + + // Create session stub + sessionStub = { + sessionId: 'test-session-id', + suggestions: [], + isRequestInProgress: false, + requestStartTime: Date.now(), + startPosition: new vscode.Position(0, 0), + } as any + + // Create item stub + itemStub = { + insertText: 'console.log("Hello World");', + range: new vscode.Range(0, 0, 0, 0), + itemId: 'test-item-id', + } as any + }) + + afterEach(function () { + sandbox.restore() + }) + + it('should return early when editor is undefined', async function () { + await showEdits(itemStub, undefined, sessionStub, languageClientStub) + + // Verify that no SVG generation or display methods were called + sinon.assert.notCalled(svgGenerationServiceStub.generateDiffSvg) + sinon.assert.notCalled(displaySvgDecorationStub) + sinon.assert.notCalled(loggerStub.error) + }) + + it('should successfully generate and display SVG when all parameters are valid', async function () { + // Setup successful SVG generation + const mockSvgResult = createMockSvgResult() + svgGenerationServiceStub.generateDiffSvg.resolves(mockSvgResult) + + await showEdits(itemStub, editorStub as unknown as vscode.TextEditor, sessionStub, languageClientStub) + + // Verify SVG generation was called with correct parameters + sinon.assert.calledOnce(svgGenerationServiceStub.generateDiffSvg) + sinon.assert.calledWith( + svgGenerationServiceStub.generateDiffSvg, + '/path/to/test/file.ts', + 'console.log("Hello World");' + ) + + // 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 + ) + + // Verify no errors were logged + sinon.assert.notCalled(loggerStub.error) + }) + + it('should log error when SVG generation returns empty result', async function () { + // Setup SVG generation to return undefined svgImage + const mockSvgResult = createMockSvgResult({ svgImage: undefined as any }) + svgGenerationServiceStub.generateDiffSvg.resolves(mockSvgResult) + + await showEdits(itemStub, editorStub as unknown as vscode.TextEditor, sessionStub, languageClientStub) + + // Verify SVG generation was called + sinon.assert.calledOnce(svgGenerationServiceStub.generateDiffSvg) + + // Verify display decoration was NOT called + sinon.assert.notCalled(displaySvgDecorationStub) + + // Verify error was logged + sinon.assert.calledOnce(loggerStub.error) + sinon.assert.calledWith(loggerStub.error, 'SVG image generation returned an empty result.') + }) + + it('should catch and log error when SVG generation throws exception', async function () { + // Setup SVG generation to throw an error + const testError = new Error('SVG generation failed') + svgGenerationServiceStub.generateDiffSvg.rejects(testError) + + await showEdits(itemStub, editorStub as unknown as vscode.TextEditor, sessionStub, languageClientStub) + + // Verify SVG generation was called + sinon.assert.calledOnce(svgGenerationServiceStub.generateDiffSvg) + + // Verify display decoration was NOT called + sinon.assert.notCalled(displaySvgDecorationStub) + + // Verify error was logged with correct message + sinon.assert.calledOnce(loggerStub.error) + const errorCall = loggerStub.error.getCall(0) + assert.strictEqual(errorCall.args[0], `Error generating SVG image: ${testError}`) + }) + + it('should catch and log error when displaySvgDecoration throws exception', async function () { + // Setup successful SVG generation + const mockSvgResult = createMockSvgResult() + svgGenerationServiceStub.generateDiffSvg.resolves(mockSvgResult) + + // Setup displaySvgDecoration to throw an error + const testError = new Error('Display decoration failed') + displaySvgDecorationStub.rejects(testError) + + await showEdits(itemStub, editorStub as unknown as vscode.TextEditor, sessionStub, languageClientStub) + + // Verify SVG generation was called + sinon.assert.calledOnce(svgGenerationServiceStub.generateDiffSvg) + + // Verify display decoration was called + sinon.assert.calledOnce(displaySvgDecorationStub) + + // Verify error was logged with correct message + sinon.assert.calledOnce(loggerStub.error) + const errorCall = loggerStub.error.getCall(0) + assert.strictEqual(errorCall.args[0], `Error generating SVG image: ${testError}`) + }) + + it('should use correct logger name', async function () { + await showEdits(itemStub, editorStub as unknown as vscode.TextEditor, sessionStub, languageClientStub) + + // Verify getLogger was called with correct name + sinon.assert.calledWith(getLoggerStub, 'nextEditPrediction') + }) + + it('should handle item with undefined insertText', async function () { + // Create item with undefined insertText + const itemWithUndefinedText = { + ...itemStub, + insertText: undefined, + } as any + + // Setup successful SVG generation + const mockSvgResult = createMockSvgResult() + svgGenerationServiceStub.generateDiffSvg.resolves(mockSvgResult) + + await showEdits( + itemWithUndefinedText, + editorStub as unknown as vscode.TextEditor, + sessionStub, + languageClientStub + ) + + // Verify SVG generation was called with undefined as string + sinon.assert.calledOnce(svgGenerationServiceStub.generateDiffSvg) + sinon.assert.calledWith(svgGenerationServiceStub.generateDiffSvg, '/path/to/test/file.ts', undefined) + }) +}) diff --git a/packages/amazonq/test/unit/app/inline/EditRendering/svgGenerator.test.ts b/packages/amazonq/test/unit/app/inline/EditRendering/svgGenerator.test.ts new file mode 100644 index 00000000000..657ff5c2915 --- /dev/null +++ b/packages/amazonq/test/unit/app/inline/EditRendering/svgGenerator.test.ts @@ -0,0 +1,252 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as sinon from 'sinon' +import assert from 'assert' +import { SvgGenerationService } from '../../../../../src/app/inline/EditRendering/svgGenerator' + +describe('SvgGenerationService', function () { + let sandbox: sinon.SinonSandbox + let service: SvgGenerationService + let documentStub: sinon.SinonStubbedInstance + let workspaceStub: sinon.SinonStubbedInstance + let editorConfigStub: any + + beforeEach(function () { + sandbox = sinon.createSandbox() + + // Create stubs for vscode objects and utilities + documentStub = { + getText: sandbox.stub().returns('function example() {\n return 42;\n}'), + lineCount: 3, + lineAt: sandbox.stub().returns({ + text: 'Line content', + range: new vscode.Range(0, 0, 0, 12), + }), + } as unknown as sinon.SinonStubbedInstance + + workspaceStub = sandbox.stub(vscode.workspace) + workspaceStub.openTextDocument.resolves(documentStub as unknown as vscode.TextDocument) + workspaceStub.getConfiguration = sandbox.stub() + + editorConfigStub = { + get: sandbox.stub(), + } + editorConfigStub.get.withArgs('fontSize').returns(14) + editorConfigStub.get.withArgs('lineHeight').returns(0) + + // Create the service instance + service = new SvgGenerationService() + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('generateDiffSvg', function () { + it('should handle empty original code', async function () { + // Create a new document stub for this test with empty content + const emptyDocStub = { + getText: sandbox.stub().returns(''), + lineCount: 0, + lineAt: sandbox.stub().returns({ + text: '', + range: new vscode.Range(0, 0, 0, 0), + }), + } as unknown as vscode.TextDocument + + // Make openTextDocument return our empty document + workspaceStub.openTextDocument.resolves(emptyDocStub as unknown as vscode.TextDocument) + + // A simple unified diff + const udiff = '--- a/example.js\n+++ b/example.js\n@@ -0,0 +1,1 @@\n+function example() {}\n' + + // Expect an error to be thrown + try { + await service.generateDiffSvg('example.js', udiff) + assert.fail('Expected an error to be thrown') + } catch (error) { + assert.ok(error) + assert.strictEqual((error as Error).message, 'udiff format error') + } + }) + }) + + describe('theme handling', function () { + it('should generate correct styles for dark theme', function () { + // Configure for dark theme + workspaceStub.getConfiguration.withArgs('editor').returns(editorConfigStub) + workspaceStub.getConfiguration.withArgs('workbench').returns({ + get: sandbox.stub().withArgs('colorTheme', 'Default').returns('Dark+ (default dark)'), + } as any) + + const getEditorTheme = (service as any).getEditorTheme.bind(service) + const theme = getEditorTheme() + + assert.strictEqual(theme.fontSize, 14) + assert.strictEqual(theme.lingHeight, 21) // 1.5 * 14 + assert.strictEqual(theme.foreground, 'rgba(212, 212, 212, 1)') + assert.strictEqual(theme.background, 'rgba(30, 30, 30, 1)') + }) + + it('should generate correct styles for light theme', function () { + // Reconfigure for light theme + editorConfigStub.get.withArgs('fontSize', 12).returns(12) + + workspaceStub.getConfiguration.withArgs('editor').returns(editorConfigStub) + workspaceStub.getConfiguration.withArgs('workbench').returns({ + get: sandbox.stub().withArgs('colorTheme', 'Default').returns('Light+ (default light)'), + } as any) + + const getEditorTheme = (service as any).getEditorTheme.bind(service) + const theme = getEditorTheme() + + assert.strictEqual(theme.fontSize, 12) + assert.strictEqual(theme.lingHeight, 18) // 1.5 * 12 + assert.strictEqual(theme.foreground, 'rgba(0, 0, 0, 1)') + assert.strictEqual(theme.background, 'rgba(255, 255, 255, 1)') + }) + + it('should handle custom line height settings', function () { + // Reconfigure for custom line height + editorConfigStub.get.withArgs('fontSize').returns(16) + editorConfigStub.get.withArgs('lineHeight').returns(2.5) + + workspaceStub.getConfiguration.withArgs('editor').returns(editorConfigStub) + workspaceStub.getConfiguration.withArgs('workbench').returns({ + get: sandbox.stub().withArgs('colorTheme', 'Default').returns('Dark+ (default dark)'), + } as any) + + const getEditorTheme = (service as any).getEditorTheme.bind(service) + const theme = getEditorTheme() + + assert.strictEqual(theme.fontSize, 16) + assert.strictEqual(theme.lingHeight, 40) // 2.5 * 16 + }) + + it('should generate CSS styles correctly', function () { + const theme = { + fontSize: 14, + lingHeight: 21, + foreground: 'rgba(212, 212, 212, 1)', + background: 'rgba(30, 30, 30, 1)', + diffAdded: 'rgba(231, 245, 231, 0.2)', + diffRemoved: 'rgba(255, 0, 0, 0.2)', + } + + const generateStyles = (service as any).generateStyles.bind(service) + const styles = generateStyles(theme) + + assert.ok(styles.includes('font-size: 14px')) + assert.ok(styles.includes('line-height: 21px')) + assert.ok(styles.includes('color: rgba(212, 212, 212, 1)')) + assert.ok(styles.includes('background-color: rgba(30, 30, 30, 1)')) + assert.ok(styles.includes('.diff-changed')) + assert.ok(styles.includes('.diff-removed')) + }) + }) + + describe('highlight ranges', function () { + it('should generate highlight ranges for word-level changes', function () { + const originalCode = ['function test() {', ' return 42;', '}'] + const afterCode = ['function test() {', ' return 100;', '}'] + const modifiedLines = new Map([[' return 42;', ' return 100;']]) + + const generateHighlightRanges = (service as any).generateHighlightRanges.bind(service) + const result = generateHighlightRanges(originalCode, afterCode, modifiedLines) + + // Should have ranges for the changed characters + assert.ok(result.removedRanges.length > 0) + assert.ok(result.addedRanges.length > 0) + + // Check that ranges are properly formatted + const removedRange = result.removedRanges[0] + assert.ok(removedRange.line >= 0) + assert.ok(removedRange.start >= 0) + assert.ok(removedRange.end > removedRange.start) + + const addedRange = result.addedRanges[0] + assert.ok(addedRange.line >= 0) + assert.ok(addedRange.start >= 0) + assert.ok(addedRange.end > addedRange.start) + }) + + it('should handle HTML escaping in highlight edits', function () { + const newLines = ['function test() {', ' return "";', '}'] + const highlightRanges = [{ line: 1, start: 10, end: 35 }] + + const getHighlightEdit = (service as any).getHighlightEdit.bind(service) + const result = getHighlightEdit(newLines, highlightRanges) + + assert.ok(result[1].includes('<script>')) + assert.ok(result[1].includes('</script>')) + assert.ok(result[1].includes('diff-changed')) + }) + }) + + describe('dimensions and positioning', function () { + it('should calculate dimensions correctly', function () { + const newLines = ['function test() {', ' return 42;', '}'] + const theme = { + fontSize: 14, + lingHeight: 21, + foreground: 'rgba(212, 212, 212, 1)', + background: 'rgba(30, 30, 30, 1)', + } + + const calculateDimensions = (service as any).calculateDimensions.bind(service) + const result = calculateDimensions(newLines, theme) + + assert.strictEqual(result.width, 287) + assert.strictEqual(result.height, 109) + assert.ok(result.height >= (newLines.length + 1) * theme.lingHeight) + }) + + it('should calculate position offset correctly', function () { + const originalLines = ['function test() {', ' return 42;', '}'] + const newLines = ['function test() {', ' return 100;', '}'] + const diffLines = [' return 100;'] + const theme = { + fontSize: 14, + lingHeight: 21, + foreground: 'rgba(212, 212, 212, 1)', + background: 'rgba(30, 30, 30, 1)', + } + + const calculatePosition = (service as any).calculatePosition.bind(service) + const result = calculatePosition(originalLines, newLines, diffLines, theme) + + assert.strictEqual(result.offset, 10) + assert.strictEqual(result.editStartLine, 1) + }) + }) + + describe('HTML content generation', function () { + it('should generate HTML content with proper structure', function () { + const diffLines = ['function test() {', ' return 42;', '}'] + const styles = '.code-container { color: white; }' + const offset = 20 + + const generateHtmlContent = (service as any).generateHtmlContent.bind(service) + const result = generateHtmlContent(diffLines, styles, offset) + + assert.ok(result.includes('
')) + assert.ok(result.includes(' + + +
+
+
${title}
+
${message}
+
+ +` + + const filePath = join(os.tmpdir(), `sagemaker-error-${randomUUID()}.html`) + await fs.writeFile(filePath, html, 'utf8') + await open(filePath) +} diff --git a/packages/core/src/awsService/sagemaker/detached-server/routes/getSession.ts b/packages/core/src/awsService/sagemaker/detached-server/routes/getSession.ts new file mode 100644 index 00000000000..0c9ce74ad30 --- /dev/null +++ b/packages/core/src/awsService/sagemaker/detached-server/routes/getSession.ts @@ -0,0 +1,59 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// Disabled: detached server files cannot import vscode. +/* eslint-disable aws-toolkits/no-console-log */ +import { IncomingMessage, ServerResponse } from 'http' +import { startSagemakerSession, parseArn, isSmusConnection } from '../utils' +import { resolveCredentialsFor } from '../credentials' +import url from 'url' +import { SageMakerServiceException } from '@amzn/sagemaker-client' +import { getVSCodeErrorText, getVSCodeErrorTitle, openErrorPage } from '../errorPage' + +export async function handleGetSession(req: IncomingMessage, res: ServerResponse): Promise { + const parsedUrl = url.parse(req.url || '', true) + const connectionIdentifier = parsedUrl.query.connection_identifier as string + + if (!connectionIdentifier) { + res.writeHead(400, { 'Content-Type': 'text/plain' }) + res.end(`Missing required query parameter: "connection_identifier" (${connectionIdentifier})`) + return + } + + let credentials + try { + credentials = await resolveCredentialsFor(connectionIdentifier) + } catch (err) { + console.error('Failed to resolve credentials:', err) + res.writeHead(500, { 'Content-Type': 'text/plain' }) + res.end((err as Error).message) + return + } + + const { region } = parseArn(connectionIdentifier) + // Detect if this is a SMUS connection for specialized error handling + const isSmus = await isSmusConnection(connectionIdentifier) + + try { + const session = await startSagemakerSession({ region, connectionIdentifier, credentials }) + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end( + JSON.stringify({ + SessionId: session.SessionId, + StreamUrl: session.StreamUrl, + TokenValue: session.TokenValue, + }) + ) + } catch (err) { + const error = err as SageMakerServiceException + console.error(`Failed to start SageMaker session for ${connectionIdentifier}:`, err) + const errorTitle = getVSCodeErrorTitle(error) + const errorText = getVSCodeErrorText(error, isSmus) + await openErrorPage(errorTitle, errorText) + res.writeHead(500, { 'Content-Type': 'text/plain' }) + res.end('Failed to start SageMaker session') + return + } +} diff --git a/packages/core/src/awsService/sagemaker/detached-server/routes/getSessionAsync.ts b/packages/core/src/awsService/sagemaker/detached-server/routes/getSessionAsync.ts new file mode 100644 index 00000000000..f8dad504067 --- /dev/null +++ b/packages/core/src/awsService/sagemaker/detached-server/routes/getSessionAsync.ts @@ -0,0 +1,70 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// Disabled: detached server files cannot import vscode. +/* eslint-disable aws-toolkits/no-console-log */ +import { IncomingMessage, ServerResponse } from 'http' +import url from 'url' +import { SessionStore } from '../sessionStore' +import { open, parseArn, readServerInfo } from '../utils' + +export async function handleGetSessionAsync(req: IncomingMessage, res: ServerResponse): Promise { + const parsedUrl = url.parse(req.url || '', true) + const connectionIdentifier = parsedUrl.query.connection_identifier as string + const requestId = parsedUrl.query.request_id as string + + if (!connectionIdentifier || !requestId) { + res.writeHead(400, { 'Content-Type': 'text/plain' }) + res.end( + `Missing required query parameters: "connection_identifier" (${connectionIdentifier}), "request_id" (${requestId})` + ) + return + } + + const store = new SessionStore() + + try { + const freshEntry = await store.getFreshEntry(connectionIdentifier, requestId) + + if (freshEntry) { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end( + JSON.stringify({ + SessionId: freshEntry.sessionId, + StreamUrl: freshEntry.url, + TokenValue: freshEntry.token, + }) + ) + return + } + + const status = await store.getStatus(connectionIdentifier, requestId) + if (status === 'pending') { + res.writeHead(204) + res.end() + return + } else if (status === 'not-started') { + const serverInfo = await readServerInfo() + const refreshUrl = await store.getRefreshUrl(connectionIdentifier) + const { spaceName } = parseArn(connectionIdentifier) + + const url = `${refreshUrl}/${encodeURIComponent(spaceName)}?remote_access_token_refresh=true&reconnect_identifier=${encodeURIComponent( + connectionIdentifier + )}&reconnect_request_id=${encodeURIComponent(requestId)}&reconnect_callback_url=${encodeURIComponent( + `http://localhost:${serverInfo.port}/refresh_token` + )}` + + await open(url) + res.writeHead(202, { 'Content-Type': 'text/plain' }) + res.end('Session is not ready yet. Please retry in a few seconds.') + await store.markPending(connectionIdentifier, requestId) + return + } + } catch (err) { + console.error('Error handling session async request:', err) + res.writeHead(500, { 'Content-Type': 'text/plain' }) + res.end('Unexpected error') + } +} diff --git a/packages/core/src/awsService/sagemaker/detached-server/routes/refreshToken.ts b/packages/core/src/awsService/sagemaker/detached-server/routes/refreshToken.ts new file mode 100644 index 00000000000..34152aa0423 --- /dev/null +++ b/packages/core/src/awsService/sagemaker/detached-server/routes/refreshToken.ts @@ -0,0 +1,46 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// Disabled: detached server files cannot import vscode. +/* eslint-disable aws-toolkits/no-console-log */ +import { IncomingMessage, ServerResponse } from 'http' +import url from 'url' +import { SessionStore } from '../sessionStore' + +export async function handleRefreshToken(req: IncomingMessage, res: ServerResponse): Promise { + const parsedUrl = url.parse(req.url || '', true) + const connectionIdentifier = parsedUrl.query.connection_identifier as string + const requestId = parsedUrl.query.request_id as string + const wsUrl = parsedUrl.query.ws_url as string + const token = parsedUrl.query.token as string + const sessionId = parsedUrl.query.session as string + + const store = new SessionStore() + + if (!connectionIdentifier || !requestId || !wsUrl || !token || !sessionId) { + res.writeHead(400, { 'Content-Type': 'text/plain' }) + res.end( + `Missing required parameters:\n` + + ` connection_identifier: ${connectionIdentifier ?? 'undefined'}\n` + + ` request_id: ${requestId ?? 'undefined'}\n` + + ` url: ${wsUrl ?? 'undefined'}\n` + + ` token: ${token ?? 'undefined'}\n` + + ` sessionId: ${sessionId ?? 'undefined'}` + ) + return + } + + try { + await store.setSession(connectionIdentifier, requestId, { sessionId, token, url: wsUrl }) + } catch (err) { + console.error('Failed to save session token:', err) + res.writeHead(500, { 'Content-Type': 'text/plain' }) + res.end('Failed to save session token') + return + } + + res.writeHead(200) + res.end('Session token refreshed successfully') +} diff --git a/packages/core/src/awsService/sagemaker/detached-server/server.ts b/packages/core/src/awsService/sagemaker/detached-server/server.ts new file mode 100644 index 00000000000..e785516146c --- /dev/null +++ b/packages/core/src/awsService/sagemaker/detached-server/server.ts @@ -0,0 +1,107 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// Disabled: detached server files cannot import vscode. +/* eslint-disable aws-toolkits/no-console-log */ +/* eslint-disable no-restricted-imports */ +import http, { IncomingMessage, ServerResponse } from 'http' +import { handleGetSession } from './routes/getSession' +import { handleGetSessionAsync } from './routes/getSessionAsync' +import { handleRefreshToken } from './routes/refreshToken' +import url from 'url' +import * as os from 'os' +import fs from 'fs' +import { execFile } from 'child_process' + +const pollInterval = 30 * 60 * 100 // 30 minutes + +const server = http.createServer((req: IncomingMessage, res: ServerResponse) => { + const parsedUrl = url.parse(req.url || '', true) + + switch (parsedUrl.pathname) { + case '/get_session': + return handleGetSession(req, res) + case '/get_session_async': + return handleGetSessionAsync(req, res) + case '/refresh_token': + return handleRefreshToken(req, res) + default: + res.writeHead(404, { 'Content-Type': 'text/plain' }) + res.end(`Not Found: ${req.url}`) + } +}) + +server.listen(0, '127.0.0.1', async () => { + const address = server.address() + if (address && typeof address === 'object') { + const port = address.port + const pid = process.pid + + console.log(`Detached server listening on http://127.0.0.1:${port} (pid: ${pid})`) + + const filePath = process.env.SAGEMAKER_LOCAL_SERVER_FILE_PATH + if (!filePath) { + throw new Error('SAGEMAKER_LOCAL_SERVER_FILE_PATH environment variable is not set') + } + + const data = { pid, port } + console.log(`Writing local endpoint info to ${filePath}`) + + fs.writeFileSync(filePath, JSON.stringify(data, undefined, 2), 'utf-8') + } else { + console.error('Failed to retrieve assigned port') + process.exit(0) + } + await monitorVSCodeAndExit() +}) + +function checkVSCodeWindows(): Promise { + return new Promise((resolve) => { + const platform = os.platform() + + if (platform === 'win32') { + execFile('tasklist', ['/FI', 'IMAGENAME eq Code.exe'], (err, stdout) => { + if (err) { + resolve(false) + return + } + resolve(/Code\.exe/i.test(stdout)) + }) + } else if (platform === 'darwin') { + execFile('ps', ['aux'], (err, stdout) => { + if (err) { + resolve(false) + return + } + + const found = stdout + .split('\n') + .some((line) => /Visual Studio Code( - Insiders)?\.app\/Contents\/MacOS\/Electron/.test(line)) + resolve(found) + }) + } else { + execFile('ps', ['-A', '-o', 'comm'], (err, stdout) => { + if (err) { + resolve(false) + return + } + + const found = stdout.split('\n').some((line) => /^(code(-insiders)?|electron)$/i.test(line.trim())) + resolve(found) + }) + } + }) +} + +async function monitorVSCodeAndExit() { + while (true) { + const found = await checkVSCodeWindows() + if (!found) { + console.log('No VSCode windows found. Shutting down detached server.') + process.exit(0) + } + await new Promise((r) => setTimeout(r, pollInterval)) + } +} diff --git a/packages/core/src/awsService/sagemaker/detached-server/sessionStore.ts b/packages/core/src/awsService/sagemaker/detached-server/sessionStore.ts new file mode 100644 index 00000000000..04098f68c89 --- /dev/null +++ b/packages/core/src/awsService/sagemaker/detached-server/sessionStore.ts @@ -0,0 +1,136 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SsmConnectionInfo } from '../types' +import { readMapping, writeMapping } from './utils' + +export type SessionStatus = 'pending' | 'fresh' | 'consumed' | 'not-started' + +export class SessionStore { + async getRefreshUrl(connectionId: string) { + const mapping = await readMapping() + + if (!mapping.deepLink) { + throw new Error('No deepLink mapping found') + } + + const entry = mapping.deepLink[connectionId] + if (!entry) { + throw new Error(`No mapping found for connectionId: "${connectionId}"`) + } + + if (!entry.refreshUrl) { + throw new Error(`No refreshUrl found for connectionId: "${connectionId}"`) + } + + return entry.refreshUrl + } + + async getFreshEntry(connectionId: string, requestId: string) { + const mapping = await readMapping() + + if (!mapping.deepLink) { + throw new Error('No deepLink mapping found') + } + + const entry = mapping.deepLink[connectionId] + if (!entry) { + throw new Error(`No mapping found for connectionId: "${connectionId}"`) + } + + const requests = entry.requests + const initialEntry = requests['initial-connection'] + if (initialEntry?.status === 'fresh') { + await this.markConsumed(connectionId, 'initial-connection') + return initialEntry + } + + const asyncEntry = requests[requestId] + if (asyncEntry?.status === 'fresh') { + delete requests[requestId] + await writeMapping(mapping) + return asyncEntry + } + + return undefined + } + + async getStatus(connectionId: string, requestId: string) { + const mapping = await readMapping() + + if (!mapping.deepLink) { + throw new Error('No deepLink mapping found') + } + const entry = mapping.deepLink[connectionId] + if (!entry) { + throw new Error(`No mapping found for connectionId: "${connectionId}"`) + } + + const status = entry.requests?.[requestId]?.status + return status ?? 'not-started' + } + + async markConsumed(connectionId: string, requestId: string) { + const mapping = await readMapping() + + if (!mapping.deepLink) { + throw new Error('No deepLink mapping found') + } + const entry = mapping.deepLink[connectionId] + if (!entry) { + throw new Error(`No mapping found for connectionId: "${connectionId}"`) + } + + const requests = entry.requests + if (!requests[requestId]) { + throw new Error(`No request entry found for requestId: "${requestId}"`) + } + + requests[requestId].status = 'consumed' + await writeMapping(mapping) + } + + async markPending(connectionId: string, requestId: string) { + const mapping = await readMapping() + + if (!mapping.deepLink) { + throw new Error('No deepLink mapping found') + } + const entry = mapping.deepLink[connectionId] + if (!entry) { + throw new Error(`No mapping found for connectionId: "${connectionId}"`) + } + + entry.requests[requestId] = { + sessionId: '', + token: '', + url: '', + status: 'pending', + } + + await writeMapping(mapping) + } + + async setSession(connectionId: string, requestId: string, ssmConnectionInfo: SsmConnectionInfo) { + const mapping = await readMapping() + + if (!mapping.deepLink) { + throw new Error('No deepLink mapping found') + } + const entry = mapping.deepLink[connectionId] + if (!entry) { + throw new Error(`No mapping found for connectionId: "${connectionId}"`) + } + + entry.requests[requestId] = { + sessionId: ssmConnectionInfo.sessionId, + token: ssmConnectionInfo.token, + url: ssmConnectionInfo.url, + status: ssmConnectionInfo.status ?? 'fresh', + } + + await writeMapping(mapping) + } +} diff --git a/packages/core/src/awsService/sagemaker/detached-server/utils.ts b/packages/core/src/awsService/sagemaker/detached-server/utils.ts new file mode 100644 index 00000000000..d4c963c40ff --- /dev/null +++ b/packages/core/src/awsService/sagemaker/detached-server/utils.ts @@ -0,0 +1,176 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// Disabled: detached server files cannot import vscode. +/* eslint-disable aws-toolkits/no-console-log */ +/* eslint-disable no-restricted-imports */ +import { ServerInfo } from '../types' +import { promises as fs } from 'fs' +import { SageMakerClient, StartSessionCommand } from '@amzn/sagemaker-client' +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') +const tempFilePath = `${mappingFilePath}.tmp` + +// Simple file lock to prevent concurrent writes +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 + */ +export async function readServerInfo(): Promise { + const filePath = process.env.SAGEMAKER_LOCAL_SERVER_FILE_PATH + if (!filePath) { + throw new Error('Environment variable SAGEMAKER_LOCAL_SERVER_FILE_PATH is not set') + } + + try { + const content = await fs.readFile(filePath, 'utf-8') + const data = JSON.parse(content) + if (typeof data.pid !== 'number' || typeof data.port !== 'number') { + throw new TypeError(`Invalid server info format in ${filePath}`) + } + return { pid: data.pid, port: data.port } + } catch (err: any) { + if (err.code === 'ENOENT') { + throw new Error(`Server info file not found at ${filePath}`) + } + throw new Error(`Failed to read server info: ${err.message ?? String(err)}`) + } +} + +/** + * Parses a SageMaker ARN to extract region, account ID, and space name. + * Supports formats like: + * arn:aws:sagemaker:::space// + * or sm_lc_arn:aws:sagemaker:::space__d-xxxx__ + * + * If the input is prefixed with an identifier (e.g. "sagemaker-user@"), the function will strip it. + * + * @param arn - The full SageMaker ARN string + * @returns An object containing the region, accountId, and spaceName + * @throws If the ARN format is invalid + */ +export function parseArn(arn: string): { region: string; accountId: string; spaceName: string } { + const cleanedArn = arn.includes('@') ? arn.split('@')[1] : arn + const regex = /^arn:aws:sagemaker:(?[^:]+):(?\d+):space[/:].+$/i + const match = cleanedArn.match(regex) + + if (!match?.groups) { + throw new Error(`Invalid SageMaker ARN format: "${arn}"`) + } + + // Extract space name from the end of the ARN (after the last forward slash) + const spaceName = cleanedArn.split('/').pop() + if (!spaceName) { + throw new Error(`Could not extract space name from ARN: "${arn}"`) + } + + return { + region: match.groups.region, + accountId: match.groups.account_id, + spaceName: spaceName, + } +} + +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, retryStrategy: startSessionRetryStrategy }) + const command = new StartSessionCommand({ ResourceIdentifier: connectionIdentifier }) + return client.send(command) +} + +/** + * Reads the mapping file and parses it as JSON. + * Throws if the file doesn't exist or is malformed. + */ +export async function readMapping() { + try { + const content = await fs.readFile(mappingFilePath, 'utf-8') + console.log(`Mapping file path: ${mappingFilePath}`) + return JSON.parse(content) + } catch (err) { + throw new Error(`Failed to read mapping file: ${err instanceof Error ? err.message : String(err)}`) + } +} + +/** + * Processes the write queue to ensure only one write operation happens at a time. + */ +async function processWriteQueue() { + if (isWriting || writeQueue.length === 0) { + return + } + + isWriting = true + try { + while (writeQueue.length > 0) { + const writeOperation = writeQueue.shift()! + await writeOperation() + } + } finally { + isWriting = false + } +} + +/** + * 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 + } +} + +/** + * 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. + */ +export async function writeMapping(mapping: SpaceMappings) { + return new Promise((resolve, reject) => { + const writeOperation = async () => { + try { + // Generate unique temp file name to avoid conflicts + const uniqueTempPath = `${tempFilePath}.${process.pid}.${Date.now()}` + + const json = JSON.stringify(mapping, undefined, 2) + await fs.writeFile(uniqueTempPath, json) + await fs.rename(uniqueTempPath, mappingFilePath) + resolve() + } catch (err) { + reject(new Error(`Failed to write mapping file: ${err instanceof Error ? err.message : String(err)}`)) + } + } + + writeQueue.push(writeOperation) + + // ProcessWriteQueue handles its own errors via individual operation callbacks + // eslint-disable-next-line @typescript-eslint/no-floating-promises + processWriteQueue() + }) +} diff --git a/packages/core/src/awsService/sagemaker/explorer/constants.ts b/packages/core/src/awsService/sagemaker/explorer/constants.ts new file mode 100644 index 00000000000..b9d7c3348b5 --- /dev/null +++ b/packages/core/src/awsService/sagemaker/explorer/constants.ts @@ -0,0 +1,20 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +export abstract class SagemakerConstants { + static readonly PlaceHolderMessage = '[No Sagemaker Spaces Found]' + static readonly EnableIdentityFilteringSetting = 'aws.sagemaker.studio.spaces.enableIdentityFiltering' + static readonly SelectedDomainUsersState = 'aws.sagemaker.selectedDomainUsers' + static readonly FilterPlaceholderKey = 'aws.filterSagemakerSpacesPlaceholder' + static readonly FilterPlaceholderMessage = 'Filter spaces by user profile or domain (unselect to hide)' + static readonly NoSpaceToFilter = 'No spaces to filter' + + static readonly IamUserArnRegex = /^arn:aws[a-z\-]*:iam::\d{12}:user\/?([a-zA-Z_0-9+=,.@\-_]+)$/ + static readonly IamSessionArnRegex = + /^arn:aws[a-z\-]*:sts::\d{12}:assumed-role\/?[a-zA-Z_0-9+=,.@\-_]+\/([a-zA-Z_0-9+=,.@\-_]+)$/ + static readonly IdentityCenterArnRegex = + /^arn:aws[a-z\-]*:sts::\d{12}:assumed-role\/?AWSReservedSSO[a-zA-Z_0-9+=,.@\-_]+\/([a-zA-Z_0-9+=,.@\-_]+)$/ + static readonly SpecialCharacterRegex = /[+=,.@\-_]/g +} diff --git a/packages/core/src/awsService/sagemaker/explorer/sagemakerParentNode.ts b/packages/core/src/awsService/sagemaker/explorer/sagemakerParentNode.ts new file mode 100644 index 00000000000..104eb7662a2 --- /dev/null +++ b/packages/core/src/awsService/sagemaker/explorer/sagemakerParentNode.ts @@ -0,0 +1,214 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { GetCallerIdentityCommandOutput } from '@aws-sdk/client-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 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: Partial = {} + public readonly pollingSet: PollingSet = new PollingSet(5000, this.updatePendingNodes.bind(this)) + + 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) + } + + 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() + } + + public async refreshNode(): Promise { + await this.clearChildren() + await vscode.commands.executeCommand('aws.refreshAwsExplorerNode', this) + } +} diff --git a/packages/core/src/awsService/sagemaker/explorer/sagemakerSpaceNode.ts b/packages/core/src/awsService/sagemaker/explorer/sagemakerSpaceNode.ts new file mode 100644 index 00000000000..1d93d325193 --- /dev/null +++ b/packages/core/src/awsService/sagemaker/explorer/sagemakerSpaceNode.ts @@ -0,0 +1,104 @@ +/*! + * 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 { AWSResourceNode } from '../../../shared/treeview/nodes/awsResourceNode' +import { AWSTreeNodeBase } from '../../../shared/treeview/nodes/awsTreeNodeBase' +import { SagemakerParentNode } from './sagemakerParentNode' +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 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.smSpace.getContext() + } + + public updateSpace(spaceApp: SagemakerSpaceApp) { + this.smSpace.updateSpace(spaceApp) + this.updateFromSpace() + if (this.isPending()) { + this.parent.trackPendingNode(this.DomainSpaceKey) + } + } + + 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.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() + this.updateFromSpace() + if (this.isPending()) { + this.parent.trackPendingNode(this.DomainSpaceKey) + } + } + + public get DomainSpaceKey(): string { + return this.spaceApp.DomainSpaceKey! + } + + public async refreshNode(): Promise { + await this.updateSpaceAppStatus() + await vscode.commands.executeCommand('aws.refreshAwsExplorerNode', this) + } +} + +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() + 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/model.ts b/packages/core/src/awsService/sagemaker/model.ts new file mode 100644 index 00000000000..e25e8791d4f --- /dev/null +++ b/packages/core/src/awsService/sagemaker/model.ts @@ -0,0 +1,247 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// 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 { createBoundProcess, ensureDependencies } from '../../shared/remoteSession' +import { SshConfig } from '../../shared/sshConfig' +import * as path from 'path' +import { persistLocalCredentials, persistSmusProjectCreds, persistSSMConnection } from './credentialMapping' +import * as os from 'os' +import _ from 'lodash' +import { fs } from '../../shared/fs/fs' +import * as nodefs from 'fs' +import { getSmSsmEnv, spawnDetachedServer } from './utils' +import { getLogger } from '../../shared/logger/logger' +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' + +const logger = getLogger('sagemaker') + +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, + '/home/sagemaker-user', + remoteEnv.vscPath, + 'sagemaker-user' + ) + } catch (err) { + getLogger().info( + `sm:OpenRemoteConnect: Unable to connect to target space with arn: ${await node.getAppArn()} error: ${err}` + ) + } +} + +export async function prepareDevEnvConnection( + spaceArn: string, + ctx: vscode.ExtensionContext, + connectionType: string, + isSMUS: boolean, + node: SagemakerSpaceNode | SagemakerUnifiedStudioSpaceNode | undefined, + session?: string, + wsUrl?: string, + token?: string, + domain?: string, + appType?: string +) { + const remoteLogger = configureRemoteConnectionLogger() + const { ssm, vsc, ssh } = (await ensureDependencies()).unwrap() + + // Check timeout setting for remote SSH connections + const remoteSshConfig = vscode.workspace.getConfiguration('remote.SSH') + const current = remoteSshConfig.get('connectTimeout') + if (typeof current === 'number' && current < 120) { + await remoteSshConfig.update('connectTimeout', 120, vscode.ConfigurationTarget.Global) + void vscode.window.showInformationMessage( + 'Updated "remote.SSH.connectTimeout" to 120 seconds to improve stability.' + ) + } + + const hostnamePrefix = connectionType + const hostname = `${hostnamePrefix}_${spaceArn.replace(/\//g, '__').replace(/:/g, '_._')}` + + // save space credential mapping + if (connectionType === 'sm_lc') { + if (!isSMUS) { + await persistLocalCredentials(spaceArn) + } else { + await persistSmusProjectCreds(spaceArn, node as SagemakerUnifiedStudioSpaceNode) + } + } else if (connectionType === 'sm_dl') { + await persistSSMConnection(spaceArn, domain ?? '', session, wsUrl, token, appType) + } + + await startLocalServer(ctx) + await removeKnownHost(hostname) + + const sshConfig = 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}`) + throw err + } + + // set envirionment variables + const vars = 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, + rejectOnErrorCode: true, + }) + + return { + hostname, + envProvider, + sshPath: ssh, + vscPath: vsc, + SessionProcess, + } +} + +export function configureRemoteConnectionLogger() { + const logPrefix = 'sagemaker:' + const logger = (data: string) => getLogger().info(`${logPrefix}: ${data}`) + return logger +} + +export async function startLocalServer(ctx: vscode.ExtensionContext) { + const storagePath = ctx.globalStorageUri.fsPath + const serverPath = ctx.asAbsolutePath(path.join('dist/src/awsService/sagemaker/detached-server/', 'server.js')) + const outLog = path.join(storagePath, 'sagemaker-local-server.out.log') + const errLog = path.join(storagePath, 'sagemaker-local-server.err.log') + const infoFilePath = path.join(storagePath, 'sagemaker-local-server-info.json') + + logger.info(`sagemaker-local-server.*.log at ${storagePath}`) + + const customEndpoint = DevSettings.instance.get('endpoints', {})['sagemaker'] + + await stopLocalServer(ctx) + + const child = spawnDetachedServer(process.execPath, [serverPath], { + cwd: path.dirname(serverPath), + detached: true, + stdio: ['ignore', nodefs.openSync(outLog, 'a'), nodefs.openSync(errLog, 'a')], + env: { + ...process.env, + SAGEMAKER_ENDPOINT: customEndpoint, + SAGEMAKER_LOCAL_SERVER_FILE_PATH: infoFilePath, + }, + }) + + child.unref() + + // Wait for the info file to appear (timeout after 10 seconds) + const maxRetries = 20 + const delayMs = 500 + for (let i = 0; i < maxRetries; i++) { + if (await fs.existsFile(infoFilePath)) { + logger.debug('Detected server info file.') + return + } + await sleep(delayMs) + } + + throw new ToolkitError(`Timed out waiting for local server info file: ${infoFilePath}`) +} + +interface LocalServerInfo { + pid: number + port: string +} + +export async function stopLocalServer(ctx: vscode.ExtensionContext): Promise { + const infoFilePath = path.join(ctx.globalStorageUri.fsPath, 'sagemaker-local-server-info.json') + + if (!(await fs.existsFile(infoFilePath))) { + logger.debug('no server info file found. nothing to stop.') + return + } + + let pid: number | undefined + try { + const content = await fs.readFileText(infoFilePath) + const infoJson = JSON.parse(content) as LocalServerInfo + pid = infoJson.pid + } catch (err: any) { + throw ToolkitError.chain(err, 'failed to parse server info file') + } + + if (typeof pid === 'number' && !isNaN(pid)) { + try { + process.kill(pid) + logger.debug(`stopped local server with PID ${pid}`) + } catch (err: any) { + if (err.code === 'ESRCH') { + logger.warn(`no process found with PID ${pid}. It may have already exited.`) + } else { + throw ToolkitError.chain(err, 'failed to stop local server') + } + } + } else { + logger.warn('no valid PID found in info file.') + } + + try { + await fs.delete(infoFilePath) + logger.debug('removed server info file.') + } catch (err: any) { + logger.warn(`could not delete info file: ${err.message ?? err}`) + } +} + +export async function removeKnownHost(hostname: string): Promise { + const knownHostsPath = path.join(os.homedir(), '.ssh', 'known_hosts') + + if (!(await fs.existsFile(knownHostsPath))) { + logger.warn(`known_hosts not found at ${knownHostsPath}`) + return + } + + let lines: string[] + try { + const content = await fs.readFileText(knownHostsPath) + lines = content.split('\n') + } catch (err: any) { + throw ToolkitError.chain(err, 'Failed to read known_hosts file') + } + + 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 { + await fs.writeFile(knownHostsPath, updatedLines.join('\n'), { atomic: true }) + logger.debug(`Removed '${hostname}' from known_hosts`) + } catch (err: any) { + throw ToolkitError.chain(err, 'Failed to write updated known_hosts file') + } + } +} diff --git a/packages/core/src/awsService/sagemaker/remoteUtils.ts b/packages/core/src/awsService/sagemaker/remoteUtils.ts new file mode 100644 index 00000000000..9ff8d8ca177 --- /dev/null +++ b/packages/core/src/awsService/sagemaker/remoteUtils.ts @@ -0,0 +1,48 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { fs } from '../../shared/fs/fs' +import { SagemakerClient } from '../../shared/clients/sagemaker' +import { RemoteAppMetadata } from './utils' +import { getLogger } from '../../shared/logger/logger' +import { parseArn } from './detached-server/utils' + +export async function getRemoteAppMetadata(): Promise { + try { + const metadataPath = '/opt/ml/metadata/resource-metadata.json' + const metadataContent = await fs.readFileText(metadataPath) + const metadata = JSON.parse(metadataContent) + + const domainId = metadata.DomainId + const spaceName = metadata.SpaceName + + if (!domainId || !spaceName) { + throw new Error('DomainId or SpaceName not found in metadata file') + } + + const { region } = parseArn(metadata.ResourceArn) + + const client = new SagemakerClient(region) + const spaceDetails = await client.describeSpace({ DomainId: domainId, SpaceName: spaceName }) + + const userProfileName = spaceDetails.OwnershipSettings?.OwnerUserProfileName + + if (!userProfileName) { + throw new Error('OwnerUserProfileName not found in space details') + } + + return { + DomainId: domainId, + UserProfileName: userProfileName, + } + } catch (error) { + const logger = getLogger() + logger.error(`getRemoteAppMetadata: Failed to read metadata file, using fallback values: ${error}`) + return { + DomainId: '', + UserProfileName: '', + } + } +} diff --git a/packages/core/src/awsService/sagemaker/sagemakerSpace.ts b/packages/core/src/awsService/sagemaker/sagemakerSpace.ts new file mode 100644 index 00000000000..14ac03d9c0e --- /dev/null +++ b/packages/core/src/awsService/sagemaker/sagemakerSpace.ts @@ -0,0 +1,229 @@ +/*! + * 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' + +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) { + 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() !== 'Running' && this.getStatus() !== '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, + }) + + const app = await this.client.describeApp({ + DomainId: this.spaceApp.DomainId, + AppName: this.spaceApp.App?.AppName, + AppType: this.spaceApp?.SpaceSettingsSummary?.AppType, + 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 ?? '-' + if (this.isSMUSSpace) { + return `**Space:** ${spaceName} \n\n**Application:** ${appType} \n\n**Instance Type:** ${instanceType}` + } + 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 === 'Running' && this.spaceApp.SpaceSettingsSummary?.RemoteAccess === 'ENABLED') { + return 'awsSagemakerSpaceRunningRemoteEnabledNode' + } else if (status === 'Running' && this.spaceApp.SpaceSettingsSummary?.RemoteAccess === 'DISABLED') { + return 'awsSagemakerSpaceRunningRemoteDisabledNode' + } else if (status === 'Running' && this.isSMUSSpace) { + return 'awsSagemakerSpaceRunningNode' + } 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' + } + 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 { + const logger = getLogger() + 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 new file mode 100644 index 00000000000..82f4d4f92d6 --- /dev/null +++ b/packages/core/src/awsService/sagemaker/types.ts @@ -0,0 +1,32 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +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'; smusProjectId: string } + +export interface DeeplinkSession { + requests: Record + refreshUrl?: string +} + +export interface SsmConnectionInfo { + sessionId: string + url: string + token: string + status?: 'fresh' | 'consumed' | 'pending' +} + +export interface ServerInfo { + pid: number + port: number +} diff --git a/packages/core/src/awsService/sagemaker/uriHandlers.ts b/packages/core/src/awsService/sagemaker/uriHandlers.ts new file mode 100644 index 00000000000..6f1143d9054 --- /dev/null +++ b/packages/core/src/awsService/sagemaker/uriHandlers.ts @@ -0,0 +1,43 @@ +/*! + * 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 { deeplinkConnect } from './commands' +import { ExtContext } from '../../shared/extensions' +import { telemetry } from '../../shared/telemetry/telemetry' + +export function register(ctx: ExtContext) { + async function connectHandler(params: ReturnType) { + await telemetry.sagemaker_deeplinkConnect.run(async () => { + await deeplinkConnect( + ctx, + params.connection_identifier, + params.session, + `${params.ws_url}&cell-number=${params['cell-number']}`, + params.token, + params.domain, + params.app_type + ) + }) + } + + return vscode.Disposable.from(ctx.uriHandler.onPath('/connect/sagemaker', connectHandler, parseConnectParams)) +} + +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') + + return { ...requiredParams, ...optionalParams } +} diff --git a/packages/core/src/awsService/sagemaker/utils.ts b/packages/core/src/awsService/sagemaker/utils.ts new file mode 100644 index 00000000000..33cc5880bee --- /dev/null +++ b/packages/core/src/awsService/sagemaker/utils.ts @@ -0,0 +1,156 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as cp from 'child_process' // eslint-disable-line no-restricted-imports +import * as path from 'path' +import { AppStatus, SpaceStatus } from '@aws-sdk/client-sagemaker' +import { SagemakerSpaceApp } from '../../shared/clients/sagemaker' +import { sshLogFileLocation } from '../../shared/sshConfig' +import { fs } from '../../shared/fs/fs' +import { getLogger } from '../../shared/logger/logger' + +export const DomainKeyDelimiter = '__' + +export function getDomainSpaceKey(domainId: string, spaceName: string): string { + return `${domainId}${DomainKeyDelimiter}${spaceName}` +} + +export function getDomainUserProfileKey(domainId: string, userProfileName: string): string { + return `${domainId}${DomainKeyDelimiter}${userProfileName}` +} + +export function generateSpaceStatus(spaceStatus?: string, appStatus?: string) { + if ( + spaceStatus === SpaceStatus.Failed || + spaceStatus === SpaceStatus.Delete_Failed || + spaceStatus === SpaceStatus.Update_Failed || + (appStatus === AppStatus.Failed && spaceStatus !== SpaceStatus.Updating) + ) { + return 'Failed' + } + + if (spaceStatus === SpaceStatus.InService && appStatus === AppStatus.InService) { + return 'Running' + } + + if (spaceStatus === SpaceStatus.InService && appStatus === AppStatus.Pending) { + return 'Starting' + } + + if (spaceStatus === SpaceStatus.Updating) { + return 'Updating' + } + + if (spaceStatus === SpaceStatus.InService && appStatus === AppStatus.Deleting) { + return 'Stopping' + } + + if (spaceStatus === SpaceStatus.InService && (appStatus === AppStatus.Deleted || !appStatus)) { + return 'Stopped' + } + + if (spaceStatus === SpaceStatus.Deleting) { + return 'Deleting' + } + + return 'Unknown' +} + +export interface RemoteAppMetadata { + DomainId: string + UserProfileName: string +} + +export function getSpaceAppsForUserProfile( + spaceApps: SagemakerSpaceApp[], + userProfilePrefix: string, + domainId?: string +): string[] { + return spaceApps.reduce((result: string[], app: SagemakerSpaceApp) => { + if (app.OwnershipSettingsSummary?.OwnerUserProfileName?.startsWith(userProfilePrefix)) { + if (domainId && app.DomainId !== domainId) { + return result + } + result.push( + getDomainUserProfileKey(app.DomainId || '', app.OwnershipSettingsSummary?.OwnerUserProfileName || '') + ) + } + + return result + }, [] as string[]) +} + +export function getSmSsmEnv(ssmPath: string, sagemakerLocalServerPath: string): NodeJS.ProcessEnv { + return Object.assign( + { + AWS_SSM_CLI: ssmPath, + SAGEMAKER_LOCAL_SERVER_FILE_PATH: sagemakerLocalServerPath, + LOF_FILE_LOCATION: sshLogFileLocation('sagemaker', 'blah'), + }, + process.env + ) +} + +export function spawnDetachedServer(...args: Parameters) { + return cp.spawn(...args) +} + +export const ActivityCheckInterval = 60000 + +/** + * Updates the idle file with the current timestamp + */ +export async function updateIdleFile(idleFilePath: string): Promise { + try { + const timestamp = new Date().toISOString() + await fs.writeFile(idleFilePath, timestamp) + } catch (error) { + getLogger().error(`Failed to update SMAI idle file: ${error}`) + } +} + +/** + * Checks for terminal activity by reading the /dev/pts directory and comparing modification times of the files. + * + * The /dev/pts directory is used in Unix-like operating systems to represent pseudo-terminal (PTY) devices. + * Each active terminal session is assigned a PTY device. These devices are represented as files within the /dev/pts directory. + * When a terminal session has activity, such as when a user inputs commands or output is written to the terminal, + * the modification time (mtime) of the corresponding PTY device file is updated. By monitoring the modification + * times of the files in the /dev/pts directory, we can detect terminal activity. + * + * If activity is detected (i.e., if any PTY device file was modified within the CHECK_INTERVAL), this function + * updates the last activity timestamp. + */ +export async function checkTerminalActivity(idleFilePath: string): Promise { + try { + const files = await fs.readdir('/dev/pts') + const now = Date.now() + + for (const [fileName] of files) { + const filePath = path.join('/dev/pts', fileName) + try { + const stats = await fs.stat(filePath) + const mtime = new Date(stats.mtime).getTime() + if (now - mtime < ActivityCheckInterval) { + await updateIdleFile(idleFilePath) + return + } + } catch (err) { + getLogger().error(`Error reading file stats:`, err) + } + } + } catch (err) { + getLogger().error(`Error reading /dev/pts directory:`, err) + } +} + +/** + * Starts monitoring terminal activity by setting an interval to check for activity in the /dev/pts directory. + */ +export function startMonitoringTerminalActivity(idleFilePath: string): NodeJS.Timeout { + return setInterval(async () => { + await checkTerminalActivity(idleFilePath) + }, ActivityCheckInterval) +} diff --git a/packages/core/src/awsexplorer/activation.ts b/packages/core/src/awsexplorer/activation.ts index f904658fcaa..ec4c23ccd79 100644 --- a/packages/core/src/awsexplorer/activation.ts +++ b/packages/core/src/awsexplorer/activation.ts @@ -36,6 +36,7 @@ import { TreeNode } from '../shared/treeview/resourceTreeDataProvider' import { getSourceNode } from '../shared/utilities/treeNodeUtils' import { openAwsCFNConsoleCommand, openAwsConsoleCommand } from '../shared/awsConsole' import { StackNameNode } from '../awsService/appBuilder/explorer/nodes/deployedStack' +import { LambdaFunctionNodeDecorationProvider } from '../lambda/explorer/lambdaFunctionNodeDecorationProvider' /** * Activates the AWS Explorer UI and related functionality. @@ -65,7 +66,10 @@ export async function activate(args: { telemetry.aws_expandExplorerNode.emit({ serviceType: element.element.serviceId, result: 'Succeeded' }) } }) - globals.context.subscriptions.push(view) + globals.context.subscriptions.push( + view, + vscode.window.registerFileDecorationProvider(LambdaFunctionNodeDecorationProvider.getInstance()) + ) await registerAwsExplorerCommands(args.context, awsExplorer, args.toolkitOutputChannel) diff --git a/packages/core/src/awsexplorer/regionNode.ts b/packages/core/src/awsexplorer/regionNode.ts index 10e8d975fe8..d78bcbec2a4 100644 --- a/packages/core/src/awsexplorer/regionNode.ts +++ b/packages/core/src/awsexplorer/regionNode.ts @@ -32,6 +32,8 @@ import { getEcsRootNode } from '../awsService/ecs/model' import { compareTreeItems, TreeShim } from '../shared/treeview/utils' import { Ec2ParentNode } from '../awsService/ec2/explorer/ec2ParentNode' import { Ec2Client } from '../shared/clients/ec2' +import { SagemakerParentNode } from '../awsService/sagemaker/explorer/sagemakerParentNode' +import { SagemakerClient } from '../shared/clients/sagemaker' interface ServiceNode { allRegions?: boolean @@ -96,6 +98,10 @@ const serviceCandidates: ServiceNode[] = [ serviceId: 's3', createFn: (regionCode: string) => new S3Node(new S3Client(regionCode)), }, + { + serviceId: 'api.sagemaker', + createFn: (regionCode: string) => new SagemakerParentNode(regionCode, new SagemakerClient(regionCode)), + }, { serviceId: 'schemas', createFn: (regionCode: string) => new SchemasNode(new DefaultSchemaClient(regionCode)), 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 e52e08bb98b..e037657958d 100644 --- a/packages/core/src/codewhisperer/activation.ts +++ b/packages/core/src/codewhisperer/activation.ts @@ -5,8 +5,6 @@ import * as vscode from 'vscode' import * as nls from 'vscode-nls' -import { getTabSizeSetting } from '../shared/utilities/editorUtilities' -import * as EditorContext from './util/editorContext' import * as CodeWhispererConstants from './models/constants' import { CodeSuggestionsState, @@ -16,7 +14,6 @@ import { CodeScanIssue, CodeIssueGroupingStrategyState, } from './models/model' -import { acceptSuggestion } from './commands/onInlineAcceptance' import { CodeWhispererSettings } from './util/codewhispererSettings' import { ExtContext } from '../shared/extensions' import { CodeWhispererTracker } from './tracker/codewhispererTracker' @@ -26,6 +23,7 @@ import { enableCodeSuggestions, toggleCodeSuggestions, showReferenceLog, + showLogs, showSecurityScan, showLearnMore, showSsoSignIn, @@ -51,7 +49,6 @@ import { regenerateFix, ignoreAllIssues, focusIssue, - showExploreAgentsView, showCodeIssueGroupingQuickPick, selectRegionProfileCommand, } from './commands/basicCommands' @@ -64,20 +61,16 @@ import { updateSecurityDiagnosticCollection, } from './service/diagnosticsProvider' import { SecurityPanelViewProvider, openEditorAtRange } from './views/securityPanelViewProvider' -import { RecommendationHandler } from './service/recommendationHandler' import { Commands, registerCommandErrorHandler, registerDeclaredCommands } from '../shared/vscode/commands2' -import { InlineCompletionService, refreshStatusBar } from './service/inlineCompletionService' -import { isInlineCompletionEnabled } from './util/commonUtil' +import { refreshStatusBar } from './service/statusBar' import { AuthUtil } from './util/authUtil' import { ImportAdderProvider } from './service/importAdderProvider' -import { TelemetryHelper } from './util/telemetryHelper' import { openUrl } from '../shared/utilities/vsCodeUtils' import { notifyNewCustomizations, onProfileChangedListener } from './util/customizationUtil' import { CodeWhispererCommandBackend, CodeWhispererCommandDeclarations } from './commands/gettingStartedPageCommands' import { SecurityIssueHoverProvider } from './service/securityIssueHoverProvider' import { SecurityIssueCodeActionProvider } from './service/securityIssueCodeActionProvider' import { listCodeWhispererCommands } from './ui/statusBarMenu' -import { Container } from './service/serviceContainer' import { debounceStartSecurityScan } from './commands/startSecurityScan' import { securityScanLanguageContext } from './util/securityScanLanguageContext' import { registerWebviewErrorHandler } from '../webviews/server' @@ -137,7 +130,6 @@ export async function activate(context: ExtContext): Promise { const client = new codewhispererClient.DefaultCodeWhispererClient() // Service initialization - const container = Container.instance ReferenceInlineProvider.instance ImportAdderProvider.instance @@ -149,10 +141,6 @@ export async function activate(context: ExtContext): Promise { * Configuration change */ vscode.workspace.onDidChangeConfiguration(async (configurationChangeEvent) => { - if (configurationChangeEvent.affectsConfiguration('editor.tabSize')) { - EditorContext.updateTabSize(getTabSizeSetting()) - } - if (configurationChangeEvent.affectsConfiguration('amazonQ.showCodeWithReferences')) { ReferenceLogViewProvider.instance.update() if (auth.isEnterpriseSsoInUse()) { @@ -169,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( @@ -215,20 +188,21 @@ export async function activate(context: ExtContext): Promise { await openSettings('amazonQ') } }), - Commands.register('aws.amazonq.refreshAnnotation', async (forceProceed: boolean) => { - telemetry.record({ - traceId: TelemetryHelper.instance.traceId, - }) - - const editor = vscode.window.activeTextEditor - if (editor) { - if (forceProceed) { - await container.lineAnnotationController.refresh(editor, 'codewhisperer', true) - } else { - await container.lineAnnotationController.refresh(editor, 'codewhisperer') - } - } - }), + // TODO port this to lsp + // Commands.register('aws.amazonq.refreshAnnotation', async (forceProceed: boolean) => { + // telemetry.record({ + // traceId: TelemetryHelper.instance.traceId, + // }) + + // const editor = vscode.window.activeTextEditor + // if (editor) { + // if (forceProceed) { + // await container.lineAnnotationController.refresh(editor, 'codewhisperer', true) + // } else { + // await container.lineAnnotationController.refresh(editor, 'codewhisperer') + // } + // } + // }), // show introduction showIntroduction.register(), // toggle code suggestions @@ -300,29 +274,17 @@ export async function activate(context: ExtContext): Promise { // notify new customizations notifyNewCustomizationsCmd.register(), selectRegionProfileCommand.register(), - /** - * On recommendation acceptance - */ - acceptSuggestion.register(context), // direct CodeWhisperer connection setup with customization connectWithCustomization.register(), - // on text document close. - vscode.workspace.onDidCloseTextDocument((e) => { - if (isInlineCompletionEnabled() && e.uri.fsPath !== InlineCompletionService.instance.filePath()) { - return - } - RecommendationHandler.instance.reportUserDecisions(-1) - }), - vscode.languages.registerHoverProvider( [...CodeWhispererConstants.platformLanguageIds], ReferenceHoverProvider.instance ), vscode.window.registerWebviewViewProvider(ReferenceLogViewProvider.viewType, ReferenceLogViewProvider.instance), showReferenceLog.register(), - showExploreAgentsView.register(), + showLogs.register(), vscode.languages.registerCodeLensProvider( [...CodeWhispererConstants.platformLanguageIds], ReferenceInlineProvider.instance @@ -473,7 +435,6 @@ export async function activate(context: ExtContext): Promise { }) await Commands.tryExecute('aws.amazonq.refreshConnectionCallback') - container.ready() function setSubscriptionsForCodeIssues() { context.extensionContext.subscriptions.push( @@ -511,7 +472,6 @@ export async function activate(context: ExtContext): Promise { } export async function shutdown() { - RecommendationHandler.instance.reportUserDecisions(-1) await CodeWhispererTracker.getTracker().shutdown() } diff --git a/packages/core/src/codewhisperer/client/codewhisperer.ts b/packages/core/src/codewhisperer/client/codewhisperer.ts index 35f699b24c2..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, } @@ -262,7 +263,7 @@ export class DefaultCodeWhispererClient { /** * @description Use this function to get the status of the code transformation. We should * be polling this function periodically to get updated results. When this function - * returns COMPLETED we know the transformation is done. + * returns PARTIALLY_COMPLETED or COMPLETED we know the transformation is done. */ public async codeModernizerGetCodeTransformation( request: CodeWhispererUserClient.GetTransformationRequest @@ -272,15 +273,15 @@ export class DefaultCodeWhispererClient { } /** - * @description After the job has been PAUSED we need to get user intervention. Once that user - * intervention has been handled we can resume the transformation job. + * @description During client-side build, or after the job has been PAUSED we need to get user intervention. + * Once that user action has been handled we can resume the transformation job. * @params transformationJobId - String id returned from StartCodeTransformationResponse * @params userActionStatus - String to determine what action the user took, if any. */ public async codeModernizerResumeTransformation( request: CodeWhispererUserClient.ResumeTransformationRequest ): Promise> { - return (await this.createUserSdkClient()).resumeTransformation(request).promise() + return (await this.createUserSdkClient(8)).resumeTransformation(request).promise() } /** 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/basicCommands.ts b/packages/core/src/codewhisperer/commands/basicCommands.ts index a24c6ade704..ec1b5ae6e63 100644 --- a/packages/core/src/codewhisperer/commands/basicCommands.ts +++ b/packages/core/src/codewhisperer/commands/basicCommands.ts @@ -12,7 +12,6 @@ import { DefaultCodeWhispererClient } from '../client/codewhisperer' import { confirmStopSecurityScan, startSecurityScan } from './startSecurityScan' import { SecurityPanelViewProvider } from '../views/securityPanelViewProvider' import { - codeFixState, CodeScanIssue, CodeScansState, codeScanState, @@ -50,7 +49,7 @@ import { once } from '../../shared/utilities/functionUtils' import { focusAmazonQPanel } from '../../codewhispererChat/commands/registerCommands' import { removeDiagnostic } from '../service/diagnosticsProvider' import { SsoAccessTokenProvider } from '../../auth/sso/ssoAccessTokenProvider' -import { ToolkitError, getErrorMsg, getTelemetryReason, getTelemetryReasonDesc } from '../../shared/errors' +import { ToolkitError, getTelemetryReason, getTelemetryReasonDesc } from '../../shared/errors' import { isRemoteWorkspace } from '../../shared/vscode/env' import { isBuilderIdConnection } from '../../auth/connection' import globals from '../../shared/extensionGlobals' @@ -61,8 +60,6 @@ import { SecurityIssueProvider } from '../service/securityIssueProvider' import { CodeWhispererSettings } from '../util/codewhispererSettings' import { closeDiff, getPatchedCode } from '../../shared/utilities/diffUtils' import { insertCommentAboveLine } from '../../shared/utilities/commentUtils' -import { startCodeFixGeneration } from './startCodeFixGeneration' -import { DefaultAmazonQAppInitContext } from '../../amazonq/apps/initContext' import path from 'path' import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' import { parsePatch } from 'diff' @@ -145,21 +142,33 @@ export const showReferenceLog = Commands.declare( if (_ !== placeholder) { source = 'ellipsesMenu' } - await vscode.commands.executeCommand('workbench.view.extension.aws-codewhisperer-reference-log') + await vscode.commands.executeCommand(`${ReferenceLogViewProvider.viewType}.focus`) } ) -export const showExploreAgentsView = Commands.declare( - { id: 'aws.amazonq.exploreAgents', compositeKey: { 1: 'source' } }, +export const showLogs = Commands.declare( + { id: 'aws.amazonq.showLogs', compositeKey: { 1: 'source' } }, () => async (_: VsCodeCommandArg, source: CodeWhispererSource) => { if (_ !== placeholder) { source = 'ellipsesMenu' } - DefaultAmazonQAppInitContext.instance.getAppsToWebViewMessagePublisher().publish({ - sender: 'amazonqCore', - command: 'showExploreAgentsView', - }) + // Show warning message without buttons - just informational + void vscode.window.showWarningMessage( + 'Log files may contain sensitive information such as account IDs, resource names, and other data. Be careful when sharing these logs.' + ) + + // Get the log directory path + const logFolderPath = globals.context.logUri?.fsPath + const path = require('path') + const logFilePath = path.join(logFolderPath, 'Amazon Q Logs.log') + if (logFilePath) { + // Open the log directory in the OS file explorer directly + await vscode.commands.executeCommand('revealFileInOS', vscode.Uri.file(logFilePath)) + } else { + // Fallback: show error if log path is not available + void vscode.window.showErrorMessage('Log location not available.') + } } ) @@ -368,10 +377,6 @@ export const openSecurityIssuePanel = Commands.declare( const targetIssue: CodeScanIssue = issue instanceof IssueItem ? issue.issue : issue const targetFilePath: string = issue instanceof IssueItem ? issue.filePath : filePath await showSecurityIssueWebview(context.extensionContext, targetIssue, targetFilePath) - - if (targetIssue.suggestedFixes.length === 0) { - await generateFix.execute(targetIssue, targetFilePath, 'webview', true, false) - } telemetry.codewhisperer_codeScanIssueViewDetails.emit({ findingId: targetIssue.findingId, detectorId: targetIssue.detectorId, @@ -640,6 +645,12 @@ const registerToolkitApiCallbackOnce = once(() => { export const registerToolkitApiCallback = Commands.declare( { id: 'aws.amazonq.refreshConnectionCallback' }, () => async (toolkitApi?: any) => { + // Early return if already registered to avoid duplicate work + if (_toolkitApi) { + getLogger().debug('Toolkit API callback already registered, skipping') + return + } + // While the Q/CW exposes an API for the Toolkit to register callbacks on auth changes, // we need to do it manually here because the Toolkit would have been unable to call // this API if the Q/CW extension started afterwards (and this code block is running). @@ -685,116 +696,13 @@ export const generateFix = Commands.declare( { id: 'aws.amazonq.security.generateFix' }, (client: DefaultCodeWhispererClient, context: ExtContext) => async ( - issue: CodeScanIssue | IssueItem | undefined, + issueItem: IssueItem, filePath: string, source: Component, refresh: boolean = false, shouldOpenSecurityIssuePanel: boolean = true ) => { - const targetIssue: CodeScanIssue | undefined = issue instanceof IssueItem ? issue.issue : issue - const targetFilePath: string = issue instanceof IssueItem ? issue.filePath : filePath - const targetSource: Component = issue instanceof IssueItem ? 'tree' : source - if (!targetIssue) { - return - } - if (targetIssue.ruleId === CodeWhispererConstants.sasRuleId) { - getLogger().warn('GenerateFix is not available for SAS findings.') - return - } - await telemetry.codewhisperer_codeScanIssueGenerateFix.run(async () => { - try { - if (shouldOpenSecurityIssuePanel) { - await vscode.commands - .executeCommand('aws.amazonq.openSecurityIssuePanel', targetIssue, targetFilePath) - .then(undefined, (e) => { - getLogger().error('Failed to open security issue panel: %s', e.message) - }) - } - await updateSecurityIssueWebview({ - isGenerateFixLoading: true, - // eslint-disable-next-line unicorn/no-null - generateFixError: null, - context: context.extensionContext, - filePath: targetFilePath, - shouldRefreshView: false, - }) - - codeFixState.setToRunning() - let hasSuggestedFix = false - const { suggestedFix, jobId } = await startCodeFixGeneration( - client, - targetIssue, - targetFilePath, - targetIssue.findingId - ) - // redact the fix if the user disabled references and there is a reference - if ( - // TODO: enable references later for scans - // !CodeWhispererSettings.instance.isSuggestionsWithCodeReferencesEnabled() && - suggestedFix?.references && - suggestedFix?.references?.length > 0 - ) { - getLogger().debug( - `Received fix with reference and user settings disallow references. Job ID: ${jobId}` - ) - // TODO: re-enable notifications once references published - // void vscode.window.showInformationMessage( - // 'Your settings do not allow code generation with references.' - // ) - hasSuggestedFix = false - } else { - hasSuggestedFix = suggestedFix !== undefined - } - telemetry.record({ includesFix: hasSuggestedFix }) - const updatedIssue: CodeScanIssue = { - ...targetIssue, - fixJobId: jobId, - suggestedFixes: - hasSuggestedFix && suggestedFix - ? [ - { - code: suggestedFix.codeDiff, - description: suggestedFix.description ?? '', - references: suggestedFix.references, - }, - ] - : [], - } - await updateSecurityIssueWebview({ - issue: updatedIssue, - isGenerateFixLoading: false, - filePath: targetFilePath, - context: context.extensionContext, - shouldRefreshView: true, - }) - - SecurityIssueProvider.instance.updateIssue(updatedIssue, targetFilePath) - SecurityIssueTreeViewProvider.instance.refresh() - } catch (err) { - const error = err instanceof Error ? err : new TypeError('Unexpected error') - await updateSecurityIssueWebview({ - issue: targetIssue, - isGenerateFixLoading: false, - generateFixError: getErrorMsg(error, true), - filePath: targetFilePath, - context: context.extensionContext, - shouldRefreshView: false, - }) - SecurityIssueProvider.instance.updateIssue(targetIssue, targetFilePath) - SecurityIssueTreeViewProvider.instance.refresh() - throw err - } finally { - telemetry.record({ - component: targetSource, - detectorId: targetIssue.detectorId, - findingId: targetIssue.findingId, - ruleId: targetIssue.ruleId, - variant: refresh ? 'refresh' : undefined, - autoDetected: targetIssue.autoDetected, - codewhispererCodeScanJobId: targetIssue.scanJobId, - }) - } - }) + await vscode.commands.executeCommand('aws.amazonq.generateFix', issueItem.issue, issueItem.filePath) } ) @@ -824,19 +732,13 @@ export const rejectFix = Commands.declare( export const regenerateFix = Commands.declare( { id: 'aws.amazonq.security.regenerateFix' }, - () => async (issue: CodeScanIssue | IssueItem | undefined, filePath: string, source: Component) => { - const targetIssue: CodeScanIssue | undefined = issue instanceof IssueItem ? issue.issue : issue - const targetFilePath: string = issue instanceof IssueItem ? issue.filePath : filePath - const targetSource: Component = issue instanceof IssueItem ? 'tree' : source - const updatedIssue = await rejectFix.execute(targetIssue, targetFilePath) - await generateFix.execute(updatedIssue, targetFilePath, targetSource, true) - } + () => async (issue: CodeScanIssue | IssueItem | undefined, filePath: string, source: Component) => {} ) export const explainIssue = Commands.declare( { id: 'aws.amazonq.security.explain' }, () => async (issueItem: IssueItem) => { - await vscode.commands.executeCommand('aws.amazonq.explainIssue', issueItem.issue) + await vscode.commands.executeCommand('aws.amazonq.explainIssue', issueItem.issue, issueItem.filePath) } ) diff --git a/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts b/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts index 50af478ba57..d193af056f7 100644 --- a/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts +++ b/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts @@ -15,7 +15,6 @@ import { sleep } from '../../shared/utilities/timeoutUtils' import { handleExtraBrackets } from '../util/closingBracketUtil' import { Commands } from '../../shared/vscode/commands2' import { isInlineCompletionEnabled } from '../util/commonUtil' -import { ExtContext } from '../../shared/extensions' import { onAcceptance } from './onAcceptance' import * as codewhispererClient from '../client/codewhisperer' import { @@ -36,7 +35,7 @@ import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' export const acceptSuggestion = Commands.declare( 'aws.amazonq.accept', - (context: ExtContext) => + (context: vscode.ExtensionContext) => async ( range: vscode.Range, effectiveRange: vscode.Range, diff --git a/packages/core/src/codewhisperer/commands/startSecurityScan.ts b/packages/core/src/codewhisperer/commands/startSecurityScan.ts index d04fe6effc3..ba9f4f9d926 100644 --- a/packages/core/src/codewhisperer/commands/startSecurityScan.ts +++ b/packages/core/src/codewhisperer/commands/startSecurityScan.ts @@ -108,6 +108,9 @@ export async function startSecurityScan( zipUtil: ZipUtil = new ZipUtil(), scanUuid?: string ) { + if (scope === CodeAnalysisScope.AGENTIC) { + throw new CreateCodeScanFailedError('Cannot use Agentic scope') + } const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile const logger = getLoggerForScope(scope) /** @@ -401,7 +404,7 @@ export function showSecurityScanResults( zipMetadata: ZipMetadata, totalIssues: number ) { - initSecurityScanRender(securityRecommendationCollection, context, editor, scope) + initSecurityScanRender(securityRecommendationCollection, editor, scope) if (scope === CodeWhispererConstants.CodeAnalysisScope.PROJECT) { populateCodeScanLogStream(zipMetadata.scannedFiles) @@ -439,7 +442,7 @@ export function showScanResultsInChat( break } - initSecurityScanRender(securityRecommendationCollection, context, editor, scope) + initSecurityScanRender(securityRecommendationCollection, editor, scope) if (totalIssues > 0) { SecurityIssueTreeViewProvider.focus() } @@ -479,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/startTestGeneration.ts b/packages/core/src/codewhisperer/commands/startTestGeneration.ts deleted file mode 100644 index e99fd499e5a..00000000000 --- a/packages/core/src/codewhisperer/commands/startTestGeneration.ts +++ /dev/null @@ -1,259 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { getLogger } from '../../shared/logger/logger' -import { ZipUtil } from '../util/zipUtil' -import { ArtifactMap } from '../client/codewhisperer' -import { testGenerationLogsDir } from '../../shared/filesystemUtilities' -import { - createTestJob, - exportResultsArchive, - getPresignedUrlAndUploadTestGen, - pollTestJobStatus, - throwIfCancelled, -} from '../service/testGenHandler' -import path from 'path' -import { testGenState } from '../models/model' -import { ChatSessionManager } from '../../amazonqTest/chat/storages/chatSession' -import { ChildProcess, spawn } from 'child_process' // eslint-disable-line no-restricted-imports -import { BuildStatus } from '../../amazonqTest/chat/session/session' -import { fs } from '../../shared/fs/fs' -import { Range } from '../client/codewhispereruserclient' -import { AuthUtil } from '../indexNode' - -// eslint-disable-next-line unicorn/no-null -let spawnResult: ChildProcess | null = null -let isCancelled = false -export async function startTestGenerationProcess( - filePath: string, - userInputPrompt: string, - tabID: string, - initialExecution: boolean, - selectionRange?: Range -) { - const logger = getLogger() - const session = ChatSessionManager.Instance.getSession() - const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile - // TODO: Step 0: Initial Test Gen telemetry - try { - logger.verbose(`Starting Test Generation `) - logger.verbose(`Tab ID: ${tabID} !== ${session.tabID}`) - if (tabID !== session.tabID) { - logger.verbose(`Tab ID mismatch: ${tabID} !== ${session.tabID}`) - return - } - /** - * Step 1: Zip the project - */ - - const zipUtil = new ZipUtil() - if (initialExecution) { - const projectPath = zipUtil.getProjectPath(filePath) ?? '' - const relativeTargetPath = path.relative(projectPath, filePath) - session.listOfTestGenerationJobId = [] - session.shortAnswer = undefined - session.sourceFilePath = relativeTargetPath - session.projectRootPath = projectPath - session.listOfTestGenerationJobId = [] - } - const zipMetadata = await zipUtil.generateZipTestGen(session.projectRootPath, initialExecution) - session.srcPayloadSize = zipMetadata.buildPayloadSizeInBytes - session.srcZipFileSize = zipMetadata.zipFileSizeInBytes - - /** - * Step 2: Get presigned Url, upload and clean up - */ - throwIfCancelled() - if (!shouldContinueRunning(tabID)) { - return - } - let artifactMap: ArtifactMap = {} - const uploadStartTime = performance.now() - try { - artifactMap = await getPresignedUrlAndUploadTestGen(zipMetadata, profile) - } finally { - const outputLogPath = path.join(testGenerationLogsDir, 'output.log') - if (await fs.existsFile(outputLogPath)) { - await fs.delete(outputLogPath) - } - await zipUtil.removeTmpFiles(zipMetadata) - session.artifactsUploadDuration = performance.now() - uploadStartTime - } - - /** - * Step 3: Create scan job with startTestGeneration - */ - throwIfCancelled() - if (!shouldContinueRunning(tabID)) { - return - } - const sessionFilePath = session.sourceFilePath - const testJob = await createTestJob( - artifactMap, - [ - { - relativeTargetPath: sessionFilePath, - targetLineRangeList: selectionRange ? [selectionRange] : [], - }, - ], - userInputPrompt, - undefined, - profile - ) - if (!testJob.testGenerationJob) { - throw Error('Test job not found') - } - session.testGenerationJob = testJob.testGenerationJob - - /** - * Step 4: Polling mechanism on test job status with getTestGenStatus - */ - throwIfCancelled() - if (!shouldContinueRunning(tabID)) { - return - } - await pollTestJobStatus( - testJob.testGenerationJob.testGenerationJobId, - testJob.testGenerationJob.testGenerationJobGroupName, - filePath, - initialExecution, - profile - ) - // TODO: Send status to test summary - throwIfCancelled() - if (!shouldContinueRunning(tabID)) { - return - } - /** - * Step 5: Process and show the view diff by getting the results from exportResultsArchive - */ - // https://github.com/aws/aws-toolkit-vscode/blob/0164d4145e58ae036ddf3815455ea12a159d491d/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts#L314-L405 - await exportResultsArchive( - artifactMap.SourceCode, - testJob.testGenerationJob.testGenerationJobGroupName, - testJob.testGenerationJob.testGenerationJobId, - path.basename(session.projectRootPath), - session.projectRootPath, - initialExecution - ) - } catch (error) { - logger.error(`startTestGenerationProcess failed: %O`, error) - // TODO: Send error message to Chat - testGenState.getChatControllers()?.errorThrown.fire({ - tabID: session.tabID, - error: error, - }) - } finally { - testGenState.setToNotStarted() - } -} - -export function shouldContinueRunning(tabID: string): boolean { - if (tabID !== ChatSessionManager.Instance.getSession().tabID) { - getLogger().verbose(`Tab ID mismatch: ${tabID} !== ${ChatSessionManager.Instance.getSession().tabID}`) - return false - } - return true -} - -/** - * Run client side build with given build commands - */ -export async function runBuildCommand(listofBuildCommand: string[]): Promise { - for (const buildCommand of listofBuildCommand) { - try { - await fs.mkdir(testGenerationLogsDir) - const tmpFile = path.join(testGenerationLogsDir, 'output.log') - const result = await runLocalBuild(buildCommand, tmpFile) - if (result.isCancelled) { - return BuildStatus.CANCELLED - } - if (result.code !== 0) { - return BuildStatus.FAILURE - } - } catch (error) { - getLogger().error(`Build process error`) - return BuildStatus.FAILURE - } - } - return BuildStatus.SUCCESS -} - -function runLocalBuild( - buildCommand: string, - tmpFile: string -): Promise<{ code: number | null; isCancelled: boolean; message: string }> { - return new Promise(async (resolve, reject) => { - const environment = process.env - const repositoryPath = ChatSessionManager.Instance.getSession().projectRootPath - const [command, ...args] = buildCommand.split(' ') - getLogger().info(`Build process started for command: ${buildCommand}, for path: ${repositoryPath}`) - - let buildLogs = '' - - spawnResult = spawn(command, args, { - cwd: repositoryPath, - shell: true, - env: environment, - }) - - if (spawnResult.stdout) { - spawnResult.stdout.on('data', async (data) => { - const output = data.toString().trim() - getLogger().info(`BUILD OUTPUT: ${output}`) - buildLogs += output - }) - } - - if (spawnResult.stderr) { - spawnResult.stderr.on('data', async (data) => { - const output = data.toString().trim() - getLogger().warn(`BUILD ERROR: ${output}`) - buildLogs += output - }) - } - - spawnResult.on('close', async (code) => { - let message = '' - if (isCancelled) { - message = 'Build cancelled' - getLogger().info('BUILD CANCELLED') - } else if (code === 0) { - message = 'Build successful' - getLogger().info('BUILD SUCCESSFUL') - } else { - message = `Build failed with exit code ${code}` - getLogger().info(`BUILD FAILED with exit code ${code}`) - } - - try { - await fs.writeFile(tmpFile, buildLogs) - getLogger().info(`Build logs written to ${tmpFile}`) - } catch (error) { - getLogger().error(`Failed to write build logs to ${tmpFile}: ${error}`) - } - - resolve({ code, isCancelled, message }) - - // eslint-disable-next-line unicorn/no-null - spawnResult = null - isCancelled = false - }) - - spawnResult.on('error', (error) => { - reject(new Error(`Failed to start build process: ${error.message}`)) - }) - }) -} - -export function cancelBuild() { - if (spawnResult) { - isCancelled = true - spawnResult.kill() - getLogger().info('Build cancellation requested') - } else { - getLogger().info('No active build to cancel') - } -} diff --git a/packages/core/src/codewhisperer/commands/startTransformByQ.ts b/packages/core/src/codewhisperer/commands/startTransformByQ.ts index 91e9ad00ab9..410465a55c1 100644 --- a/packages/core/src/codewhisperer/commands/startTransformByQ.ts +++ b/packages/core/src/codewhisperer/commands/startTransformByQ.ts @@ -16,10 +16,10 @@ import { jobPlanProgress, FolderInfo, ZipManifest, - TransformByQStatus, TransformationType, TransformationCandidateProject, RegionProfile, + sessionJobHistory, } from '../models/model' import { createZipManifest, @@ -43,7 +43,6 @@ import { validateOpenProjects, } from '../service/transformByQ/transformProjectValidationHandler' import { - getVersionData, prepareProjectDependencies, runMavenDependencyUpdateCommands, } from '../service/transformByQ/transformMavenHandler' @@ -79,10 +78,16 @@ import { convertDateToTimestamp } from '../../shared/datetime' import { findStringInDirectory } from '../../shared/utilities/workspaceUtils' import { makeTemporaryToolkitFolder } from '../../shared/filesystemUtilities' import { AuthUtil } from '../util/authUtil' +import { + cleanupTempJobFiles, + createMetadataFile, + JobMetadata, + writeToHistoryFile, +} from '../service/transformByQ/transformationHistoryHandler' export function getFeedbackCommentData() { const jobId = transformByQState.getJobId() - const s = `Q CodeTransform jobId: ${jobId ? jobId : 'none'}` + const s = `Q CodeTransformation jobId: ${jobId ? jobId : 'none'}` return s } @@ -110,10 +115,10 @@ export async function processSQLConversionTransformFormInput(pathToProject: stri export async function compileProject() { try { - const dependenciesFolder: FolderInfo = getDependenciesFolderInfo() + const dependenciesFolder: FolderInfo = await getDependenciesFolderInfo() transformByQState.setDependencyFolderInfo(dependenciesFolder) - const modulePath = transformByQState.getProjectPath() - await prepareProjectDependencies(dependenciesFolder, modulePath) + const projectPath = transformByQState.getProjectPath() + await prepareProjectDependencies(dependenciesFolder.path, projectPath) } catch (err) { // open build-logs.txt file to show user error logs await writeAndShowBuildLogs(true) @@ -175,8 +180,7 @@ export async function humanInTheLoopRetryLogic(jobId: string, profile: RegionPro if (status === 'PAUSED') { const hilStatusFailure = await initiateHumanInTheLoopPrompt(jobId) if (hilStatusFailure) { - // We rejected the changes and resumed the job and should - // try to resume normal polling asynchronously + // resume polling void humanInTheLoopRetryLogic(jobId, profile) } } else { @@ -184,9 +188,7 @@ export async function humanInTheLoopRetryLogic(jobId: string, profile: RegionPro } } catch (error) { status = 'FAILED' - // TODO if we encounter error in HIL, do we stop job? await finalizeTransformByQ(status) - // bubble up error to callee function throw error } } @@ -225,11 +227,9 @@ export async function preTransformationUploadCode() { const payloadFilePath = zipCodeResult.tempFilePath const zipSize = zipCodeResult.fileSize - const dependenciesCopied = zipCodeResult.dependenciesCopied telemetry.record({ codeTransformTotalByteSize: zipSize, - codeTransformDependenciesCopied: dependenciesCopied, }) transformByQState.setPayloadFilePath(payloadFilePath) @@ -408,7 +408,7 @@ export async function finishHumanInTheLoop(selectedDependency?: string) { // 7) We need to take that output of maven and use CreateUploadUrl const uploadFolderInfo = humanInTheLoopManager.getUploadFolderInfo() - await prepareProjectDependencies(uploadFolderInfo, uploadFolderInfo.path) + await prepareProjectDependencies(uploadFolderInfo.path, uploadFolderInfo.path) // zipCode side effects deletes the uploadFolderInfo right away const uploadResult = await zipCode({ dependenciesFolder: uploadFolderInfo, @@ -449,13 +449,11 @@ export async function finishHumanInTheLoop(selectedDependency?: string) { await terminateHILEarly(jobId) void humanInTheLoopRetryLogic(jobId, profile) } finally { - // Always delete the dependency directories telemetry.codeTransform_humanInTheLoop.emit({ codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId(), codeTransformJobId: jobId, codeTransformMetadata: CodeTransformTelemetryState.instance.getCodeTransformMetaDataString(), result: hilResult, - // TODO: make a generic reason field for telemetry logging so we don't log sensitive PII data reason: hilResult === MetadataResult.Fail ? 'Runtime error occurred' : undefined, }) await HumanInTheLoopManager.instance.cleanUpArtifacts() @@ -482,6 +480,23 @@ export async function startTransformationJob( codeTransformRunTimeLatency: calculateTotalLatency(transformStartTime), }) }) + + // create local history folder(s) and store metadata + const metadata: JobMetadata = { + jobId: jobId, + projectName: transformByQState.getProjectName(), + transformationType: transformByQState.getTransformationType() ?? TransformationType.LANGUAGE_UPGRADE, + sourceJDKVersion: transformByQState.getSourceJDKVersion() ?? JDKVersion.JDK8, + targetJDKVersion: transformByQState.getTargetJDKVersion() ?? JDKVersion.JDK17, + customDependencyVersionFilePath: transformByQState.getCustomDependencyVersionFilePath(), + customBuildCommand: transformByQState.getCustomBuildCommand(), + targetJavaHome: transformByQState.getTargetJavaHome() ?? '', + projectPath: transformByQState.getProjectPath(), + startTime: transformByQState.getStartTime(), + } + + const jobHistoryPath = await createMetadataFile(transformByQState.getPayloadFilePath(), metadata) + transformByQState.setJobHistoryPath(jobHistoryPath) } catch (error) { getLogger().error(`CodeTransformation: ${CodeWhispererConstants.failedToStartJobNotification}`, error) const errorMessage = (error as Error).message.toLowerCase() @@ -504,7 +519,7 @@ export async function startTransformationJob( throw new JobStartError() } - await sleep(2000) // sleep before polling job to prevent ThrottlingException + await sleep(5000) // sleep before polling job status to prevent ThrottlingException throwIfCancelled() return jobId @@ -523,9 +538,7 @@ export async function pollTransformationStatusUntilPlanReady(jobId: string, prof transformByQState.setJobFailureErrorChatMessage(CodeWhispererConstants.failedToCompleteJobChatMessage) } - // Since we don't yet have a good way of knowing what the error was, - // we try to fetch any build failure artifacts that may exist so that we can optionally - // show them to the user if they exist. + // try to download pre-build error logs if available let pathToLog = '' try { const tempToolkitFolder = await makeTemporaryToolkitFolder() @@ -651,6 +664,7 @@ export async function setTransformationToRunningState() { transformByQState.resetSessionJobHistory() transformByQState.setJobId('') // so that details for last job are not overwritten when running one job after another transformByQState.setPolledJobStatus('') // so that previous job's status does not display at very beginning of this job + transformByQState.setHasSeenTransforming(false) CodeTransformTelemetryState.instance.setStartTime() transformByQState.setStartTime( @@ -677,11 +691,15 @@ export async function postTransformationJob() { let chatMessage = transformByQState.getJobFailureErrorChatMessage() if (transformByQState.isSucceeded()) { - chatMessage = CodeWhispererConstants.jobCompletedChatMessage(transformByQState.getTargetJDKVersion() ?? '') + chatMessage = CodeWhispererConstants.jobCompletedChatMessage } else if (transformByQState.isPartiallySucceeded()) { chatMessage = CodeWhispererConstants.jobPartiallyCompletedChatMessage } + if (transformByQState.getSourceJDKVersion() !== transformByQState.getTargetJDKVersion()) { + chatMessage += CodeWhispererConstants.upgradeLibrariesMessage + } + transformByQState.getChatControllers()?.transformationFinished.fire({ message: chatMessage, tabID: ChatSessionManager.Instance.getSession().tabID, @@ -689,37 +707,35 @@ export async function postTransformationJob() { const durationInMs = calculateTotalLatency(CodeTransformTelemetryState.instance.getStartTime()) const resultStatusMessage = transformByQState.getStatus() - if (transformByQState.getTransformationType() !== TransformationType.SQL_CONVERSION) { - // the below is only applicable when user is doing a Java 8/11 language upgrade - const versionInfo = await getVersionData() - const mavenVersionInfoMessage = `${versionInfo[0]} (${transformByQState.getMavenName()})` - const javaVersionInfoMessage = `${versionInfo[1]} (${transformByQState.getMavenName()})` + telemetry.codeTransform_totalRunTime.emit({ + codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId(), + codeTransformJobId: transformByQState.getJobId(), + codeTransformResultStatusMessage: resultStatusMessage, + codeTransformRunTimeLatency: durationInMs, + reason: transformByQState.getPolledJobStatus(), + result: + transformByQState.isSucceeded() || transformByQState.isPartiallySucceeded() + ? MetadataResult.Pass + : MetadataResult.Fail, + }) - telemetry.codeTransform_totalRunTime.emit({ - buildSystemVersion: mavenVersionInfoMessage, - codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId(), - codeTransformJobId: transformByQState.getJobId(), - codeTransformResultStatusMessage: resultStatusMessage, - codeTransformRunTimeLatency: durationInMs, - codeTransformLocalJavaVersion: javaVersionInfoMessage, - result: resultStatusMessage === TransformByQStatus.Succeeded ? MetadataResult.Pass : MetadataResult.Fail, - reason: `${resultStatusMessage}-${chatMessage}`, - }) - } + let notificationMessage = '' if (transformByQState.isSucceeded()) { - void vscode.window.showInformationMessage( - CodeWhispererConstants.jobCompletedNotification(transformByQState.getTargetJDKVersion() ?? ''), - { - title: localizedText.ok, - } - ) + notificationMessage = CodeWhispererConstants.jobCompletedNotification + if (transformByQState.getSourceJDKVersion() !== transformByQState.getTargetJDKVersion()) { + notificationMessage += CodeWhispererConstants.upgradeLibrariesMessage + } + void vscode.window.showInformationMessage(notificationMessage, { + title: localizedText.ok, + }) } else if (transformByQState.isPartiallySucceeded()) { + notificationMessage = CodeWhispererConstants.jobPartiallyCompletedNotification + if (transformByQState.getSourceJDKVersion() !== transformByQState.getTargetJDKVersion()) { + notificationMessage += CodeWhispererConstants.upgradeLibrariesMessage + } void vscode.window - .showInformationMessage( - CodeWhispererConstants.jobPartiallyCompletedNotification, - CodeWhispererConstants.amazonQFeedbackText - ) + .showInformationMessage(notificationMessage, CodeWhispererConstants.amazonQFeedbackText) .then((choice) => { if (choice === CodeWhispererConstants.amazonQFeedbackText) { void submitFeedback( @@ -731,16 +747,36 @@ export async function postTransformationJob() { }) } - if (transformByQState.getPayloadFilePath() !== '') { - // delete original upload ZIP at very end of transformation - fs.rmSync(transformByQState.getPayloadFilePath(), { recursive: true, force: true }) - } + await cleanupTempJobFiles( + transformByQState.getJobHistoryPath(), + transformByQState.getPolledJobStatus(), + transformByQState.getPayloadFilePath() + ) // attempt download for user // TODO: refactor as explained here https://github.com/aws/aws-toolkit-vscode/pull/6519/files#r1946873107 if (transformByQState.isSucceeded() || transformByQState.isPartiallySucceeded()) { await vscode.commands.executeCommand('aws.amazonq.transformationHub.reviewChanges.startReview') } + + // store job details and diff path locally (history) + // TODO: ideally when job is cancelled, should be stored as CANCELLED instead of FAILED (remove this if statement after bug is fixed) + if (!transformByQState.isCancelled()) { + const latest = sessionJobHistory[transformByQState.getJobId()] + await writeToHistoryFile( + latest.startTime, + latest.projectName, + latest.status, + latest.duration, + transformByQState.getJobId(), + transformByQState.getJobHistoryPath(), + latest.transformationType, + latest.sourceJDKVersion, + latest.targetJDKVersion, + latest.customDependencyVersionsFilePath, + latest.customBuildCommand + ) + } } export async function transformationJobErrorHandler(error: any) { @@ -749,25 +785,9 @@ export async function transformationJobErrorHandler(error: any) { transformByQState.setToFailed() transformByQState.setPolledJobStatus('FAILED') // jobFailureErrorNotification should always be defined here - let displayedErrorMessage = - transformByQState.getJobFailureErrorNotification() ?? CodeWhispererConstants.failedToCompleteJobNotification - if (transformByQState.getJobFailureMetadata() !== '') { - displayedErrorMessage += ` ${transformByQState.getJobFailureMetadata()}` - transformByQState.setJobFailureErrorChatMessage( - `${transformByQState.getJobFailureErrorChatMessage()} ${transformByQState.getJobFailureMetadata()}` - ) - } - void vscode.window - .showErrorMessage(displayedErrorMessage, CodeWhispererConstants.amazonQFeedbackText) - .then((choice) => { - if (choice === CodeWhispererConstants.amazonQFeedbackText) { - void submitFeedback( - placeholder, - CodeWhispererConstants.amazonQFeedbackKey, - getFeedbackCommentData() - ) - } - }) + transformByQState.setJobFailureErrorChatMessage( + transformByQState.getJobFailureErrorChatMessage() ?? CodeWhispererConstants.failedToCompleteJobChatMessage + ) } else { transformByQState.setToCancelled() transformByQState.setPolledJobStatus('CANCELLED') diff --git a/packages/core/src/codewhisperer/index.ts b/packages/core/src/codewhisperer/index.ts index 4235ae28668..066e5ca2fcb 100644 --- a/packages/core/src/codewhisperer/index.ts +++ b/packages/core/src/codewhisperer/index.ts @@ -9,13 +9,6 @@ export * from './models/model' export * from './models/constants' export * from './commands/basicCommands' export * from './commands/types' -export { - AutotriggerState, - EndState, - ManualtriggerState, - PressTabState, - TryMoreExState, -} from './views/lineAnnotationController' export type { TransformationProgressUpdate, TransformationStep, @@ -43,7 +36,8 @@ export { codeWhispererClient, } from './client/codewhisperer' export { listCodeWhispererCommands, listCodeWhispererCommandsId } from './ui/statusBarMenu' -export { refreshStatusBar, CodeWhispererStatusBar, InlineCompletionService } from './service/inlineCompletionService' +export { InlineCompletionService } from './service/inlineCompletionService' +export { refreshStatusBar, CodeWhispererStatusBarManager } from './service/statusBar' export { SecurityIssueHoverProvider } from './service/securityIssueHoverProvider' export { SecurityIssueCodeActionProvider } from './service/securityIssueCodeActionProvider' export { @@ -53,37 +47,32 @@ export { IssueItem, SeverityItem, } from './service/securityIssueTreeViewProvider' -export { invokeRecommendation } from './commands/invokeRecommendation' export { onAcceptance } from './commands/onAcceptance' export { CodeWhispererTracker } from './tracker/codewhispererTracker' -export { RecommendationHandler } from './service/recommendationHandler' export { CodeWhispererUserGroupSettings } from './util/userGroupUtil' export { session } from './util/codeWhispererSession' export { onInlineAcceptance } from './commands/onInlineAcceptance' export { stopTransformByQ } from './commands/startTransformByQ' -export { getCompletionItems, getCompletionItem, getLabel } from './service/completionProvider' export { featureDefinitions, FeatureConfigProvider } from '../shared/featureConfig' export { ReferenceInlineProvider } from './service/referenceInlineProvider' export { ReferenceHoverProvider } from './service/referenceHoverProvider' export { CWInlineCompletionItemProvider } from './service/inlineCompletionItemProvider' -export { RecommendationService } from './service/recommendationService' export { ClassifierTrigger } from './service/classifierTrigger' -export { DocumentChangedSource, KeyStrokeHandler, DefaultDocumentChangedType } from './service/keyStrokeHandler' 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 { CodeWhispererCodeCoverageTracker } from './tracker/codewhispererCodeCoverageTracker' export { TelemetryHelper } from './util/telemetryHelper' export { LineSelection, LineTracker } from './tracker/lineTracker' export { BM25Okapi } from './util/supplementalContext/rankBm25' -export { handleExtraBrackets } from './util/closingBracketUtil' 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' @@ -91,6 +80,7 @@ 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' @@ -112,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 e5cd9525ddb..81736d478da 100644 --- a/packages/core/src/codewhisperer/models/constants.ts +++ b/packages/core/src/codewhisperer/models/constants.ts @@ -140,8 +140,14 @@ 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 */ @@ -180,12 +186,9 @@ export const securityScanLearnMoreUri = 'https://docs.aws.amazon.com/amazonq/lat export const identityPoolID = 'us-east-1:70717e99-906f-4add-908c-bd9074a2f5b9' /** - * the interval of the background thread invocation, which is triggered by the timer + * Delay for making requests once the user stops typing. Without a delay, inline suggestions request is triggered every keystroke. */ -export const defaultCheckPeriodMillis = 1000 * 60 * 5 - -// suggestion show delay, in milliseconds -export const suggestionShowDelay = 250 +export const inlineCompletionsDebounceDelay = 200 // add 200ms more delay on top of inline default 30-50ms export const inlineSuggestionShowDelay = 200 @@ -521,7 +524,7 @@ If you'd like to update and test your code with fewer changes at a time, I can d export const uploadingCodeStepMessage = 'Upload your code' -export const buildCodeStepMessage = 'Build uploaded code in secure build environment' +export const buildCodeStepMessage = 'Analyze uploaded code in secure environment' export const generatePlanStepMessage = 'Generate transformation plan' @@ -550,7 +553,7 @@ export const noChangesMadeMessage = "I didn't make any changes for this transfor export const noOngoingJobMessage = 'No ongoing job.' -export const nothingToShowMessage = 'Nothing to show' +export const noJobHistoryMessage = 'No job history' export const jobStartedNotification = 'Amazon Q is transforming your code. It can take 10 to 30 minutes to upgrade your code, depending on the size of your project. To monitor progress, go to the Transformation Hub.' @@ -585,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 = - 'Your .YAML file is not formatted correctly. Make sure that the .YAML file you upload follows the format of the sample file provided.' +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." @@ -646,24 +649,34 @@ export const jobCancelledNotification = 'You cancelled the transformation.' export const continueWithoutHilMessage = 'I will continue transforming your code without upgrading this dependency.' -export const continueWithoutYamlMessage = 'Ok, I will continue without this information.' +export const continueWithoutConfigFileMessage = + 'Ok, I will continue the transformation without additional dependency upgrade information.' + +export const receivedValidConfigFileMessage = + 'The dependency upgrade file looks good. I will use this information to upgrade the dependencies you specified.' + +export const chooseConfigFileMessageJdkUpgrade = + 'Would you like to provide a dependency upgrade file? You can specify first party dependencies and their versions in a YAML file, and I will upgrade them during the JDK upgrade transformation. For an example dependency upgrade file, see the [documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/code-transformation.html#dependency-upgrade-file).' -export const chooseYamlMessage = - 'You can optionally upload a YAML file to specify which dependency versions to upgrade to.' +export const chooseConfigFileMessageLibraryUpgrade = + 'Would you like to provide a dependency upgrade file? You can specify third party dependencies and their versions in a YAML file, and I will only upgrade these dependencies during the library upgrade transformation. For an example dependency upgrade file, see the [documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/code-transformation.html#dependency-upgrade-file).' export const enterJavaHomePlaceholder = 'Enter the path to your Java installation' export const openNewTabPlaceholder = 'Open a new tab to chat with Q' -export const jobCompletedChatMessage = (version: string) => - `I completed your transformation. You can review the diff to see my proposed changes and accept or reject them. The transformation summary has details about the changes I'm proposing. If you want to upgrade additional libraries and other dependencies, run /transform with the transformed code and specify ${version} as the source and target version.` +export const jobCompletedChatMessage = + 'I completed your transformation. You can review the diff to see my proposed changes and accept or reject them. The transformation summary has details about the changes I am proposing. ' -export const jobCompletedNotification = (version: string) => - `Amazon Q transformed your code. You can review the diff to see my proposed changes and accept or reject them. The transformation summary has details about the changes. If you want to upgrade additional libraries and other dependencies, run /transform with the transformed code and specify ${version} as the source and target version.` +export const jobCompletedNotification = + 'Amazon Q transformed your code. You can review the diff to see my proposed changes and accept or reject them. The transformation summary has details about the changes. ' -export const jobPartiallyCompletedChatMessage = `I transformed part of your code. You can review the diff to see my proposed changes and accept or reject them. The transformation summary has details about the files I updated and the errors that prevented a complete transformation.` +export const upgradeLibrariesMessage = + 'After successfully transforming to Java 17 or 21, an additional transformation is required to upgrade your libraries and dependencies. Choose the same source code version and target code version (for example, 17 to 17) to do this.' -export const jobPartiallyCompletedNotification = `Amazon Q transformed part of your code. You can review the diff to see my proposed changes and accept or reject them. The transformation summary has details about the files I updated and the errors that prevented a complete transformation.` +export const jobPartiallyCompletedChatMessage = `I transformed part of your code. You can review the diff to see my proposed changes and accept or reject them. The transformation summary has details about the files I updated and the errors that prevented a complete transformation. ` + +export const jobPartiallyCompletedNotification = `Amazon Q transformed part of your code. You can review the diff to see my proposed changes and accept or reject them. The transformation summary has details about the files I updated and the errors that prevented a complete transformation. ` export const noPomXmlFoundChatMessage = `I couldn\'t find a project that I can upgrade. I couldn\'t find a pom.xml file in any of your open projects, nor could I find any embedded SQL statements. Currently, I can upgrade Java 8, 11, or 17 projects built on Maven, or Oracle SQL to PostgreSQL statements in Java projects. For more information, see the [Amazon Q documentation](${codeTransformPrereqDoc}).` @@ -723,14 +736,14 @@ export const linkToBillingInfo = 'https://aws.amazon.com/q/developer/pricing/' export const dependencyFolderName = 'transformation_dependencies_temp_' -export const cleanInstallErrorChatMessage = `Sorry, I couldn\'t run the Maven clean install command to build your project. For more information, see the [Amazon Q documentation](${codeTransformTroubleshootMvnFailure}).` +export const cleanTestCompileErrorChatMessage = `I could not run \`mvn clean test-compile\` to build your project. For more information, see the [Amazon Q documentation](${codeTransformTroubleshootMvnFailure}).` -export const cleanInstallErrorNotification = `Amazon Q could not run the Maven clean install command to build your project. For more information, see the [Amazon Q documentation](${codeTransformTroubleshootMvnFailure}).` +export const cleanTestCompileErrorNotification = `Amazon Q could not run \`mvn clean test-compile\` to build your project. For more information, see the [Amazon Q documentation](${codeTransformTroubleshootMvnFailure}).` export const enterJavaHomeChatMessage = 'Enter the path to JDK' export const projectPromptChatMessage = - 'I can upgrade your Java project. To start the transformation, I need some information from you. Choose the project you want to upgrade and the target code version to upgrade to. Then, choose Confirm.' + "I can upgrade your Java project. To start the transformation, I need some information from you. Choose the project you want to upgrade and the target code version to upgrade to. Then, choose Confirm.\n\nAfter successfully transforming to Java 17 or 21, an additional transformation is required to upgrade your libraries and dependencies. Choose the same source code version and target code version (for example, 17 to 17) to do this.\n\nI will perform the transformation based on your project's requests, descriptions, and content. To maintain security, avoid including external, unvetted artifacts in your project repository prior to starting the transformation and always validate transformed code for both functionality and security. Do not turn off or close your machine during the transformation because a stable network connection is required." export const windowsJavaHomeHelpChatMessage = 'To find the JDK path, run the following commands in a new terminal: `cd "C:/Program Files/Java"` and then `dir`. If you see your JDK version, run `cd ` and then `cd` to show the path.' @@ -741,10 +754,6 @@ export const macJavaVersionHomeHelpChatMessage = (version: number) => export const linuxJavaHomeHelpChatMessage = 'To find the JDK path, run the following command in a new terminal: `update-java-alternatives --list`' -export const projectSizeTooLargeChatMessage = `Sorry, your project size exceeds the Amazon Q Code Transformation upload limit of 2GB. For more information, see the [Amazon Q documentation](${codeTransformTroubleshootProjectSize}).` - -export const projectSizeTooLargeNotification = `Your project size exceeds the Amazon Q Code Transformation upload limit of 2GB. For more information, see the [Amazon Q documentation](${codeTransformTroubleshootProjectSize}).` - export const JDK8VersionNumber = '52' export const JDK11VersionNumber = '55' @@ -762,7 +771,7 @@ export const chooseProjectSchemaFormMessage = 'To continue, choose the project a export const skipUnitTestsFormTitle = 'Choose to skip unit tests' export const skipUnitTestsFormMessage = - 'I will build your project using `mvn clean test` by default. If you would like me to build your project without running unit tests, I will use `mvn clean test-compile`.' + 'I will build generated code in your local environment, not on the server side. For information on how I scan code to reduce security risks associated with building the code in your local environment, see the [documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/code-transformation.html#java-local-builds).\n\nI will build your project using `mvn clean test` by default. If you would like me to build your project without running unit tests, I will use `mvn clean test-compile`.' export const runUnitTestsMessage = 'Run unit tests' @@ -796,6 +805,34 @@ export const formattedStringMap = new Map([ ['numChangedFiles', 'Files to be changed'], ]) +export const refreshInProgressChatMessage = 'A job refresh is currently in progress. Please wait for it to complete.' + +export const refreshingJobChatMessage = (jobId: string) => + `I am now resuming your job (id: ${jobId}). This can take 10 to 30 minutes to complete.` + +export const jobHistoryButtonText = 'Open job history' + +export const viewHistoryMessage = (numInProgress: number) => + numInProgress > 0 + ? `You have ${numInProgress} job${numInProgress > 1 ? 's' : ''} in progress. You can resume ${numInProgress > 1 ? 'them' : 'it'} in the transformation history table.` + : 'View previous transformations run from the IDE' + +export const transformationHistoryTableDescription = + 'This table lists the most recent jobs that you have run in the past 30 days. To open the diff patch and summary files, click the provided links. To get an updated job status, click the refresh icon. The diff patch and summary will appear once they are available.

' + + 'Jobs with a status of FAILED may still be in progress. Resume these jobs within 12 hours of starting the job to get an updated job status and artifacts.' + +export const refreshErrorChatMessage = + "Sorry, I couldn't refresh the job. Please try again or start a new transformation." + +export const refreshErrorNotification = (jobId: string) => `There was an error refreshing this job. Job Id: ${jobId}` + +export const refreshCompletedChatMessage = + 'Job refresh completed. Please see the transformation history table for the updated status and artifacts.' + +export const refreshCompletedNotification = (jobId: string) => `Job refresh completed. (Job Id: ${jobId})` + +export const refreshNoUpdatesNotification = (jobId: string) => `No updates. (Job Id: ${jobId})` + // end of QCT Strings export enum UserGroup { @@ -838,6 +875,7 @@ export enum CodeAnalysisScope { FILE_AUTO = 'FILE_AUTO', FILE_ON_DEMAND = 'FILE_ON_DEMAND', PROJECT = 'PROJECT', + AGENTIC = 'AGENTIC', } export enum TestGenerationJobStatus { @@ -903,3 +941,9 @@ export const predictionTrackerDefaultConfig = { maxAgeMs: 30000, maxSupplementalContext: 15, } + +export const codeReviewFindingsSuffix = '_codeReviewFindings' +export const displayFindingsSuffix = '_displayFindings' + +export const displayFindingsDetectorName = 'DisplayFindings' +export const findingsSuffix = '_codeReviewFindings' diff --git a/packages/core/src/codewhisperer/models/model.ts b/packages/core/src/codewhisperer/models/model.ts index d77c52254bc..f074fe74bd6 100644 --- a/packages/core/src/codewhisperer/models/model.ts +++ b/packages/core/src/codewhisperer/models/model.ts @@ -18,7 +18,6 @@ import globals from '../../shared/extensionGlobals' import { ChatControllerEventEmitters } from '../../amazonqGumby/chat/controller/controller' import { TransformationSteps } from '../client/codewhispereruserclient' import { Messenger } from '../../amazonqGumby/chat/controller/messenger/messenger' -import { TestChatControllerEventEmitters } from '../../amazonqTest/chat/controller/controller' import { ScanChatControllerEventEmitters } from '../../amazonqScan/controller' import { localize } from '../../shared/utilities/vsCodeUtils' @@ -33,19 +32,29 @@ interface VsCodeState { * Flag indicates whether codewhisperer is doing vscode.TextEditor.edit */ isCodeWhispererEditing: boolean + /** + * Keeps track of whether or not recommendations are currently running + */ + isRecommendationsActive: boolean /** * Timestamp of previous user edit */ lastUserModificationTime: number isFreeTierLimitReached: boolean + + lastManualTriggerTime: number } export const vsCodeState: VsCodeState = { isIntelliSenseActive: false, isCodeWhispererEditing: false, + // hack to globally keep track of whether or not recommendations are currently running. This allows us to know + // when recommendations have ran during e2e tests + isRecommendationsActive: false, lastUserModificationTime: 0, isFreeTierLimitReached: false, + lastManualTriggerTime: 0, } export interface CodeWhispererConfig { @@ -365,55 +374,6 @@ export interface CodeLine { number: number } -/** - * Unit Test Generation - */ -enum TestGenStatus { - NotStarted, - Running, - Cancelling, -} -// TODO: Refactor model of /scan and /test -export class TestGenState { - // Define a constructor for this class - private testGenState: TestGenStatus = TestGenStatus.NotStarted - - protected chatControllers: TestChatControllerEventEmitters | undefined = undefined - - public isNotStarted() { - return this.testGenState === TestGenStatus.NotStarted - } - - public isRunning() { - return this.testGenState === TestGenStatus.Running - } - - public isCancelling() { - return this.testGenState === TestGenStatus.Cancelling - } - - public setToNotStarted() { - this.testGenState = TestGenStatus.NotStarted - } - - public setToCancelling() { - this.testGenState = TestGenStatus.Cancelling - } - - public setToRunning() { - this.testGenState = TestGenStatus.Running - } - - public setChatControllers(controllers: TestChatControllerEventEmitters) { - this.chatControllers = controllers - } - public getChatControllers() { - return this.chatControllers - } -} - -export const testGenState: TestGenState = new TestGenState() - enum CodeFixStatus { NotStarted, Running, @@ -668,16 +628,15 @@ export enum BuildSystem { Unknown = 'Unknown', } -// TO-DO: include the custom YAML file path here somewhere? export class ZipManifest { sourcesRoot: string = 'sources/' dependenciesRoot: string = 'dependencies/' - buildLogs: string = 'build-logs.txt' version: string = '1.0' hilCapabilities: string[] = ['HIL_1pDependency_VersionUpgrade'] - // TO-DO: add 'CLIENT_SIDE_BUILD' here when releasing - transformCapabilities: string[] = ['EXPLAINABILITY_V1', 'SELECTIVE_TRANSFORMATION_V2'] + transformCapabilities: string[] = ['EXPLAINABILITY_V1', 'SELECTIVE_TRANSFORMATION_V2', 'CLIENT_SIDE_BUILD', 'IDE'] noInteractiveMode: boolean = true + dependencyUpgradeConfigFile?: string = undefined + compilationsJsonFile: string = 'compilations.json' customBuildCommand: string = 'clean test' requestedConversions?: { sqlConversion?: { @@ -729,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 { @@ -748,6 +717,8 @@ export class TransformByQState { private targetJDKVersion: JDKVersion | undefined = undefined + private jdkVersionToPath: Map = new Map() + private customBuildCommand: string = '' private sourceDB: DB | undefined = undefined @@ -769,13 +740,14 @@ export class TransformByQState { private planFilePath: string = '' private summaryFilePath: string = '' private preBuildLogFilePath: string = '' + private jobHistoryPath: string = '' private resultArchiveFilePath: string = '' private projectCopyFilePath: string = '' private polledJobStatus: string = '' - private jobFailureMetadata: string = '' + private hasSeenTransforming: boolean = false private payloadFilePath: string = '' @@ -800,6 +772,8 @@ export class TransformByQState { private intervalId: NodeJS.Timeout | undefined = undefined + private refreshInProgress: boolean = false + public isNotStarted() { return this.transformByQState === TransformByQStatus.NotStarted } @@ -824,6 +798,14 @@ export class TransformByQState { return this.transformByQState === TransformByQStatus.PartiallySucceeded } + public isRefreshInProgress() { + return this.refreshInProgress + } + + public getHasSeenTransforming() { + return this.hasSeenTransforming + } + public getTransformationType() { return this.transformationType } @@ -864,6 +846,14 @@ export class TransformByQState { return this.targetJDKVersion } + public getPathFromJdkVersion(version: JDKVersion | undefined) { + if (version) { + return this.jdkVersionToPath.get(version) + } else { + return undefined + } + } + public getSourceDB() { return this.sourceDB } @@ -908,6 +898,10 @@ export class TransformByQState { return this.summaryFilePath } + public getJobHistoryPath() { + return this.jobHistoryPath + } + public getResultArchiveFilePath() { return this.resultArchiveFilePath } @@ -916,10 +910,6 @@ export class TransformByQState { return this.projectCopyFilePath } - public getJobFailureMetadata() { - return this.jobFailureMetadata - } - public getPayloadFilePath() { return this.payloadFilePath } @@ -948,6 +938,12 @@ export class TransformByQState { return this.targetJavaHome } + public setJdkVersionToPath(jdkVersion: JDKVersion | undefined, path: string) { + if (jdkVersion) { + this.jdkVersionToPath.set(jdkVersion, path) + } + } + public getChatControllers() { return this.chatControllers } @@ -1000,6 +996,14 @@ export class TransformByQState { this.transformByQState = TransformByQStatus.PartiallySucceeded } + public setRefreshInProgress(inProgress: boolean) { + this.refreshInProgress = inProgress + } + + public setHasSeenTransforming(hasSeen: boolean) { + this.hasSeenTransforming = hasSeen + } + public setTransformationType(type: TransformationType) { this.transformationType = type } @@ -1076,6 +1080,10 @@ export class TransformByQState { this.summaryFilePath = filePath } + public setJobHistoryPath(filePath: string) { + this.jobHistoryPath = filePath + } + public setResultArchiveFilePath(filePath: string) { this.resultArchiveFilePath = filePath } @@ -1084,10 +1092,6 @@ export class TransformByQState { this.projectCopyFilePath = filePath } - public setJobFailureMetadata(data: string) { - this.jobFailureMetadata = data - } - public setPayloadFilePath(payloadFilePath: string) { this.payloadFilePath = payloadFilePath } @@ -1146,9 +1150,10 @@ export class TransformByQState { public setJobDefaults() { this.setToNotStarted() + this.refreshInProgress = false + this.hasSeenTransforming = false this.jobFailureErrorNotification = undefined this.jobFailureErrorChatMessage = undefined - this.jobFailureMetadata = '' this.payloadFilePath = '' this.metadataPathSQL = '' this.customVersionPath = '' @@ -1162,6 +1167,7 @@ export class TransformByQState { this.buildLog = '' this.customBuildCommand = '' this.intervalId = undefined + this.jobHistoryPath = '' } } diff --git a/packages/core/src/codewhisperer/region/regionProfileManager.ts b/packages/core/src/codewhisperer/region/regionProfileManager.ts index a85a2133d89..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' @@ -69,7 +70,7 @@ export class RegionProfileManager { constructor(private readonly profileProvider: () => Promise) { super( 'aws.amazonq.regionProfiles.cache', - 60000, + 3600000, { resource: { locked: false, @@ -77,7 +78,7 @@ export class RegionProfileManager { result: undefined, }, }, - { timeout: 15000, interval: 1500, truthy: true } + { timeout: 15000, interval: 500, truthy: true } ) } @@ -285,32 +286,6 @@ export class RegionProfileManager { if (!previousSelected) { return } - // cross-validation - this.getProfiles() - .then(async (profiles) => { - const r = profiles.find((it) => it.arn === previousSelected.arn) - if (!r) { - telemetry.amazonq_profileState.emit({ - source: 'reload', - amazonQProfileRegion: 'not-set', - reason: 'profile could not be selected', - result: 'Failed', - }) - - await this.invalidateProfile(previousSelected.arn) - RegionProfileManager.logger.warn( - `invlaidating ${previousSelected.name} profile, arn=${previousSelected.arn}` - ) - } - }) - .catch((e) => { - telemetry.amazonq_profileState.emit({ - source: 'reload', - amazonQProfileRegion: 'not-set', - reason: (e as Error).message, - result: 'Failed', - }) - }) await this.switchRegionProfile(previousSelected, 'reload') } @@ -420,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/completionProvider.ts b/packages/core/src/codewhisperer/service/completionProvider.ts deleted file mode 100644 index 226d04dec2b..00000000000 --- a/packages/core/src/codewhisperer/service/completionProvider.ts +++ /dev/null @@ -1,77 +0,0 @@ -/*! - * 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 { runtimeLanguageContext } from '../util/runtimeLanguageContext' -import { Recommendation } from '../client/codewhisperer' -import { LicenseUtil } from '../util/licenseUtil' -import { RecommendationHandler } from './recommendationHandler' -import { session } from '../util/codeWhispererSession' -import path from 'path' -/** - * completion provider for intelliSense popup - */ -export function getCompletionItems(document: vscode.TextDocument, position: vscode.Position) { - const completionItems: vscode.CompletionItem[] = [] - for (const [index, recommendation] of session.recommendations.entries()) { - completionItems.push(getCompletionItem(document, position, recommendation, index)) - session.setSuggestionState(index, 'Showed') - } - return completionItems -} - -export function getCompletionItem( - document: vscode.TextDocument, - position: vscode.Position, - recommendationDetail: Recommendation, - recommendationIndex: number -) { - const start = session.startPos - const range = new vscode.Range(start, start) - const recommendation = recommendationDetail.content - const completionItem = new vscode.CompletionItem(recommendation) - completionItem.insertText = new vscode.SnippetString(recommendation) - completionItem.documentation = new vscode.MarkdownString().appendCodeblock(recommendation, document.languageId) - completionItem.kind = vscode.CompletionItemKind.Method - completionItem.detail = CodeWhispererConstants.completionDetail - completionItem.keepWhitespace = true - completionItem.label = getLabel(recommendation) - completionItem.preselect = true - completionItem.sortText = String(recommendationIndex + 1).padStart(10, '0') - completionItem.range = new vscode.Range(start, position) - const languageContext = runtimeLanguageContext.getLanguageContext( - document.languageId, - path.extname(document.fileName) - ) - let references: typeof recommendationDetail.references - if (recommendationDetail.references !== undefined && recommendationDetail.references.length > 0) { - references = recommendationDetail.references - const licenses = [ - ...new Set(references.map((r) => `[${r.licenseName}](${LicenseUtil.getLicenseHtml(r.licenseName)})`)), - ].join(', ') - completionItem.documentation.appendMarkdown(CodeWhispererConstants.suggestionDetailReferenceText(licenses)) - } - completionItem.command = { - command: 'aws.amazonq.accept', - title: 'On acceptance', - arguments: [ - range, - recommendationIndex, - recommendation, - RecommendationHandler.instance.requestId, - session.sessionId, - session.triggerType, - session.getCompletionType(recommendationIndex), - languageContext.language, - references, - ], - } - return completionItem -} - -export function getLabel(recommendation: string): string { - return recommendation.slice(0, CodeWhispererConstants.labelLength) + '..' -} diff --git a/packages/core/src/codewhisperer/service/diagnosticsProvider.ts b/packages/core/src/codewhisperer/service/diagnosticsProvider.ts index 72407cd80f5..b7a950bba5c 100644 --- a/packages/core/src/codewhisperer/service/diagnosticsProvider.ts +++ b/packages/core/src/codewhisperer/service/diagnosticsProvider.ts @@ -25,10 +25,14 @@ export const securityScanRender: SecurityScanRender = { export function initSecurityScanRender( securityRecommendationList: AggregatedCodeScanIssue[], - context: vscode.ExtensionContext, editor: vscode.TextEditor | undefined, - scope: CodeAnalysisScope + scope: CodeAnalysisScope, + fromQCA: boolean = true ) { + // fromQCA parameter is used to determine if the findings are coming from QCA or from displayFindings tool. + // if the incoming findings are from QCA review, then keep only existing findings from displayFindings + // if the incoming findings are not from QCA review, then keep only the existing QCA findings + securityScanRender.securityDiagnosticCollection = createSecurityDiagnosticCollection() securityScanRender.initialized = false if (scope === CodeAnalysisScope.FILE_ON_DEMAND && editor) { securityScanRender.securityDiagnosticCollection?.delete(editor.document.uri) @@ -37,22 +41,20 @@ export function initSecurityScanRender( } for (const securityRecommendation of securityRecommendationList) { updateSecurityDiagnosticCollection(securityRecommendation) - updateSecurityIssuesForProviders(securityRecommendation, scope === CodeAnalysisScope.FILE_AUTO) + updateSecurityIssuesForProviders(securityRecommendation, scope === CodeAnalysisScope.FILE_AUTO, fromQCA) } securityScanRender.initialized = true } -function updateSecurityIssuesForProviders(securityRecommendation: AggregatedCodeScanIssue, isAutoScope?: boolean) { +function updateSecurityIssuesForProviders( + securityRecommendation: AggregatedCodeScanIssue, + isAutoScope?: boolean, + fromQCA: boolean = true +) { if (isAutoScope) { SecurityIssueProvider.instance.mergeIssues(securityRecommendation) } else { - const updatedSecurityRecommendationList = [ - ...SecurityIssueProvider.instance.issues.filter( - (group) => group.filePath !== securityRecommendation.filePath - ), - securityRecommendation, - ] - SecurityIssueProvider.instance.issues = updatedSecurityRecommendationList + SecurityIssueProvider.instance.mergeIssuesDisplayFindings(securityRecommendation, fromQCA) } SecurityIssueTreeViewProvider.instance.refresh() } diff --git a/packages/core/src/codewhisperer/service/inlineCompletionService.ts b/packages/core/src/codewhisperer/service/inlineCompletionService.ts index cc9887adb1f..cd37663af49 100644 --- a/packages/core/src/codewhisperer/service/inlineCompletionService.ts +++ b/packages/core/src/codewhisperer/service/inlineCompletionService.ts @@ -14,19 +14,16 @@ import { TelemetryHelper } from '../util/telemetryHelper' import { AuthUtil } from '../util/authUtil' import { shared } from '../../shared/utilities/functionUtils' import { ClassifierTrigger } from './classifierTrigger' -import { getSelectedCustomization } from '../util/customizationUtil' -import { codicon, getIcon } from '../../shared/icons' import { session } from '../util/codeWhispererSession' import { noSuggestions } from '../models/constants' -import { Commands } from '../../shared/vscode/commands2' -import { listCodeWhispererCommandsId } from '../ui/statusBarMenu' +import { CodeWhispererStatusBarManager } from './statusBar' export class InlineCompletionService { private maxPage = 100 - private statusBar: CodeWhispererStatusBar + private statusBar: CodeWhispererStatusBarManager private _showRecommendationTimer?: NodeJS.Timer - constructor(statusBar: CodeWhispererStatusBar = CodeWhispererStatusBar.instance) { + constructor(statusBar: CodeWhispererStatusBarManager = CodeWhispererStatusBarManager.instance) { this.statusBar = statusBar RecommendationHandler.instance.onDidReceiveRecommendation((e) => { @@ -34,7 +31,7 @@ export class InlineCompletionService { }) CodeSuggestionsState.instance.onDidChangeState(() => { - return this.refreshStatusBar() + return this.statusBar.refreshStatusBar() }) } @@ -58,7 +55,7 @@ export class InlineCompletionService { this._showRecommendationTimer = undefined } this._showRecommendationTimer = setInterval(() => { - const delay = performance.now() - vsCodeState.lastUserModificationTime + const delay = Date.now() - vsCodeState.lastUserModificationTime if (delay < CodeWhispererConstants.inlineSuggestionShowDelay) { return } @@ -110,7 +107,7 @@ export class InlineCompletionService { } } - await this.setState('loading') + await this.statusBar.setLoading() RecommendationHandler.instance.checkAndResetCancellationTokens() RecommendationHandler.instance.documentUri = editor.document.uri @@ -163,111 +160,4 @@ export class InlineCompletionService { recommendationCount: session.recommendations.length, } } - - /** Updates the status bar to represent the latest CW state */ - refreshStatusBar() { - if (AuthUtil.instance.isConnectionValid()) { - if (AuthUtil.instance.requireProfileSelection()) { - return this.setState('needsProfile') - } - return this.setState('ok') - } else if (AuthUtil.instance.isConnectionExpired()) { - return this.setState('expired') - } else { - return this.setState('notConnected') - } - } - - private async setState(state: keyof typeof states) { - switch (state) { - case 'loading': { - await this.statusBar.setState('loading') - break - } - case 'ok': { - await this.statusBar.setState('ok', CodeSuggestionsState.instance.isSuggestionsEnabled()) - break - } - case 'expired': { - await this.statusBar.setState('expired') - break - } - case 'notConnected': { - await this.statusBar.setState('notConnected') - break - } - case 'needsProfile': { - await this.statusBar.setState('needsProfile') - break - } - } - } -} - -/** The states that the completion service can be in */ -const states = { - loading: 'loading', - ok: 'ok', - expired: 'expired', - notConnected: 'notConnected', - needsProfile: 'needsProfile', -} as const - -export class CodeWhispererStatusBar { - protected statusBar: vscode.StatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1) - - static #instance: CodeWhispererStatusBar - static get instance() { - return (this.#instance ??= new this()) - } - - protected constructor() {} - - async setState(state: keyof Omit): Promise - async setState(status: keyof Pick, isSuggestionsEnabled: boolean): Promise - async setState(status: keyof typeof states, isSuggestionsEnabled?: boolean): Promise { - const statusBar = this.statusBar - statusBar.command = listCodeWhispererCommandsId - statusBar.backgroundColor = undefined - - const title = 'Amazon Q' - switch (status) { - case 'loading': { - const selectedCustomization = getSelectedCustomization() - statusBar.text = codicon` ${getIcon('vscode-loading~spin')} ${title}${ - selectedCustomization.arn === '' ? '' : ` | ${selectedCustomization.name}` - }` - break - } - case 'ok': { - const selectedCustomization = getSelectedCustomization() - const icon = isSuggestionsEnabled ? getIcon('vscode-debug-start') : getIcon('vscode-debug-pause') - statusBar.text = codicon`${icon} ${title}${ - selectedCustomization.arn === '' ? '' : ` | ${selectedCustomization.name}` - }` - break - } - - case 'expired': { - statusBar.text = codicon` ${getIcon('vscode-debug-disconnect')} ${title}` - statusBar.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground') - break - } - case 'needsProfile': - case 'notConnected': - statusBar.text = codicon` ${getIcon('vscode-chrome-close')} ${title}` - statusBar.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground') - break - } - - statusBar.show() - } } - -/** In this module due to circulare dependency issues */ -export const refreshStatusBar = Commands.declare( - { id: 'aws.amazonq.refreshStatusBar', logging: false }, - () => async () => { - await InlineCompletionService.instance.refreshStatusBar() - } -) diff --git a/packages/core/src/codewhisperer/service/keyStrokeHandler.ts b/packages/core/src/codewhisperer/service/keyStrokeHandler.ts index 49ef633a98f..312e31c248a 100644 --- a/packages/core/src/codewhisperer/service/keyStrokeHandler.ts +++ b/packages/core/src/codewhisperer/service/keyStrokeHandler.ts @@ -56,7 +56,7 @@ export class KeyStrokeHandler { return } this.idleTriggerTimer = setInterval(() => { - const duration = (performance.now() - RecommendationHandler.instance.lastInvocationTime) / 1000 + const duration = (Date.now() - RecommendationHandler.instance.lastInvocationTime) / 1000 if (duration < CodeWhispererConstants.invocationTimeIntervalThreshold) { return } diff --git a/packages/core/src/codewhisperer/service/recommendationHandler.ts b/packages/core/src/codewhisperer/service/recommendationHandler.ts index 8ab491b32e0..42f5ceb9b21 100644 --- a/packages/core/src/codewhisperer/service/recommendationHandler.ts +++ b/packages/core/src/codewhisperer/service/recommendationHandler.ts @@ -10,8 +10,8 @@ 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 { AWSError } from 'aws-sdk' -import { isAwsError } from '../../shared/errors' +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' @@ -44,6 +44,7 @@ import { indent } from '../../shared/utilities/textUtilities' import path from 'path' import { isIamConnection } from '../../auth/connection' import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' +import { LanguageClient } from 'vscode-languageclient' /** * This class is for getRecommendation/listRecommendation API calls and its states @@ -99,12 +100,13 @@ export class RecommendationHandler { private next: vscode.Disposable private prev: vscode.Disposable private _timer?: NodeJS.Timer + private languageClient?: LanguageClient documentUri: vscode.Uri | undefined = undefined constructor() { this.requestId = '' this.nextToken = '' - this.lastInvocationTime = performance.now() - CodeWhispererConstants.invocationTimeIntervalThreshold * 1000 + this.lastInvocationTime = Date.now() - CodeWhispererConstants.invocationTimeIntervalThreshold * 1000 this.cancellationToken = new vscode.CancellationTokenSource() this.prev = new vscode.Disposable(() => {}) this.next = new vscode.Disposable(() => {}) @@ -121,6 +123,10 @@ export class RecommendationHandler { return session.recommendations.some((r) => r.content.trim() !== '') } + setLanguageClient(languageClient: LanguageClient) { + this.languageClient = languageClient + } + async getServerResponse( triggerType: CodewhispererTriggerType, isManualTriggerOn: boolean, @@ -204,7 +210,8 @@ export class RecommendationHandler { session.requestContext = await EditorContext.buildListRecommendationRequest( editor as vscode.TextEditor, this.nextToken, - config.isSuggestionsWithCodeReferencesEnabled + config.isSuggestionsWithCodeReferencesEnabled, + this.languageClient ) } else { session.requestContext = { @@ -249,7 +256,7 @@ export class RecommendationHandler { } try { - startTime = performance.now() + startTime = Date.now() this.lastInvocationTime = startTime const mappedReq = runtimeLanguageContext.mapToRuntimeLanguage(request) const codewhispererPromise = @@ -258,7 +265,7 @@ export class RecommendationHandler { : client.generateRecommendations(mappedReq) const resp = await this.getServerResponse(triggerType, config.isManualTriggerEnabled, codewhispererPromise) TelemetryHelper.instance.setSdkApiCallEndTime() - latency = startTime !== 0 ? performance.now() - startTime : 0 + latency = startTime !== 0 ? Date.now() - startTime : 0 if ('recommendations' in resp) { recommendations = (resp && resp.recommendations) || [] } else { @@ -270,7 +277,7 @@ export class RecommendationHandler { sessionId = resp?.$response?.httpResponse?.headers['x-amzn-sessionid'] TelemetryHelper.instance.setFirstResponseRequestId(requestId) if (page === 0) { - session.setTimeToFirstRecommendation(performance.now()) + session.setTimeToFirstRecommendation(Date.now()) } if (nextToken === '') { TelemetryHelper.instance.setAllPaginationEndTime() @@ -280,17 +287,17 @@ export class RecommendationHandler { shouldRecordServiceInvocation = false } if (latency === 0) { - latency = startTime !== 0 ? performance.now() - startTime : 0 + latency = startTime !== 0 ? Date.now() - startTime : 0 } getLogger().error('amazonq inline-suggest: Invocation Exception : %s', (error as Error).message) - if (isAwsError(error)) { + if (isServiceException(error)) { errorMessage = error.message - requestId = error.requestId || '' - errorCode = error.code - reason = `CodeWhisperer Invocation Exception: ${error?.code ?? error?.name ?? 'unknown'}` + requestId = error.$metadata.requestId || '' + errorCode = error.name + reason = `CodeWhisperer Invocation Exception: ${error?.name ?? 'unknown'}` await this.onThrottlingException(error, triggerType) - if (error?.code === 'AccessDeniedException' && errorMessage?.includes('no identity-based policy')) { + 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) @@ -328,7 +335,7 @@ export class RecommendationHandler { msg += `\n ${index.toString().padStart(2, '0')}: ${indent(item.content, 8, true).trim()}` session.requestIdList.push(requestId) } - getLogger('nextEditPrediction').debug(`codeWhisper request ${requestId}`) + getLogger().debug(msg) if (invocationResult === 'Succeeded') { CodeWhispererCodeCoverageTracker.getTracker(session.language)?.incrementServiceInvocationCount() UserWrittenCodeTracker.instance.onQFeatureInvoked() @@ -567,9 +574,9 @@ export class RecommendationHandler { return true } - async onThrottlingException(awsError: AWSError, triggerType: CodewhispererTriggerType) { + async onThrottlingException(awsError: ServiceException, triggerType: CodewhispererTriggerType) { if ( - awsError.code === 'ThrottlingException' && + awsError.name === 'ThrottlingException' && awsError.message.includes(CodeWhispererConstants.throttlingMessage) ) { if (triggerType === 'OnDemand') { @@ -714,7 +721,7 @@ export class RecommendationHandler { codewhispererCompletionType: session.getCompletionType(0), codewhispererCustomizationArn: getSelectedCustomization().arn, codewhispererLanguage: languageContext.language, - duration: performance.now() - this.lastInvocationTime, + duration: Date.now() - this.lastInvocationTime, passive: true, credentialStartUrl: AuthUtil.instance.startUrl, result: 'Succeeded', 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/referenceLogViewProvider.ts b/packages/core/src/codewhisperer/service/referenceLogViewProvider.ts index 9ec20b8cb44..d51424b1c46 100644 --- a/packages/core/src/codewhisperer/service/referenceLogViewProvider.ts +++ b/packages/core/src/codewhisperer/service/referenceLogViewProvider.ts @@ -4,13 +4,15 @@ */ import * as vscode from 'vscode' -import { References } from '../client/codewhisperer' import { LicenseUtil } from '../util/licenseUtil' import * as CodeWhispererConstants from '../models/constants' import { CodeWhispererSettings } from '../util/codewhispererSettings' import globals from '../../shared/extensionGlobals' import { AuthUtil } from '../util/authUtil' import { session } from '../util/codeWhispererSession' +import CodeWhispererClient from '../client/codewhispererclient' +import CodeWhispererUserClient from '../client/codewhispereruserclient' +import { InlineCompletionItemWithReferences } from '@aws/language-server-runtimes-types' export class ReferenceLogViewProvider implements vscode.WebviewViewProvider { public static readonly viewType = 'aws.codeWhisperer.referenceLog' @@ -52,28 +54,23 @@ export class ReferenceLogViewProvider implements vscode.WebviewViewProvider { } } - public static getReferenceLog(recommendation: string, references: References, editor: vscode.TextEditor): string { + public static getReferenceLog(recommendation: string, references: Reference[], editor: vscode.TextEditor): string { const filePath = editor.document.uri.path const time = new Date().toLocaleString() let text = `` for (const reference of references) { + const standardReference = toStandardReference(reference) if ( - reference.recommendationContentSpan === undefined || - reference.recommendationContentSpan.start === undefined || - reference.recommendationContentSpan.end === undefined + standardReference.position === undefined || + standardReference.position.start === undefined || + standardReference.position.end === undefined ) { continue } - const code = recommendation.substring( - reference.recommendationContentSpan.start, - reference.recommendationContentSpan.end - ) - const firstCharLineNumber = - editor.document.positionAt(session.startCursorOffset + reference.recommendationContentSpan.start).line + - 1 - const lastCharLineNumber = - editor.document.positionAt(session.startCursorOffset + reference.recommendationContentSpan.end - 1) - .line + 1 + const { start, end } = standardReference.position + const code = recommendation.substring(start, end) + const firstCharLineNumber = editor.document.positionAt(session.startCursorOffset + start).line + 1 + const lastCharLineNumber = editor.document.positionAt(session.startCursorOffset + end - 1).line + 1 let lineInfo = `` if (firstCharLineNumber === lastCharLineNumber) { lineInfo = `(line at ${firstCharLineNumber})` @@ -84,11 +81,11 @@ export class ReferenceLogViewProvider implements vscode.WebviewViewProvider { text += `And ` } - let license = `${reference.licenseName}` - let repository = reference.repository?.length ? reference.repository : 'unknown' - if (reference.url?.length) { - repository = `${reference.repository}` - license = `${reference.licenseName || 'unknown'}` + let license = `${standardReference.licenseName}` + let repository = standardReference.repository?.length ? standardReference.repository : 'unknown' + if (standardReference.url?.length) { + repository = `${standardReference.repository}` + license = `${standardReference.licenseName || 'unknown'}` } text += @@ -144,3 +141,48 @@ export class ReferenceLogViewProvider implements vscode.WebviewViewProvider { ` } } + +/** + * Reference log needs to support references directly from CW, as well as those from Flare. These references have different shapes, so we standarize them here. + */ +type GetInnerType = T extends (infer U)[] ? U : never +type Reference = + | CodeWhispererClient.Reference + | CodeWhispererUserClient.Reference + | GetInnerType + +type StandardizedReference = { + licenseName?: string + position?: { + start?: number + end?: number + } + repository?: string + url?: string +} + +/** + * Convert a general reference to the standardized format expected by the reference log. + * @param ref + * @returns + */ +function toStandardReference(ref: Reference): StandardizedReference { + const isCWReference = (ref: any) => ref.recommendationContentSpan !== undefined + + if (isCWReference(ref)) { + const castRef = ref as CodeWhispererClient.Reference + return { + licenseName: castRef.licenseName!, + position: { start: castRef.recommendationContentSpan?.start, end: castRef.recommendationContentSpan?.end }, + repository: castRef.repository, + url: castRef.url, + } + } + const castRef = ref as GetInnerType + return { + licenseName: castRef.licenseName, + position: { start: castRef.position?.startCharacter, end: castRef.position?.endCharacter }, + repository: castRef.referenceName, + url: castRef.referenceUrl, + } +} diff --git a/packages/core/src/codewhisperer/service/securityIssueCodeActionProvider.ts b/packages/core/src/codewhisperer/service/securityIssueCodeActionProvider.ts index f1d01494d54..4dc7bceebe7 100644 --- a/packages/core/src/codewhisperer/service/securityIssueCodeActionProvider.ts +++ b/packages/core/src/codewhisperer/service/securityIssueCodeActionProvider.ts @@ -66,7 +66,7 @@ export class SecurityIssueCodeActionProvider implements vscode.CodeActionProvide `Amazon Q: Explain "${issue.title}"`, vscode.CodeActionKind.QuickFix ) - const explainWithQArgs = [issue] + const explainWithQArgs = [issue, group.filePath] explainWithQ.command = { title: 'Explain with Amazon Q', command: 'aws.amazonq.explainIssue', diff --git a/packages/core/src/codewhisperer/service/securityIssueHoverProvider.ts b/packages/core/src/codewhisperer/service/securityIssueHoverProvider.ts index b82c10063e6..bb9fe2cafa4 100644 --- a/packages/core/src/codewhisperer/service/securityIssueHoverProvider.ts +++ b/packages/core/src/codewhisperer/service/securityIssueHoverProvider.ts @@ -10,7 +10,6 @@ import path from 'path' import { AuthUtil } from '../util/authUtil' import { TelemetryHelper } from '../util/telemetryHelper' import { SecurityIssueProvider } from './securityIssueProvider' -import { amazonqCodeIssueDetailsTabTitle } from '../models/constants' export class SecurityIssueHoverProvider implements vscode.HoverProvider { static #instance: SecurityIssueHoverProvider @@ -79,23 +78,23 @@ export class SecurityIssueHoverProvider implements vscode.HoverProvider { `${suggestedFix?.code && suggestedFix.description !== '' ? suggestedFix.description : issue.recommendation.text}\n\n` ) - const viewDetailsCommand = this._getCommandMarkdown( - 'aws.amazonq.openSecurityIssuePanel', - [issue, filePath], - 'eye', - 'View Details', - `Open "${amazonqCodeIssueDetailsTabTitle}"` - ) - markdownString.appendMarkdown(viewDetailsCommand) - const explainWithQCommand = this._getCommandMarkdown( 'aws.amazonq.explainIssue', - [issue], + [issue, filePath], 'comment', 'Explain', 'Explain with Amazon Q' ) - markdownString.appendMarkdown(' | ' + explainWithQCommand) + markdownString.appendMarkdown(explainWithQCommand) + + const generateFixCommand = this._getCommandMarkdown( + 'aws.amazonq.generateFix', + [issue, filePath], + 'wrench', + 'Fix', + 'Fix with Amazon Q' + ) + markdownString.appendMarkdown(' | ' + generateFixCommand) const ignoreIssueCommand = this._getCommandMarkdown( 'aws.amazonq.security.ignore', @@ -115,22 +114,6 @@ export class SecurityIssueHoverProvider implements vscode.HoverProvider { ) markdownString.appendMarkdown(' | ' + ignoreSimilarIssuesCommand) - if (suggestedFix && suggestedFix.code) { - const applyFixCommand = this._getCommandMarkdown( - 'aws.amazonq.applySecurityFix', - [issue, filePath, 'hover'], - 'wrench', - 'Fix', - 'Fix with Amazon Q' - ) - markdownString.appendMarkdown(' | ' + applyFixCommand) - - markdownString.appendMarkdown('### Suggested Fix Preview\n') - markdownString.appendMarkdown( - `${this._makeCodeBlock(suggestedFix.code, issue.detectorId.split('/').shift())}\n` - ) - } - return markdownString } @@ -145,60 +128,4 @@ export class SecurityIssueHoverProvider implements vscode.HoverProvider { } return `![${severity}](severity-${severity.toLowerCase()}.svg)` } - - /** - * Creates a markdown string to render a code diff block for a given code block. Lines - * that are highlighted red indicate deletion while lines highlighted in green indicate - * addition. An optional language can be provided for syntax highlighting on lines which are - * not additions or deletions. - * - * @param code The code containing the diff - * @param language The language for syntax highlighting - * @returns The markdown string - */ - private _makeCodeBlock(code: string, language?: string) { - const lines = code - .replaceAll('\n\\ No newline at end of file', '') - .replaceAll('--- buggyCode\n', '') - .replaceAll('+++ fixCode\n', '') - .split('\n') - const maxLineChars = lines.reduce((acc, curr) => Math.max(acc, curr.length), 0) - const paddedLines = lines.map((line) => line.padEnd(maxLineChars + 2)) - - // Group the lines into sections so consecutive lines of the same type can be placed in - // the same span below - const sections = [paddedLines[0]] - let i = 1 - while (i < paddedLines.length) { - if (paddedLines[i][0] === sections[sections.length - 1][0]) { - sections[sections.length - 1] += '\n' + paddedLines[i] - } else { - sections.push(paddedLines[i]) - } - i++ - } - - // Return each section with the correct syntax highlighting and background color - return sections - .map( - (section) => ` - - -\`\`\`${section.startsWith('-') || section.startsWith('+') ? 'diff' : section.startsWith('@@') ? undefined : language} -${section} -\`\`\` - - -` - ) - .join('
') - } } diff --git a/packages/core/src/codewhisperer/service/securityIssueProvider.ts b/packages/core/src/codewhisperer/service/securityIssueProvider.ts index 61957e6eca5..01f1cd880bd 100644 --- a/packages/core/src/codewhisperer/service/securityIssueProvider.ts +++ b/packages/core/src/codewhisperer/service/securityIssueProvider.ts @@ -5,6 +5,9 @@ import * as vscode from 'vscode' import { AggregatedCodeScanIssue, CodeScanIssue, SuggestedFix } from '../models/model' +import { randomUUID } from '../../shared/crypto' +import { displayFindingsDetectorName } from '../models/constants' + export class SecurityIssueProvider { static #instance: SecurityIssueProvider public static get instance() { @@ -20,6 +23,15 @@ export class SecurityIssueProvider { this._issues = issues } + private _id: string = randomUUID() + public get id() { + return this._id + } + + public set id(id: string) { + this._id = id + } + public handleDocumentChange(event: vscode.TextDocumentChangeEvent) { // handleDocumentChange function may be triggered while testing by our own code generation. if (!event.contentChanges || event.contentChanges.length === 0) { @@ -150,6 +162,30 @@ export class SecurityIssueProvider { ) } + public mergeIssuesDisplayFindings(newIssues: AggregatedCodeScanIssue, fromQCA: boolean) { + const existingGroup = this._issues.find((group) => group.filePath === newIssues.filePath) + if (!existingGroup) { + this._issues.push(newIssues) + return + } + + this._issues = this._issues.map((group) => + group.filePath !== newIssues.filePath + ? group + : { + ...group, + issues: [ + ...group.issues.filter( + // if the incoming findings are from QCA review, then keep only existing findings from displayFindings + // if the incoming findings are not from QCA review, then keep only the existing QCA findings + (issue) => fromQCA === (issue.detectorName === displayFindingsDetectorName) + ), + ...newIssues.issues, + ], + } + ) + } + private isExistingIssue(issue: CodeScanIssue, filePath: string) { return this._issues .find((group) => group.filePath === filePath) diff --git a/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts b/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts index d7c93f70423..9990b50fd96 100644 --- a/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts +++ b/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts @@ -189,7 +189,7 @@ export class IssueItem extends vscode.TreeItem { } private getDescription() { - const positionStr = `[Ln ${this.issue.startLine + 1}, Col 1]` + const positionStr = `[Ln ${this.issue.startLine + 1}]` const groupingStrategy = CodeIssueGroupingStrategyState.instance.getState() return groupingStrategy !== CodeIssueGroupingStrategy.FileLocation ? `${path.basename(this.filePath)} ${positionStr}` diff --git a/packages/core/src/codewhisperer/service/securityScanHandler.ts b/packages/core/src/codewhisperer/service/securityScanHandler.ts index b83fdbebb1a..14485642aed 100644 --- a/packages/core/src/codewhisperer/service/securityScanHandler.ts +++ b/packages/core/src/codewhisperer/service/securityScanHandler.ts @@ -35,13 +35,11 @@ import { SecurityScanTimedOutError, UploadArtifactToS3Error, } from '../models/errors' -import { getTelemetryReasonDesc, isAwsError } from '../../shared/errors' +import { getTelemetryReasonDesc } from '../../shared/errors' import { CodeWhispererSettings } from '../util/codewhispererSettings' import { detectCommentAboveLine } from '../../shared/utilities/commentUtils' import { runtimeLanguageContext } from '../util/runtimeLanguageContext' import { FeatureUseCase } from '../models/constants' -import { UploadTestArtifactToS3Error } from '../../amazonqTest/error' -import { ChatSessionManager } from '../../amazonqTest/chat/storages/chatSession' import { AmazonqCreateUpload, Span, telemetry } from '../../shared/telemetry/telemetry' import { AuthUtil } from '../util/authUtil' @@ -432,10 +430,7 @@ export async function uploadArtifactToS3( } else { errorMessage = errorDesc ?? defaultMessage } - if (isAwsError(error) && featureUseCase === FeatureUseCase.TEST_GENERATION) { - ChatSessionManager.Instance.getSession().startTestGenerationRequestId = error.requestId - } - throw isCodeScan ? new UploadArtifactToS3Error(errorMessage) : new UploadTestArtifactToS3Error(errorMessage) + throw new UploadArtifactToS3Error(errorMessage) } finally { getLogger().debug(`Upload to S3 response details: x-amz-request-id: ${requestId}, x-amz-id-2: ${id2}`) if (span) { diff --git a/packages/core/src/codewhisperer/service/statusBar.ts b/packages/core/src/codewhisperer/service/statusBar.ts new file mode 100644 index 00000000000..6aacfec73b7 --- /dev/null +++ b/packages/core/src/codewhisperer/service/statusBar.ts @@ -0,0 +1,147 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as vscode from 'vscode' +import { CodeSuggestionsState } from '../models/model' +import { AuthUtil } from '../util/authUtil' +import { getSelectedCustomization } from '../util/customizationUtil' +import { codicon, getIcon } from '../../shared/icons' +import { Commands } from '../../shared/vscode/commands2' +import { listCodeWhispererCommandsId } from '../ui/statusBarMenu' + +export class CodeWhispererStatusBarManager { + private statusBar: CodeWhispererStatusBar + + constructor(statusBar: CodeWhispererStatusBar = CodeWhispererStatusBar.instance) { + this.statusBar = statusBar + + CodeSuggestionsState.instance.onDidChangeState(() => { + return this.refreshStatusBar() + }) + } + + static #instance: CodeWhispererStatusBarManager + + public static get instance() { + return (this.#instance ??= new this()) + } + + /** Updates the status bar to represent the latest CW state */ + refreshStatusBar() { + if (AuthUtil.instance.isConnectionValid()) { + if (AuthUtil.instance.requireProfileSelection()) { + return this.setState('needsProfile') + } + return this.setState('ok') + } else if (AuthUtil.instance.isConnectionExpired()) { + return this.setState('expired') + } else { + return this.setState('notConnected') + } + } + + /** + * Sets the status bar in to a "loading state", effectively showing + * the spinning circle. + * + * When loading is done, call {@link refreshStatusBar} to update the + * status bar to the latest state. + */ + async setLoading(): Promise { + await this.setState('loading') + } + + private async setState(state: keyof typeof states) { + switch (state) { + case 'loading': { + await this.statusBar.setState('loading') + break + } + case 'ok': { + await this.statusBar.setState('ok', CodeSuggestionsState.instance.isSuggestionsEnabled()) + break + } + case 'expired': { + await this.statusBar.setState('expired') + break + } + case 'notConnected': { + await this.statusBar.setState('notConnected') + break + } + case 'needsProfile': { + await this.statusBar.setState('needsProfile') + break + } + } + } +} + +/** The states that the completion service can be in */ +const states = { + loading: 'loading', + ok: 'ok', + expired: 'expired', + notConnected: 'notConnected', + needsProfile: 'needsProfile', +} as const + +class CodeWhispererStatusBar { + protected statusBar: vscode.StatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1) + + static #instance: CodeWhispererStatusBar + static get instance() { + return (this.#instance ??= new this()) + } + + protected constructor() {} + + async setState(state: keyof Omit): Promise + async setState(status: keyof Pick, isSuggestionsEnabled: boolean): Promise + async setState(status: keyof typeof states, isSuggestionsEnabled?: boolean): Promise { + const statusBar = this.statusBar + statusBar.command = listCodeWhispererCommandsId + statusBar.backgroundColor = undefined + + const title = 'Amazon Q' + switch (status) { + case 'loading': { + const selectedCustomization = getSelectedCustomization() + statusBar.text = codicon` ${getIcon('vscode-loading~spin')} ${title}${ + selectedCustomization.arn === '' ? '' : ` | ${selectedCustomization.name}` + }` + break + } + case 'ok': { + const selectedCustomization = getSelectedCustomization() + const icon = isSuggestionsEnabled ? getIcon('vscode-debug-start') : getIcon('vscode-debug-pause') + statusBar.text = codicon`${icon} ${title}${ + selectedCustomization.arn === '' ? '' : ` | ${selectedCustomization.name}` + }` + break + } + + case 'expired': { + statusBar.text = codicon` ${getIcon('vscode-debug-disconnect')} ${title}` + statusBar.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground') + break + } + case 'needsProfile': + case 'notConnected': + statusBar.text = codicon` ${getIcon('vscode-chrome-close')} ${title}` + statusBar.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground') + break + } + + statusBar.show() + } +} + +/** In this module due to circular dependency issues */ +export const refreshStatusBar = Commands.declare( + { id: 'aws.amazonq.refreshStatusBar', logging: false }, + () => async () => { + await CodeWhispererStatusBarManager.instance.refreshStatusBar() + } +) diff --git a/packages/core/src/codewhisperer/service/testGenHandler.ts b/packages/core/src/codewhisperer/service/testGenHandler.ts deleted file mode 100644 index 5ca8ca665da..00000000000 --- a/packages/core/src/codewhisperer/service/testGenHandler.ts +++ /dev/null @@ -1,326 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ZipMetadata } from '../util/zipUtil' -import { getLogger } from '../../shared/logger/logger' -import * as CodeWhispererConstants from '../models/constants' -import * as codewhispererClient from '../client/codewhisperer' -import * as codeWhisperer from '../client/codewhisperer' -import CodeWhispererUserClient, { - ArtifactMap, - CreateUploadUrlRequest, - TargetCode, -} from '../client/codewhispereruserclient' -import { - CreateTestJobError, - CreateUploadUrlError, - ExportResultsArchiveError, - InvalidSourceZipError, - TestGenFailedError, - TestGenStoppedError, - TestGenTimedOutError, -} from '../../amazonqTest/error' -import { getMd5, uploadArtifactToS3 } from './securityScanHandler' -import { testGenState, Reference, RegionProfile } from '../models/model' -import { ChatSessionManager } from '../../amazonqTest/chat/storages/chatSession' -import { createCodeWhispererChatStreamingClient } from '../../shared/clients/codewhispererChatClient' -import { downloadExportResultArchive } from '../../shared/utilities/download' -import AdmZip from 'adm-zip' -import path from 'path' -import { ExportIntent } from '@amzn/codewhisperer-streaming' -import { glob } from 'glob' -import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' -import { randomUUID } from '../../shared/crypto' -import { sleep } from '../../shared/utilities/timeoutUtils' -import { tempDirPath } from '../../shared/filesystemUtilities' -import fs from '../../shared/fs/fs' -import { AuthUtil } from '../util/authUtil' - -// TODO: Get TestFileName and Framework and to error message -export function throwIfCancelled() { - // TODO: fileName will be '' if user gives propt without opening - if (testGenState.isCancelling()) { - throw new TestGenStoppedError() - } -} - -export async function getPresignedUrlAndUploadTestGen(zipMetadata: ZipMetadata, profile: RegionProfile | undefined) { - const logger = getLogger() - if (zipMetadata.zipFilePath === '') { - getLogger().error('Failed to create valid source zip') - throw new InvalidSourceZipError() - } - const srcReq: CreateUploadUrlRequest = { - contentMd5: getMd5(zipMetadata.zipFilePath), - artifactType: 'SourceCode', - uploadIntent: CodeWhispererConstants.testGenUploadIntent, - profileArn: profile?.arn, - } - logger.verbose(`Prepare for uploading src context...`) - const srcResp = await codeWhisperer.codeWhispererClient.createUploadUrl(srcReq).catch((err) => { - getLogger().error(`Failed getting presigned url for uploading src context. Request id: ${err.requestId}`) - throw new CreateUploadUrlError(err.message) - }) - logger.verbose(`CreateUploadUrlRequest requestId: ${srcResp.$response.requestId}`) - logger.verbose(`Complete Getting presigned Url for uploading src context.`) - logger.verbose(`Uploading src context...`) - await uploadArtifactToS3(zipMetadata.zipFilePath, srcResp, CodeWhispererConstants.FeatureUseCase.TEST_GENERATION) - logger.verbose(`Complete uploading src context.`) - const artifactMap: ArtifactMap = { - SourceCode: srcResp.uploadId, - } - return artifactMap -} - -export async function createTestJob( - artifactMap: codewhispererClient.ArtifactMap, - relativeTargetPath: TargetCode[], - userInputPrompt: string, - clientToken?: string, - profile?: RegionProfile -) { - const logger = getLogger() - logger.verbose(`Creating test job and starting startTestGeneration...`) - - // JS will minify this input object - fix that - const targetCodeList = relativeTargetPath.map((targetCode) => ({ - relativeTargetPath: targetCode.relativeTargetPath, - targetLineRangeList: targetCode.targetLineRangeList?.map((range) => ({ - start: { line: range.start.line, character: range.start.character }, - end: { line: range.end.line, character: range.end.character }, - })), - })) - logger.debug('updated target code list: %O', targetCodeList) - const req: CodeWhispererUserClient.StartTestGenerationRequest = { - uploadId: artifactMap.SourceCode, - targetCodeList, - userInput: userInputPrompt, - testGenerationJobGroupName: ChatSessionManager.Instance.getSession().testGenerationJobGroupName ?? randomUUID(), // TODO: remove fallback - clientToken, - profileArn: profile?.arn, - } - logger.debug('Unit test generation request body: %O', req) - logger.debug('target code list: %O', req.targetCodeList[0]) - const firstTargetCodeList = req.targetCodeList?.[0] - const firstTargetLineRangeList = firstTargetCodeList?.targetLineRangeList?.[0] - logger.debug('target line range list: %O', firstTargetLineRangeList) - logger.debug('target line range start: %O', firstTargetLineRangeList?.start) - logger.debug('target line range end: %O', firstTargetLineRangeList?.end) - - const resp = await codewhispererClient.codeWhispererClient.startTestGeneration(req).catch((err) => { - ChatSessionManager.Instance.getSession().startTestGenerationRequestId = err.requestId - logger.error(`Failed creating test job. Request id: ${err.requestId}`) - throw new CreateTestJobError(err.message) - }) - logger.info('Unit test generation request id: %s', resp.$response.requestId) - logger.debug('Unit test generation data: %O', resp.$response.data) - ChatSessionManager.Instance.getSession().startTestGenerationRequestId = resp.$response.requestId - if (resp.$response.error) { - logger.error('Unit test generation error: %O', resp.$response.error) - } - if (resp.testGenerationJob) { - ChatSessionManager.Instance.getSession().listOfTestGenerationJobId.push( - resp.testGenerationJob?.testGenerationJobId - ) - ChatSessionManager.Instance.getSession().testGenerationJobGroupName = - resp.testGenerationJob?.testGenerationJobGroupName - } - return resp -} - -export async function pollTestJobStatus( - jobId: string, - jobGroupName: string, - filePath: string, - initialExecution: boolean, - profile?: RegionProfile -) { - const session = ChatSessionManager.Instance.getSession() - const pollingStartTime = performance.now() - // We don't expect to get results immediately, so sleep for some time initially to not make unnecessary calls - await sleep(CodeWhispererConstants.testGenPollingDelaySeconds) - - const logger = getLogger() - logger.verbose(`Polling testgen job status...`) - let status = CodeWhispererConstants.TestGenerationJobStatus.IN_PROGRESS - while (true) { - throwIfCancelled() - const req: CodeWhispererUserClient.GetTestGenerationRequest = { - testGenerationJobId: jobId, - testGenerationJobGroupName: jobGroupName, - profileArn: profile?.arn, - } - const resp = await codewhispererClient.codeWhispererClient.getTestGeneration(req) - logger.verbose('pollTestJobStatus request id: %s', resp.$response.requestId) - logger.debug('pollTestJobStatus testGenerationJob %O', resp.testGenerationJob) - ChatSessionManager.Instance.getSession().testGenerationJob = resp.testGenerationJob - const progressRate = resp.testGenerationJob?.progressRate ?? 0 - testGenState.getChatControllers()?.sendUpdatePromptProgress.fire({ - tabID: ChatSessionManager.Instance.getSession().tabID, - status: 'InProgress', - progressRate, - }) - const jobSummary = resp.testGenerationJob?.jobSummary ?? '' - const jobSummaryNoBackticks = jobSummary.replace(/^`+|`+$/g, '') - ChatSessionManager.Instance.getSession().jobSummary = jobSummaryNoBackticks - const packageInfoList = resp.testGenerationJob?.packageInfoList ?? [] - const packageInfo = packageInfoList[0] - const targetFileInfo = packageInfo?.targetFileInfoList?.[0] - - if (packageInfo) { - // TODO: will need some fields from packageInfo such as buildCommand, packagePlan, packageSummary - } - if (targetFileInfo) { - if (targetFileInfo.numberOfTestMethods) { - session.numberOfTestsGenerated = Number(targetFileInfo.numberOfTestMethods) - } - if (targetFileInfo.codeReferences) { - session.references = targetFileInfo.codeReferences as Reference[] - } - if (initialExecution) { - session.generatedFilePath = targetFileInfo.testFilePath ?? '' - const currentPlanSummary = session.targetFileInfo?.filePlan - const newPlanSummary = targetFileInfo?.filePlan - - if (currentPlanSummary !== newPlanSummary && newPlanSummary) { - const chatControllers = testGenState.getChatControllers() - if (chatControllers) { - const currentSession = ChatSessionManager.Instance.getSession() - chatControllers.updateTargetFileInfo.fire({ - tabID: currentSession.tabID, - targetFileInfo, - testGenerationJobGroupName: resp.testGenerationJob?.testGenerationJobGroupName, - testGenerationJobId: resp.testGenerationJob?.testGenerationJobId, - filePath, - }) - } - } - } - } - ChatSessionManager.Instance.getSession().targetFileInfo = targetFileInfo - status = resp.testGenerationJob?.status as CodeWhispererConstants.TestGenerationJobStatus - if (status === CodeWhispererConstants.TestGenerationJobStatus.FAILED) { - session.numberOfTestsGenerated = 0 - logger.verbose(`Test generation failed.`) - if (resp.testGenerationJob?.jobStatusReason) { - session.stopIteration = true - throw new TestGenFailedError(resp.testGenerationJob?.jobStatusReason) - } else { - throw new TestGenFailedError() - } - } else if (status === CodeWhispererConstants.TestGenerationJobStatus.COMPLETED) { - logger.verbose(`testgen job status: ${status}`) - logger.verbose(`Complete polling test job status.`) - break - } - throwIfCancelled() - await sleep(CodeWhispererConstants.testGenJobPollingIntervalMilliseconds) - const elapsedTime = performance.now() - pollingStartTime - if (elapsedTime > CodeWhispererConstants.testGenJobTimeoutMilliseconds) { - logger.verbose(`testgen job status: ${status}`) - logger.verbose(`testgen job failed. Amazon Q timed out.`) - throw new TestGenTimedOutError() - } - } - return status -} - -/** - * Download the zip from exportResultsArchieve API and store in temp zip - */ -export async function exportResultsArchive( - uploadId: string, - groupName: string, - jobId: string, - projectName: string, - projectPath: string, - initialExecution: boolean -) { - // TODO: Make a common Temp folder - const pathToArchiveDir = path.join(tempDirPath, 'q-testgen') - - const archivePathExists = await fs.existsDir(pathToArchiveDir) - if (archivePathExists) { - await fs.delete(pathToArchiveDir, { recursive: true }) - } - await fs.mkdir(pathToArchiveDir) - - let downloadErrorMessage = undefined - - const session = ChatSessionManager.Instance.getSession() - try { - const pathToArchive = path.join(pathToArchiveDir, 'QTestGeneration.zip') - // Download and deserialize the zip - await downloadResultArchive(uploadId, groupName, jobId, pathToArchive) - const zip = new AdmZip(pathToArchive) - zip.extractAllTo(pathToArchiveDir, true) - - const testFilePathFromResponse = session?.targetFileInfo?.testFilePath - const testFilePath = testFilePathFromResponse - ? testFilePathFromResponse.split('/').slice(1).join('/') // remove the project name - : await getTestFilePathFromZip(pathToArchiveDir) - if (initialExecution) { - testGenState.getChatControllers()?.showCodeGenerationResults.fire({ - tabID: session.tabID, - filePath: testFilePath, - projectName, - }) - - // If User accepts the diff - testGenState.getChatControllers()?.sendUpdatePromptProgress.fire({ - tabID: ChatSessionManager.Instance.getSession().tabID, - status: 'Completed', - }) - } - } catch (e) { - session.numberOfTestsGenerated = 0 - downloadErrorMessage = (e as Error).message - getLogger().error(`Unit Test Generation: ExportResultArchive error = ${downloadErrorMessage}`) - throw new ExportResultsArchiveError(downloadErrorMessage) - } -} - -async function getTestFilePathFromZip(pathToArchiveDir: string) { - const resultArtifactsDir = path.join(pathToArchiveDir, 'resultArtifacts') - const paths = await glob([resultArtifactsDir + '/**/*', '!**/.DS_Store'], { nodir: true }) - const absolutePath = paths[0] - const result = path.relative(resultArtifactsDir, absolutePath) - return result -} - -export async function downloadResultArchive( - uploadId: string, - testGenerationJobGroupName: string, - testGenerationJobId: string, - pathToArchive: string -) { - let downloadErrorMessage = undefined - const cwStreamingClient = await createCodeWhispererChatStreamingClient() - - try { - await downloadExportResultArchive( - cwStreamingClient, - { - exportId: uploadId, - exportIntent: ExportIntent.UNIT_TESTS, - exportContext: { - unitTestGenerationExportContext: { - testGenerationJobGroupName, - testGenerationJobId, - }, - }, - }, - pathToArchive, - AuthUtil.instance.regionProfileManager.activeRegionProfile - ) - } catch (e: any) { - downloadErrorMessage = (e as Error).message - getLogger().error(`Unit Test Generation: ExportResultArchive error = ${downloadErrorMessage}`) - throw new ExportResultsArchiveError(downloadErrorMessage) - } finally { - cwStreamingClient.destroy() - UserWrittenCodeTracker.instance.onQFeatureInvoked() - } -} diff --git a/packages/core/src/codewhisperer/service/transformByQ/humanInTheLoopManager.ts b/packages/core/src/codewhisperer/service/transformByQ/humanInTheLoopManager.ts index 63c1bfe3a2f..1646864e066 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/humanInTheLoopManager.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/humanInTheLoopManager.ts @@ -8,12 +8,19 @@ import path from 'path' import { FolderInfo, transformByQState } from '../../models/model' import fs from '../../../shared/fs/fs' import { createPomCopy, replacePomVersion } from './transformFileHandler' -import { IManifestFile } from '../../../amazonqFeatureDev/models' import { getLogger } from '../../../shared/logger/logger' import { telemetry } from '../../../shared/telemetry/telemetry' import { CodeTransformTelemetryState } from '../../../amazonqGumby/telemetry/codeTransformTelemetryState' import { MetadataResult } from '../../../shared/telemetry/telemetryClient' +export interface IManifestFile { + pomArtifactId: string + pomFolderName: string + hilCapability: string + pomGroupId: string + sourcePomVersion: string +} + /** * @description This class helps encapsulate the "human in the loop" behavior of Amazon Q transform. Users * will be prompted for input during the transformation process. Amazon Q will make some temporary folders diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts index e284207540d..c2934dc24ba 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts @@ -40,13 +40,12 @@ import { CodeTransformTelemetryState } from '../../../amazonqGumby/telemetry/cod import { calculateTotalLatency } from '../../../amazonqGumby/telemetry/codeTransformTelemetry' import { MetadataResult } from '../../../shared/telemetry/telemetryClient' import request from '../../../shared/request' -import { JobStoppedError, ZipExceedsSizeLimitError } from '../../../amazonqGumby/errors' +import { JobStoppedError } from '../../../amazonqGumby/errors' import { createLocalBuildUploadZip, extractOriginalProjectSources, writeAndShowBuildLogs } from './transformFileHandler' import { createCodeWhispererChatStreamingClient } from '../../../shared/clients/codewhispererChatClient' import { downloadExportResultArchive } from '../../../shared/utilities/download' import { ExportContext, ExportIntent, TransformationDownloadArtifactType } from '@amzn/codewhisperer-streaming' import fs from '../../../shared/fs/fs' -import { ChatSessionManager } from '../../../amazonqGumby/chat/storages/chatSession' import { encodeHTML } from '../../../shared/utilities/textUtilities' import { convertToTimeString } from '../../../shared/datetime' import { getAuthType } from '../../../auth/utils' @@ -55,7 +54,6 @@ import { setContext } from '../../../shared/vscode/setContext' import { AuthUtil } from '../../util/authUtil' import { DiffModel } from './transformationResultsViewProvider' import { spawnSync } from 'child_process' // eslint-disable-line no-restricted-imports -import { isClientSideBuildEnabled } from '../../../dev/config' export function getSha256(buffer: Buffer) { const hasher = crypto.createHash('sha256') @@ -70,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 @@ -187,12 +202,13 @@ export async function stopJob(jobId: string) { return } + getLogger().info(`CodeTransformation: Stopping transformation job with ID: ${jobId}`) + try { await codeWhisperer.codeWhispererClient.codeModernizerStopCodeTransformation({ transformationJobId: jobId, }) } catch (e: any) { - transformByQState.setJobFailureMetadata(` (request ID: ${e.requestId ?? 'unavailable'})`) getLogger().error(`CodeTransformation: StopTransformation error = %O`, e) throw new Error('Stop job failed') } @@ -218,7 +234,6 @@ export async function uploadPayload( }) } catch (e: any) { const errorMessage = `Creating the upload URL failed due to: ${(e as Error).message}` - transformByQState.setJobFailureMetadata(` (request ID: ${e.requestId ?? 'unavailable'})`) getLogger().error(`CodeTransformation: CreateUploadUrl error: = %O`, e) throw new Error(errorMessage) } @@ -277,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) => { @@ -286,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 { @@ -309,24 +330,20 @@ export function createZipManifest({ hilZipParams }: IZipManifestParams) { interface IZipCodeParams { dependenciesFolder?: FolderInfo - humanInTheLoopFlag?: boolean projectPath?: string zipManifest: ZipManifest | HilZipManifest } interface ZipCodeResult { - dependenciesCopied: boolean tempFilePath: string fileSize: number } export async function zipCode( - { dependenciesFolder, humanInTheLoopFlag, projectPath, zipManifest }: IZipCodeParams, + { dependenciesFolder, projectPath, zipManifest }: IZipCodeParams, zip: AdmZip = new AdmZip() ) { let tempFilePath = undefined - let logFilePath = undefined - let dependenciesCopied = false try { throwIfCancelled() @@ -384,65 +401,48 @@ export async function zipCode( continue } const relativePath = path.relative(dependenciesFolder.path, file) - // const paddedPath = path.join(`dependencies/${dependenciesFolder.name}`, relativePath) - const paddedPath = path.join(`dependencies/`, relativePath) - zip.addLocalFile(file, path.dirname(paddedPath)) + if (relativePath.includes('compilations.json')) { + let fileContents = await nodefs.promises.readFile(file, 'utf-8') + if (os.platform() === 'win32') { + fileContents = fileContents.replace(/\\\\/g, '/') + } + zip.addFile('compilations.json', Buffer.from(fileContents, 'utf-8')) + } else { + zip.addLocalFile(file, path.dirname(relativePath)) + } dependencyFilesSize += (await nodefs.promises.stat(file)).size } getLogger().info(`CodeTransformation: dependency files size = ${dependencyFilesSize}`) - dependenciesCopied = true } - // TO-DO: decide where exactly to put the YAML file / what to name it if (transformByQState.getCustomDependencyVersionFilePath() && zipManifest instanceof ZipManifest) { zip.addLocalFile( transformByQState.getCustomDependencyVersionFilePath(), - 'custom-upgrades', - 'dependency-versions.yaml' + 'sources', + 'dependency_upgrade.yml' ) + zipManifest.dependencyUpgradeConfigFile = 'dependency_upgrade.yml' } zip.addFile('manifest.json', Buffer.from(JSON.stringify(zipManifest)), 'utf-8') throwIfCancelled() - // add text file with logs from mvn clean install and mvn copy-dependencies - logFilePath = await writeAndShowBuildLogs() - // We don't add build-logs.txt file to the manifest if we are - // uploading HIL artifacts - if (!humanInTheLoopFlag) { - zip.addLocalFile(logFilePath) - } - tempFilePath = path.join(os.tmpdir(), 'zipped-code.zip') await fs.writeFile(tempFilePath, zip.toBuffer()) - if (dependenciesFolder && (await fs.exists(dependenciesFolder.path))) { + if (dependenciesFolder?.path) { await fs.delete(dependenciesFolder.path, { recursive: true, force: true }) } } catch (e: any) { getLogger().error(`CodeTransformation: zipCode error = ${e}`) throw Error('Failed to zip project') - } finally { - if (logFilePath) { - await fs.delete(logFilePath) - } } - const zipSize = (await nodefs.promises.stat(tempFilePath)).size - - const exceedsLimit = zipSize > CodeWhispererConstants.uploadZipSizeLimitInBytes + const fileSize = (await nodefs.promises.stat(tempFilePath)).size - getLogger().info(`CodeTransformation: created ZIP of size ${zipSize} at ${tempFilePath}`) + getLogger().info(`CodeTransformation: created ZIP of size ${fileSize} at ${tempFilePath}`) - if (exceedsLimit) { - void vscode.window.showErrorMessage(CodeWhispererConstants.projectSizeTooLargeNotification) - transformByQState.getChatControllers()?.transformationFinished.fire({ - message: CodeWhispererConstants.projectSizeTooLargeChatMessage, - tabID: ChatSessionManager.Instance.getSession().tabID, - }) - throw new ZipExceedsSizeLimitError() - } - return { dependenciesCopied: dependenciesCopied, tempFilePath: tempFilePath, fileSize: zipSize } as ZipCodeResult + return { tempFilePath: tempFilePath, fileSize: fileSize } as ZipCodeResult } export async function startJob(uploadId: string, profile: RegionProfile | undefined) { @@ -465,7 +465,6 @@ export async function startJob(uploadId: string, profile: RegionProfile | undefi return response.transformationJobId } catch (e: any) { const errorMessage = `Starting the job failed due to: ${(e as Error).message}` - transformByQState.setJobFailureMetadata(` (request ID: ${e.requestId ?? 'unavailable'})`) getLogger().error(`CodeTransformation: StartTransformation error = %O`, e) throw new Error(errorMessage) } @@ -652,12 +651,9 @@ export async function getTransformationPlan(jobId: string, profile: RegionProfil return plan } catch (e: any) { const errorMessage = (e as Error).message - transformByQState.setJobFailureMetadata(` (request ID: ${e.requestId ?? 'unavailable'})`) getLogger().error(`CodeTransformation: GetTransformationPlan error = %O`, e) - /* Means API call failed - * If response is defined, means a display/parsing error occurred, so continue transformation - */ + // GetTransformationPlan API call failed, but if response is defined, a display/parsing error occurred, so continue transformation if (response === undefined) { throw new Error(errorMessage) } @@ -672,7 +668,6 @@ export async function getTransformationSteps(jobId: string, profile: RegionProfi }) return response.transformationPlan.transformationSteps.slice(1) // skip step 0 (contains supplemental info) } catch (e: any) { - transformByQState.setJobFailureMetadata(` (request ID: ${e.requestId ?? 'unavailable'})`) getLogger().error(`CodeTransformation: GetTransformationPlan error = %O`, e) throw e } @@ -692,6 +687,9 @@ export async function pollTransformationJob(jobId: string, validStates: string[] if (CodeWhispererConstants.validStatesForBuildSucceeded.includes(status)) { jobPlanProgress['buildCode'] = StepProgress.Succeeded } + if (status === 'TRANSFORMING') { + transformByQState.setHasSeenTransforming(true) + } // emit metric when job status changes if (status !== transformByQState.getPolledJobStatus()) { telemetry.codeTransform_jobStatusChanged.emit({ @@ -728,16 +726,23 @@ export async function pollTransformationJob(jobId: string, validStates: string[] // final plan is complete; show to user isPlanComplete = true } + // for JDK upgrades without a YAML file, we show a static plan so no need to keep refreshing it + if ( + plan && + transformByQState.getSourceJDKVersion() !== transformByQState.getTargetJDKVersion() && + !transformByQState.getCustomDependencyVersionFilePath() + ) { + isPlanComplete = true + } } if (validStates.includes(status)) { break } - // TO-DO: remove isClientSideBuildEnabled when releasing CSB + // TO-DO: later, handle case where PlannerAgent needs to run mvn dependency:tree during PLANNING stage; not needed for now if ( - isClientSideBuildEnabled && - status === 'TRANSFORMING' && + transformByQState.getHasSeenTransforming() && transformByQState.getTransformationType() === TransformationType.LANGUAGE_UPGRADE ) { // client-side build is N/A for SQL conversions @@ -761,8 +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) - transformByQState.setJobFailureMetadata(` (request ID: ${e.requestId ?? 'unavailable'})`) + getLogger().error(`CodeTransformation: error = %O`, e) throw e } } @@ -844,17 +848,24 @@ async function processClientInstructions(jobId: string, clientInstructionsPath: const destinationPath = path.join(os.tmpdir(), `originalCopy_${jobId}_${artifactId}`) await extractOriginalProjectSources(destinationPath) getLogger().info(`CodeTransformation: copied project to ${destinationPath}`) - 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 diffContents = await fs.readFileText(clientInstructionsPath) + if (diffContents.trim()) { + // 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')) + getLogger().info(`CodeTransformation: diff.patch is empty`) + } await runClientSideBuild(transformByQState.getProjectCopyFilePath(), artifactId) } -export async function runClientSideBuild(projectCopyPath: string, clientInstructionArtifactId: string) { +export async function runClientSideBuild(projectCopyDir: string, clientInstructionArtifactId: string) { const baseCommand = transformByQState.getMavenName() - const args = [] + const args = ['clean'] if (transformByQState.getCustomBuildCommand() === CodeWhispererConstants.skipUnitTestsBuildCommand) { args.push('test-compile') } else { @@ -864,22 +875,22 @@ export async function runClientSideBuild(projectCopyPath: string, clientInstruct const argString = args.join(' ') const spawnResult = spawnSync(baseCommand, args, { - cwd: projectCopyPath, + cwd: projectCopyDir, shell: true, encoding: 'utf-8', env: environment, }) - const buildLogs = `Intermediate build result from running ${baseCommand} ${argString}:\n\n${spawnResult.stdout}` + const buildLogs = `Intermediate build result from running mvn ${argString}:\n\n${spawnResult.stdout}` transformByQState.clearBuildLog() transformByQState.appendToBuildLog(buildLogs) await writeAndShowBuildLogs() - const uploadZipBaseDir = path.join( + const uploadZipDir = path.join( os.tmpdir(), `clientInstructionsResult_${transformByQState.getJobId()}_${clientInstructionArtifactId}` ) - const uploadZipPath = await createLocalBuildUploadZip(uploadZipBaseDir, spawnResult.status, spawnResult.stdout) + const uploadZipPath = await createLocalBuildUploadZip(uploadZipDir, spawnResult.status, spawnResult.stdout) // upload build results const uploadContext: UploadContext = { @@ -892,10 +903,33 @@ export async function runClientSideBuild(projectCopyPath: string, clientInstruct try { await uploadPayload(uploadZipPath, AuthUtil.instance.regionProfileManager.activeRegionProfile, uploadContext) await resumeTransformationJob(transformByQState.getJobId(), 'COMPLETED') + } catch (err: any) { + getLogger().error(`CodeTransformation: upload client build results / resumeTransformation error = %O`, err) + transformByQState.setJobFailureErrorChatMessage( + `${CodeWhispererConstants.failedToCompleteJobGenericChatMessage} ${err.message}` + ) + transformByQState.setJobFailureErrorNotification( + `${CodeWhispererConstants.failedToCompleteJobGenericNotification} ${err.message}` + ) + // in case server-side execution times out, still call resumeTransformationJob + if (err.message.includes('find a step in desired state:AWAITING_CLIENT_ACTION')) { + getLogger().info('CodeTransformation: resuming job after server-side execution timeout') + await resumeTransformationJob(transformByQState.getJobId(), 'COMPLETED') + } else { + throw err + } } finally { - await fs.delete(projectCopyPath, { recursive: true }) - await fs.delete(uploadZipBaseDir, { recursive: true }) - getLogger().info(`CodeTransformation: Just deleted project copy and uploadZipBaseDir after client-side build`) + await fs.delete(projectCopyDir, { recursive: true }) + await fs.delete(uploadZipDir, { recursive: true }) + await fs.delete(uploadZipPath, { force: true }) + const exportZipDir = path.join( + os.tmpdir(), + `downloadClientInstructions_${transformByQState.getJobId()}_${clientInstructionArtifactId}` + ) + await fs.delete(exportZipDir, { recursive: true }) + getLogger().info( + `CodeTransformation: deleted projectCopy, clientInstructionsResult, and downloadClientInstructions directories/files` + ) } } diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts index fd74ca7b147..b16ea64022c 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts @@ -6,21 +6,25 @@ 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 -import { BuildSystem, DB, FolderInfo, TransformationType, transformByQState } from '../../models/model' -import { IManifestFile } from '../../../amazonqFeatureDev/models' +import { BuildSystem, DB, FolderInfo, transformByQState } from '../../models/model' import fs from '../../../shared/fs/fs' import globals from '../../../shared/extensionGlobals' import { ChatSessionManager } from '../../../amazonqGumby/chat/storages/chatSession' import { AbsolutePathDetectedError } from '../../../amazonqGumby/errors' import { getLogger } from '../../../shared/logger/logger' import AdmZip from 'adm-zip' +import { IManifestFile } from './humanInTheLoopManager' +import { ExportResultArchiveStructure } from '../../../shared/utilities/download' +import { isFileNotFoundError } from '../../../shared/errors' -export function getDependenciesFolderInfo(): FolderInfo { +export async function getDependenciesFolderInfo(): Promise { const dependencyFolderName = `${CodeWhispererConstants.dependencyFolderName}${globals.clock.Date.now()}` const dependencyFolderPath = path.join(os.tmpdir(), dependencyFolderName) + await fs.mkdir(dependencyFolderPath) return { name: dependencyFolderName, path: dependencyFolderPath, @@ -31,15 +35,12 @@ export async function writeAndShowBuildLogs(isLocalInstall: boolean = false) { const logFilePath = path.join(os.tmpdir(), 'build-logs.txt') writeFileSync(logFilePath, transformByQState.getBuildLog()) const doc = await vscode.workspace.openTextDocument(logFilePath) - if ( - !transformByQState.getBuildLog().includes('clean install succeeded') && - transformByQState.getTransformationType() !== TransformationType.SQL_CONVERSION - ) { + const logs = transformByQState.getBuildLog().toLowerCase() + if (logs.includes('intermediate build result') || logs.includes('maven jar failed')) { // only show the log if the build failed; show it in second column for intermediate builds only const options = isLocalInstall ? undefined : { viewColumn: vscode.ViewColumn.Two } await vscode.window.showTextDocument(doc, options) } - return logFilePath } export async function createLocalBuildUploadZip(baseDir: string, exitCode: number | null, stdout: string) { @@ -119,15 +120,64 @@ export async function parseBuildFile() { return undefined } -export async function validateCustomVersionsFile(fileContents: string) { - const requiredKeys = ['dependencyManagement:', 'identifier:', 'targetVersion:'] +// 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 false + return `Missing required key: \`${key}\`` } } - return true + 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 } export async function validateSQLMetadataFile(fileContents: string, message: any) { @@ -174,8 +224,7 @@ export async function validateSQLMetadataFile(fileContents: string, message: any } export function setMaven() { - // for now, just use regular Maven since the Maven executables can - // cause permissions issues when building if user has not ran 'chmod' + // avoid using maven wrapper since we can run into permissions issues transformByQState.setMavenName('mvn') } @@ -214,7 +263,6 @@ export async function getJsonValuesFromManifestFile( return { hilCapability: jsonValues?.hilType, pomFolderName: jsonValues?.pomFolderName, - // TODO remove this forced version sourcePomVersion: jsonValues?.sourcePomVersion || '1.0', pomArtifactId: jsonValues?.pomArtifactId, pomGroupId: jsonValues?.pomGroupId, @@ -351,3 +399,40 @@ export async function parseVersionsListFromPomFile(xmlString: string): Promise { - telemetry.record({ codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId() }) +function collectDependenciesAndMetadata(dependenciesFolderPath: string, workingDirPath: string) { + getLogger().info('CodeTransformation: running mvn clean test-compile with maven JAR') - // will always be 'mvn' - const baseCommand = transformByQState.getMavenName() + const baseCommand = transformByQState.getMavenName() // always 'mvn' + const jarPath = globals.context.asAbsolutePath(path.join('resources', 'amazonQCT', 'QCT-Maven-1-0-156-0.jar')) - const args = [`-Dmaven.repo.local=${dependenciesFolder.path}`, 'clean', 'install', '-q'] - - transformByQState.appendToBuildLog(`Running ${baseCommand} ${args.join(' ')}`) - - if (transformByQState.getCustomBuildCommand() === CodeWhispererConstants.skipUnitTestsBuildCommand) { - args.push('-DskipTests') - } - - let environment = process.env - - if (transformByQState.getSourceJavaHome()) { - environment = { ...process.env, JAVA_HOME: transformByQState.getSourceJavaHome() } - } - - const argString = args.join(' ') - const spawnResult = spawnSync(baseCommand, args, { - cwd: modulePath, - shell: true, - encoding: 'utf-8', - env: environment, - maxBuffer: CodeWhispererConstants.maxBufferSize, - }) - - const mavenBuildCommand = transformByQState.getMavenName() - telemetry.record({ codeTransformBuildCommand: mavenBuildCommand as CodeTransformBuildCommand }) - - if (spawnResult.status !== 0) { - let errorLog = '' - errorLog += spawnResult.error ? JSON.stringify(spawnResult.error) : '' - errorLog += `${spawnResult.stderr}\n${spawnResult.stdout}` - transformByQState.appendToBuildLog(`${baseCommand} ${argString} failed: \n ${errorLog}`) - getLogger().error( - `CodeTransformation: Error in running Maven command ${baseCommand} ${argString} = ${errorLog}` - ) - throw new ToolkitError(`Maven ${argString} error`, { code: 'MavenExecutionError' }) - } else { - transformByQState.appendToBuildLog(`mvn clean install succeeded`) - } - }) -} - -function copyProjectDependencies(dependenciesFolder: FolderInfo, modulePath: string) { - const baseCommand = transformByQState.getMavenName() + getLogger().info('CodeTransformation: running Maven extension with JAR') const args = [ - 'dependency:copy-dependencies', - `-DoutputDirectory=${dependenciesFolder.path}`, - '-Dmdep.useRepositoryLayout=true', - '-Dmdep.copyPom=true', - '-Dmdep.addParentPoms=true', - '-q', + `-Dmaven.ext.class.path="${jarPath}"`, + `-Dcom.amazon.aws.developer.transform.jobDirectory="${dependenciesFolderPath}"`, + 'clean', + 'test-compile', ] let environment = process.env - if (transformByQState.getSourceJavaHome()) { + if (transformByQState.getSourceJavaHome() !== undefined) { environment = { ...process.env, JAVA_HOME: transformByQState.getSourceJavaHome() } } const spawnResult = spawnSync(baseCommand, args, { - cwd: modulePath, + cwd: workingDirPath, shell: true, encoding: 'utf-8', env: environment, - maxBuffer: CodeWhispererConstants.maxBufferSize, }) + + getLogger().info( + `CodeTransformation: Ran mvn clean test-compile with maven JAR; status code = ${spawnResult.status}}` + ) + if (spawnResult.status !== 0) { let errorLog = '' errorLog += spawnResult.error ? JSON.stringify(spawnResult.error) : '' errorLog += `${spawnResult.stderr}\n${spawnResult.stdout}` - getLogger().info( - `CodeTransformation: Maven command ${baseCommand} ${args} failed, but still continuing with transformation: ${errorLog}` - ) - throw new Error('Maven copy-deps error') + errorLog = errorLog.toLowerCase().replace('elasticgumby', 'QCT') + transformByQState.appendToBuildLog(`mvn clean test-compile with maven JAR failed:\n${errorLog}`) + getLogger().error(`CodeTransformation: Error in running mvn clean test-compile with maven JAR = ${errorLog}`) + throw new Error('mvn clean test-compile with maven JAR failed') } + getLogger().info( + `CodeTransformation: mvn clean test-compile with maven JAR succeeded; dependencies copied to ${dependenciesFolderPath}` + ) } -export async function prepareProjectDependencies(dependenciesFolder: FolderInfo, rootPomPath: string) { +export async function prepareProjectDependencies(dependenciesFolderPath: string, workingDirPath: string) { setMaven() - getLogger().info('CodeTransformation: running Maven copy-dependencies') // pause to give chat time to update await sleep(100) try { - copyProjectDependencies(dependenciesFolder, rootPomPath) - } catch (err) { - // continue in case of errors - getLogger().info( - `CodeTransformation: Maven copy-dependencies failed, but transformation will continue and may succeed` - ) - } - - getLogger().info('CodeTransformation: running Maven install') - try { - installProjectDependencies(dependenciesFolder, rootPomPath) + collectDependenciesAndMetadata(dependenciesFolderPath, workingDirPath) } catch (err) { - void vscode.window.showErrorMessage(CodeWhispererConstants.cleanInstallErrorNotification) + getLogger().error('CodeTransformation: collectDependenciesAndMetadata failed') + void vscode.window.showErrorMessage(CodeWhispererConstants.cleanTestCompileErrorNotification) throw err } - throwIfCancelled() void vscode.window.showInformationMessage(CodeWhispererConstants.buildSucceededNotification) } -export async function getVersionData() { - const baseCommand = transformByQState.getMavenName() - const projectPath = transformByQState.getProjectPath() - const args = ['-v'] - const spawnResult = spawnSync(baseCommand, args, { cwd: projectPath, shell: true, encoding: 'utf-8' }) - - let localMavenVersion: string | undefined = '' - let localJavaVersion: string | undefined = '' - - try { - const localMavenVersionIndex = spawnResult.stdout.indexOf('Apache Maven') - const localMavenVersionString = spawnResult.stdout.slice(localMavenVersionIndex + 13).trim() - localMavenVersion = localMavenVersionString.slice(0, localMavenVersionString.indexOf(' ')).trim() - } catch (e: any) { - localMavenVersion = undefined // if this happens here or below, user most likely has JAVA_HOME incorrectly defined - } - - try { - const localJavaVersionIndex = spawnResult.stdout.indexOf('Java version: ') - const localJavaVersionString = spawnResult.stdout.slice(localJavaVersionIndex + 14).trim() - localJavaVersion = localJavaVersionString.slice(0, localJavaVersionString.indexOf(',')).trim() // will match value of JAVA_HOME - } catch (e: any) { - localJavaVersion = undefined - } - - getLogger().info( - `CodeTransformation: Ran ${baseCommand} to get Maven version = ${localMavenVersion} and Java version = ${localJavaVersion} with project JDK = ${transformByQState.getSourceJDKVersion()}` - ) - return [localMavenVersion, localJavaVersion] -} - export function runMavenDependencyUpdateCommands(dependenciesFolder: FolderInfo) { const baseCommand = transformByQState.getMavenName() diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformationHistoryHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformationHistoryHandler.ts new file mode 100644 index 00000000000..6aba818b4fc --- /dev/null +++ b/packages/core/src/codewhisperer/service/transformByQ/transformationHistoryHandler.ts @@ -0,0 +1,416 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import fs from '../../../shared/fs/fs' +import path from 'path' +import os from 'os' +import * as CodeWhispererConstants from '../../models/constants' +import { JDKVersion, TransformationType, transformByQState } from '../../models/model' +import { getLogger } from '../../../shared/logger/logger' +import { codeWhispererClient } from '../../../codewhisperer/client/codewhisperer' +import { downloadAndExtractResultArchive, pollTransformationJob } from './transformApiHandler' +import { ChatSessionManager } from '../../../amazonqGumby/chat/storages/chatSession' +import { AuthUtil } from '../../util/authUtil' +import { setMaven } from './transformFileHandler' +import { convertToTimeString, isWithin30Days } from '../../../shared/datetime' +import { copyArtifacts } from './transformFileHandler' + +export interface HistoryObject { + startTime: string + projectName: string + status: string + duration: string + diffPath: string + summaryPath: string + jobId: string + transformationType: string + sourceJDKVersion: string + targetJDKVersion: string + customDependencyVersionFilePath: string + customBuildCommand: string +} + +export interface JobMetadata { + jobId: string + projectName: string + transformationType: TransformationType + sourceJDKVersion: JDKVersion + targetJDKVersion: JDKVersion + customDependencyVersionFilePath: string + customBuildCommand: string + targetJavaHome: string + projectPath: string + startTime: string +} + +/** + * Reads 'transformation_history.tsv' (history) file + * + * @returns history array of 10 most recent jobs from within past 30 days + */ +export async function readHistoryFile(): Promise { + const history: HistoryObject[] = [] + const jobHistoryFilePath = path.join(os.homedir(), '.aws', 'transform', 'transformation_history.tsv') + + if (!(await fs.existsFile(jobHistoryFilePath))) { + return history + } + + const historyFile = await fs.readFileText(jobHistoryFilePath) + const jobs = historyFile.split('\n') + jobs.shift() // removes headers + + // Process from end, stop at 10 valid entries + for (let i = jobs.length - 1; i >= 0 && history.length < 10; i--) { + const job = jobs[i] + if (job && isWithin30Days(job.split('\t')[0])) { + const jobInfo = job.split('\t') + history.push({ + startTime: jobInfo[0], + projectName: jobInfo[1], + status: jobInfo[2], + duration: jobInfo[3], + diffPath: jobInfo[4], + summaryPath: jobInfo[5], + jobId: jobInfo[6], + transformationType: jobInfo[7], + sourceJDKVersion: jobInfo[8], + targetJDKVersion: jobInfo[9], + customDependencyVersionFilePath: jobInfo[10], + customBuildCommand: jobInfo[11], + }) + } + } + return history +} + +/** + * Creates temporary metadata JSON file with transformation config info and saves a copy of upload zip + * + * These files are used when a job is resumed after interruption + * + * @param payloadFilePath path to upload zip + * @param metadata + * @returns + */ +export async function createMetadataFile(payloadFilePath: string, metadata: JobMetadata): Promise { + const jobHistoryPath = path.join(os.homedir(), '.aws', 'transform', metadata.projectName, metadata.jobId) + + // create job history folders + await fs.mkdir(jobHistoryPath) + + // save a copy of the upload zip + try { + await fs.copy(payloadFilePath, path.join(jobHistoryPath, 'zipped-code.zip')) + } catch (error) { + getLogger().error('Code Transformation: error saving copy of upload zip: %s', (error as Error).message) + } + + // create metadata file with transformation config info + try { + await fs.writeFile(path.join(jobHistoryPath, 'metadata.json'), JSON.stringify(metadata)) + } catch (error) { + getLogger().error('Code Transformation: error creating metadata file: %s', (error as Error).message) + } + + return jobHistoryPath +} + +/** + * Writes job details to history file + * + * @param startTime job start timestamp (ex. "01/01/23, 12:00 AM") + * @param projectName + * @param status + * @param duration job duration in hr / min / sec format (ex. "1 hr 15 min") + * @param jobId + * @param jobHistoryPath path to where job's history details are stored (ex. "~/.aws/transform/proj_name/job_id") + */ +export async function writeToHistoryFile( + startTime: string, + projectName: string, + status: string, + duration: string, + jobId: 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\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 = [ + startTime, + projectName, + status, + duration, + 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' + await fs.appendFile(historyLogFilePath, jobDetails) + + // update Transformation Hub table + await vscode.commands.executeCommand('aws.amazonq.transformationHub.updateContent', 'job history', undefined, true) +} + +/** + * Delete temporary files at the end of a transformation + * + * @param jobHistoryPath path to history directory for this job + * @param jobStatus final transformation status + * @param payloadFilePath path to original upload zip; providing this param will also delete any temp build logs + */ +export async function cleanupTempJobFiles(jobHistoryPath: string, jobStatus: string, payloadFilePath?: string) { + if (payloadFilePath) { + // delete original upload ZIP + await fs.delete(payloadFilePath, { force: true }) + // delete temporary build logs file + const logFilePath = path.join(os.tmpdir(), 'build-logs.txt') + await fs.delete(logFilePath, { force: true }) + } + + // delete metadata file and upload zip copy if no longer need them (i.e. will not be resuming) + if (jobStatus !== 'FAILED') { + await fs.delete(path.join(jobHistoryPath, 'metadata.json'), { force: true }) + await fs.delete(path.join(jobHistoryPath, 'zipped-code.zip'), { force: true }) + } +} + +/* Job refresh-related functions */ + +export async function refreshJob(jobId: string, currentStatus: string, projectName: string) { + // fetch status from server + let status = '' + let duration = '' + if (currentStatus === 'COMPLETED' || currentStatus === 'PARTIALLY_COMPLETED') { + // job is already completed, no need to fetch status + status = currentStatus + } else { + try { + const response = await codeWhispererClient.codeModernizerGetCodeTransformation({ + transformationJobId: jobId, + profileArn: undefined, + }) + status = response.transformationJob.status ?? currentStatus + if (response.transformationJob.endExecutionTime && response.transformationJob.creationTime) { + duration = convertToTimeString( + response.transformationJob.endExecutionTime.getTime() - + response.transformationJob.creationTime.getTime() + ) + } + + getLogger().debug( + 'Code Transformation: Job refresh - Fetched status for job id: %s\n{Status: %s; Duration: %s}', + jobId, + status, + duration + ) + } catch (error) { + const errorMessage = (error as Error).message + getLogger().error('Code Transformation: Error fetching status (job id: %s): %s', jobId, errorMessage) + if (errorMessage.includes('not authorized to make this call')) { + // job not available on backend + status = 'FAILED' // won't allow retries for this job + } else { + // some other error (e.g. network error) + return + } + } + } + + // retrieve artifacts and updated duration if available + let jobHistoryPath: string = '' + if (status === 'COMPLETED' || status === 'PARTIALLY_COMPLETED') { + // artifacts should be available to download + jobHistoryPath = await retrieveArtifacts(jobId, projectName) + + await cleanupTempJobFiles(path.join(os.homedir(), '.aws', 'transform', projectName, jobId), status) + } else if (CodeWhispererConstants.validStatesForBuildSucceeded.includes(status)) { + // still in progress on server side + if (transformByQState.isRunning()) { + getLogger().warn( + 'Code Transformation: There is a job currently running (id: %s). Cannot resume another job (id: %s)', + transformByQState.getJobId(), + jobId + ) + return + } + transformByQState.setRefreshInProgress(true) + const messenger = transformByQState.getChatMessenger() + const tabID = ChatSessionManager.Instance.getSession().tabID + messenger?.sendJobRefreshInProgressMessage(tabID!, jobId) + await vscode.commands.executeCommand('aws.amazonq.transformationHub.updateContent', 'job history') // refreshing the table disables all jobs' refresh buttons while this one is resuming + + // resume job and bring to completion + try { + status = await resumeJob(jobId, projectName, status) + } catch (error) { + getLogger().error('Code Transformation: Error resuming job (id: %s): %s', jobId, (error as Error).message) + transformByQState.setJobDefaults() + messenger?.sendJobFinishedMessage(tabID!, CodeWhispererConstants.refreshErrorChatMessage) + void vscode.window.showErrorMessage(CodeWhispererConstants.refreshErrorNotification(jobId)) + await vscode.commands.executeCommand('aws.amazonq.transformationHub.updateContent', 'job history') + return + } + + // download artifacts if available + if ( + CodeWhispererConstants.validStatesForCheckingDownloadUrl.includes(status) && + !CodeWhispererConstants.failureStates.includes(status) + ) { + duration = convertToTimeString(Date.now() - new Date(transformByQState.getStartTime()).getTime()) + jobHistoryPath = await retrieveArtifacts(jobId, projectName) + } + + // reset state + transformByQState.setJobDefaults() + messenger?.sendJobFinishedMessage(tabID!, CodeWhispererConstants.refreshCompletedChatMessage) + } else { + // FAILED or STOPPED job + getLogger().info('Code Transformation: No artifacts available to download (job status = %s)', status) + if (status === 'FAILED') { + // if job failed on backend, mark it to disable the refresh button + status = 'FAILED_BE' // this will be truncated to just 'FAILED' in the table + } + await cleanupTempJobFiles(path.join(os.homedir(), '.aws', 'transform', projectName, jobId), status) + } + + if (status === currentStatus && !jobHistoryPath) { + // no changes, no need to update file/table + void vscode.window.showInformationMessage(CodeWhispererConstants.refreshNoUpdatesNotification(jobId)) + return + } + + void vscode.window.showInformationMessage(CodeWhispererConstants.refreshCompletedNotification(jobId)) + // update local file and history table + + await updateHistoryFile(status, duration, jobHistoryPath, jobId) +} + +async function retrieveArtifacts(jobId: string, projectName: string) { + const resultsPath = path.join(os.homedir(), '.aws', 'transform', projectName, 'results') // temporary directory for extraction + let jobHistoryPath = path.join(os.homedir(), '.aws', 'transform', projectName, jobId) + + if (await fs.existsFile(path.join(jobHistoryPath, 'diff.patch'))) { + getLogger().info('Code Transformation: Diff patch already exists for job id: %s', jobId) + jobHistoryPath = '' + } else { + try { + await downloadAndExtractResultArchive(jobId, resultsPath) + await copyArtifacts(resultsPath, jobHistoryPath) + } catch (error) { + jobHistoryPath = '' + } finally { + // delete temporary extraction directory + await fs.delete(resultsPath, { recursive: true, force: true }) + } + } + return jobHistoryPath +} + +async function updateHistoryFile(status: string, duration: string, jobHistoryPath: string, jobId: string) { + const history: string[][] = [] + const historyLogFilePath = path.join(os.homedir(), '.aws', 'transform', 'transformation_history.tsv') + if (await fs.existsFile(historyLogFilePath)) { + const historyFile = await fs.readFileText(historyLogFilePath) + const jobs = historyFile.split('\n') + jobs.shift() // removes headers + if (jobs.length > 0) { + for (const job of jobs) { + if (job) { + const jobInfo = job.split('\t') + // 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 + if (duration) { + jobInfo[3] = duration + } + if (jobHistoryPath) { + jobInfo[4] = path.join(jobHistoryPath, 'diff.patch') + jobInfo[5] = path.join(jobHistoryPath, 'summary', 'summary.md') + } + } + history.push(jobInfo) + } + } + } + } + + if (history.length === 0) { + return + } + + // rewrite file + 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) + + // update table content + await vscode.commands.executeCommand('aws.amazonq.transformationHub.updateContent', 'job history', undefined, true) +} + +async function resumeJob(jobId: string, projectName: string, status: string) { + // set state to prepare to resume job + await setupTransformationState(jobId, projectName, status) + // resume polling the job + return await pollAndCompleteTransformation(jobId) +} + +async function setupTransformationState(jobId: string, projectName: string, status: string) { + transformByQState.setJobId(jobId) + transformByQState.setPolledJobStatus(status) + transformByQState.setJobHistoryPath(path.join(os.homedir(), '.aws', 'transform', projectName, jobId)) + + const metadata: JobMetadata = JSON.parse( + await fs.readFileText(path.join(transformByQState.getJobHistoryPath(), 'metadata.json')) + ) + transformByQState.setTransformationType(metadata.transformationType) + transformByQState.setSourceJDKVersion(metadata.sourceJDKVersion) + transformByQState.setTargetJDKVersion(metadata.targetJDKVersion) + transformByQState.setCustomDependencyVersionFilePath(metadata.customDependencyVersionFilePath) + transformByQState.setPayloadFilePath( + path.join(os.homedir(), '.aws', 'transform', projectName, jobId, 'zipped-code.zip') + ) + setMaven() + transformByQState.setCustomBuildCommand(metadata.customBuildCommand) + transformByQState.setTargetJavaHome(metadata.targetJavaHome) + transformByQState.setProjectPath(metadata.projectPath) + transformByQState.setStartTime(metadata.startTime) +} + +async function pollAndCompleteTransformation(jobId: string) { + const status = await pollTransformationJob( + jobId, + CodeWhispererConstants.validStatesForCheckingDownloadUrl, + AuthUtil.instance.regionProfileManager.activeRegionProfile + ) + await cleanupTempJobFiles(transformByQState.getJobHistoryPath(), status, transformByQState.getPayloadFilePath()) + return status +} diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts b/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts index 052ef53b56c..97e69570c76 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts @@ -24,20 +24,26 @@ import { startInterval } from '../../commands/startTransformByQ' import { CodeTransformTelemetryState } from '../../../amazonqGumby/telemetry/codeTransformTelemetryState' import { convertToTimeString } from '../../../shared/datetime' import { AuthUtil } from '../../util/authUtil' +import { refreshJob, readHistoryFile, HistoryObject } from './transformationHistoryHandler' export class TransformationHubViewProvider implements vscode.WebviewViewProvider { public static readonly viewType = 'aws.amazonq.transformationHub' private _view?: vscode.WebviewView private lastClickedButton: string = '' private _extensionUri: vscode.Uri = globals.context.extensionUri + private transformationHistory: HistoryObject[] = [] constructor() {} static #instance: TransformationHubViewProvider public async updateContent( button: 'job history' | 'plan progress', - startTime: number = CodeTransformTelemetryState.instance.getStartTime() + startTime: number = CodeTransformTelemetryState.instance.getStartTime(), + historyFileUpdated?: boolean ) { this.lastClickedButton = button + if (historyFileUpdated) { + this.transformationHistory = await readHistoryFile() + } if (this._view) { if (this.lastClickedButton === 'job history') { clearInterval(transformByQState.getIntervalId()) @@ -62,18 +68,33 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider return (this.#instance ??= new this()) } - public resolveWebviewView( + public async resolveWebviewView( webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext, token: vscode.CancellationToken - ): void | Thenable { + ) { this._view = webviewView + this._view.webview.onDidReceiveMessage((message) => { + switch (message.command) { + case 'refreshJob': + void refreshJob(message.jobId, message.currentStatus, message.projectName) + break + case 'openSummaryPreview': + void vscode.commands.executeCommand('markdown.showPreview', vscode.Uri.file(message.filePath)) + break + case 'openDiffFile': + void vscode.commands.executeCommand('vscode.open', vscode.Uri.file(message.filePath)) + break + } + }) + this._view.webview.options = { enableScripts: true, localResourceRoots: [this._extensionUri], } + this.transformationHistory = await readHistoryFile() if (this.lastClickedButton === 'job history') { this._view!.webview.html = this.showJobHistory() } else { @@ -88,6 +109,24 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider } private showJobHistory(): string { + const jobsToDisplay: HistoryObject[] = [...this.transformationHistory] + if (transformByQState.isRunning()) { + const current = sessionJobHistory[transformByQState.getJobId()] + jobsToDisplay.unshift({ + startTime: current.startTime, + projectName: current.projectName, + status: current.status, + duration: current.duration, + diffPath: '', + summaryPath: '', + jobId: transformByQState.getJobId(), + transformationType: current.transformationType, + sourceJDKVersion: current.sourceJDKVersion, + targetJDKVersion: current.targetJDKVersion, + customDependencyVersionFilePath: current.customDependencyVersionsFilePath, + customBuildCommand: current.customBuildCommand, + }) + } return ` @@ -99,18 +138,70 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider -

Transformation Status

+

Transformation History

+

${CodeWhispererConstants.transformationHistoryTableDescription}

${ - Object.keys(sessionJobHistory).length === 0 - ? `

${CodeWhispererConstants.nothingToShowMessage}

` - : this.getTableMarkup(sessionJobHistory[transformByQState.getJobId()]) + jobsToDisplay.length === 0 + ? `


${CodeWhispererConstants.noJobHistoryMessage}

` + : this.getTableMarkup(jobsToDisplay) } + ` } - private getTableMarkup(job: { startTime: string; projectName: string; status: string; duration: string }) { + private getTableMarkup(history: HistoryObject[]) { return ` + @@ -118,17 +209,58 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider - + + + + + + + + + - - - - - - - + ${history + .map( + (job) => ` + + + + + + + + + + + + + + + + ` + ) + .join('')}
Project Status DurationIdDiff PatchSummary FileJob IdRefresh JobTransformation TypeSource JDK VersionTarget JDK VersionCustom Dependency Version File PathCustom Build Command
${job.startTime}${job.projectName}${job.status}${job.duration}${transformByQState.getJobId()}
${job.startTime}${job.projectName}${job.status === 'FAILED_BE' ? 'FAILED' : job.status}${job.duration}${job.diffPath ? `diff.patch` : ''}${job.summaryPath ? `summary.md` : ''}${job.jobId} + + ${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 e5de2099753..68d11b800fc 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts @@ -22,6 +22,7 @@ import { setContext } from '../../../shared/vscode/setContext' import * as codeWhisperer from '../../client/codewhisperer' import { UserWrittenCodeTracker } from '../../tracker/userWrittenCodeTracker' import { AuthUtil } from '../../util/authUtil' +import { copyArtifacts } from './transformFileHandler' export abstract class ProposedChangeNode { abstract readonly resourcePath: string @@ -165,7 +166,11 @@ export class DiffModel { throw new Error(CodeWhispererConstants.noChangesMadeMessage) } - const changedFiles = parsePatch(diffContents) + 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')) getLogger().info('CodeTransformation: parsed patch file successfully') // if doing intermediate client-side build, pathToWorkspace is the path to the unzipped project's 'sources' directory (re-using upload ZIP) // otherwise, we are at the very end of the transformation and need to copy the changed files in the project to show the diff(s) @@ -424,6 +429,7 @@ export class ProposedTransformationExplorer { let deserializeErrorMessage = undefined let pathContainingArchive = '' patchFiles = [] // reset patchFiles if there was a previous transformation + try { // Download and deserialize the zip pathContainingArchive = path.dirname(pathToArchive) @@ -439,6 +445,9 @@ export class ProposedTransformationExplorer { transformByQState.setSummaryFilePath( path.join(pathContainingArchive, ExportResultArchiveStructure.PathToSummary) ) + + await copyArtifacts(pathContainingArchive, transformByQState.getJobHistoryPath()) + transformByQState.setResultArchiveFilePath(pathContainingArchive) await setContext('gumby.isSummaryAvailable', true) 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/closingBracketUtil.ts b/packages/core/src/codewhisperer/util/closingBracketUtil.ts index 466ca31a0b9..4892c5694b4 100644 --- a/packages/core/src/codewhisperer/util/closingBracketUtil.ts +++ b/packages/core/src/codewhisperer/util/closingBracketUtil.ts @@ -1,6 +1,7 @@ /*! * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 + * Reference: https://github.com/aws/aws-toolkit-vscode/blob/amazonq/v1.74.0/packages/core/src/codewhisperer/util/closingBracketUtil.ts */ import * as vscode from 'vscode' 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/editorContext.ts b/packages/core/src/codewhisperer/util/editorContext.ts index 95df5eb509a..dacf3b326a1 100644 --- a/packages/core/src/codewhisperer/util/editorContext.ts +++ b/packages/core/src/codewhisperer/util/editorContext.ts @@ -22,6 +22,7 @@ import { indent } from '../../shared/utilities/textUtilities' import { isInDirectory } from '../../shared/filesystemUtilities' import { AuthUtil } from './authUtil' import { predictionTracker } from '../nextEditPrediction/activation' +import { LanguageClient } from 'vscode-languageclient' let tabSize: number = getTabSizeSetting() @@ -224,7 +225,8 @@ async function getWorkspaceId(editor: vscode.TextEditor): Promise | undefined> { const supplementalContextConfig = getSupplementalContextConfig(editor.document.languageId) @@ -102,7 +108,7 @@ export async function fetchSupplementalContextForSrc( async function () { const result: CodeWhispererSupplementalContextItem[] = [] const opentabsContext = await fetchOpentabsContext(editor, cancellationToken) - const codemap = await fetchProjectContext(editor, 'codemap') + const codemap = await fetchProjectContext(editor, 'codemap', languageClient) function addToResult(items: CodeWhispererSupplementalContextItem[]) { for (const item of items) { @@ -146,7 +152,7 @@ export async function fetchSupplementalContextForSrc( if (supplementalContextConfig === 'bm25') { const projectBM25Promise = waitUntil( async function () { - return await fetchProjectContext(editor, 'bm25') + return await fetchProjectContext(editor, 'bm25', languageClient) }, { timeout: supplementalContextTimeoutInMs, interval: 5, truthy: false } ) @@ -169,7 +175,7 @@ export async function fetchSupplementalContextForSrc( // global bm25 with repomap const projectContextAndCodemapPromise = waitUntil( async function () { - return await fetchProjectContext(editor, 'default') + return await fetchProjectContext(editor, 'default', languageClient) }, { timeout: supplementalContextTimeoutInMs, interval: 5, truthy: false } ) @@ -193,18 +199,24 @@ export async function fetchSupplementalContextForSrc( export async function fetchProjectContext( editor: vscode.TextEditor, - target: 'default' | 'codemap' | 'bm25' + target: 'default' | 'codemap' | 'bm25', + languageclient?: LanguageClient ): Promise { - const inputChunkContent = getInputChunk(editor) - - const inlineProjectContext: { content: string; score: number; filePath: string }[] = - await LspController.instance.queryInlineProjectContext( - inputChunkContent.content, - editor.document.uri.fsPath, - target - ) - - return inlineProjectContext + 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( diff --git a/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts b/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts index bd214ace44e..edda43ddcf6 100644 --- a/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts +++ b/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts @@ -13,12 +13,14 @@ import { getLogger } from '../../../shared/logger/logger' import { CodeWhispererSupplementalContext } from '../../models/model' import * as os from 'os' import { crossFileContextConfig } from '../../models/constants' +import { LanguageClient } from 'vscode-languageclient' export async function fetchSupplementalContext( editor: vscode.TextEditor, - cancellationToken: vscode.CancellationToken + cancellationToken: vscode.CancellationToken, + languageClient?: LanguageClient ): Promise { - const timesBeforeFetching = performance.now() + const timesBeforeFetching = Date.now() const isUtg = await isTestFile(editor.document.uri.fsPath, { languageId: editor.document.languageId, @@ -32,7 +34,7 @@ export async function fetchSupplementalContext( if (isUtg) { supplementalContextPromise = fetchSupplementalContextForTest(editor, cancellationToken) } else { - supplementalContextPromise = fetchSupplementalContextForSrc(editor, cancellationToken) + supplementalContextPromise = fetchSupplementalContextForSrc(editor, cancellationToken, languageClient) } return supplementalContextPromise @@ -45,7 +47,7 @@ export async function fetchSupplementalContext( (item) => item.content.trim().length !== 0 ), contentsLength: value.supplementalContextItems.reduce((acc, curr) => acc + curr.content.length, 0), - latency: performance.now() - timesBeforeFetching, + latency: Date.now() - timesBeforeFetching, strategy: value.strategy, } @@ -61,7 +63,7 @@ export async function fetchSupplementalContext( isProcessTimeout: true, supplementalContextItems: [], contentsLength: 0, - latency: performance.now() - timesBeforeFetching, + latency: Date.now() - timesBeforeFetching, strategy: 'empty', } } else { diff --git a/packages/core/src/codewhisperer/util/telemetryHelper.ts b/packages/core/src/codewhisperer/util/telemetryHelper.ts index 060a5ecb282..72f88ab9dc2 100644 --- a/packages/core/src/codewhisperer/util/telemetryHelper.ts +++ b/packages/core/src/codewhisperer/util/telemetryHelper.ts @@ -13,7 +13,6 @@ import { CodewhispererPreviousSuggestionState, CodewhispererUserDecision, CodewhispererUserTriggerDecision, - Status, telemetry, } from '../../shared/telemetry/telemetry' import { CodewhispererCompletionType, CodewhispererSuggestionState } from '../../shared/telemetry/telemetry' @@ -28,7 +27,6 @@ import { CodeWhispererSupplementalContext } from '../models/model' import { FeatureConfigProvider } from '../../shared/featureConfig' import CodeWhispererUserClient, { CodeScanRemediationsEventType } from '../client/codewhispereruserclient' import { CodeAnalysisScope as CodeAnalysisScopeClientSide } from '../models/constants' -import { Session } from '../../amazonqTest/chat/session/session' import { sleep } from '../../shared/utilities/timeoutUtils' import { getDiagnosticsDifferences, getDiagnosticsOfCurrentFile, toIdeDiagnostics } from './diagnosticsUtil' import { Auth } from '../../auth/auth' @@ -71,54 +69,6 @@ export class TelemetryHelper { return (this.#instance ??= new this()) } - public sendTestGenerationToolkitEvent( - session: Session, - isSupportedLanguage: boolean, - isFileInWorkspace: boolean, - result: 'Succeeded' | 'Failed' | 'Cancelled', - requestId?: string, - perfClientLatency?: number, - reasonDesc?: string, - isCodeBlockSelected?: boolean, - artifactsUploadDuration?: number, - buildPayloadBytes?: number, - buildZipFileBytes?: number, - acceptedCharactersCount?: number, - acceptedCount?: number, - acceptedLinesCount?: number, - generatedCharactersCount?: number, - generatedCount?: number, - generatedLinesCount?: number, - reason?: string, - status?: Status - ) { - telemetry.amazonq_utgGenerateTests.emit({ - cwsprChatProgrammingLanguage: session.fileLanguage ?? 'plaintext', - hasUserPromptSupplied: session.hasUserPromptSupplied, - isSupportedLanguage: session.isSupportedLanguage, - isFileInWorkspace: isFileInWorkspace, - result: result, - artifactsUploadDuration: artifactsUploadDuration, - buildPayloadBytes: buildPayloadBytes, - buildZipFileBytes: buildZipFileBytes, - credentialStartUrl: AuthUtil.instance.startUrl, - acceptedCharactersCount: acceptedCharactersCount, - acceptedCount: acceptedCount, - acceptedLinesCount: acceptedLinesCount, - generatedCharactersCount: generatedCharactersCount, - generatedCount: generatedCount, - generatedLinesCount: generatedLinesCount, - isCodeBlockSelected: isCodeBlockSelected, - jobGroup: session.testGenerationJobGroupName, - jobId: session.listOfTestGenerationJobId[0], - perfClientLatency: perfClientLatency, - requestId: requestId, - reasonDesc: reasonDesc, - reason: reason, - status: status, - }) - } - public recordServiceInvocationTelemetry( requestId: string, sessionId: string, @@ -191,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, @@ -405,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, @@ -416,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. @@ -442,6 +392,7 @@ export class TelemetryHelper { generatedLine: generatedLines, numberOfRecommendations: suggestionCount, acceptedCharacterCount: acceptedRecommendationContent.length, + suggestionType: 'COMPLETIONS', } this.resetUserTriggerDecisionTelemetry() @@ -478,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 } } @@ -606,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/codewhisperer/util/zipUtil.ts b/packages/core/src/codewhisperer/util/zipUtil.ts index 32687a6452c..719116efdc7 100644 --- a/packages/core/src/codewhisperer/util/zipUtil.ts +++ b/packages/core/src/codewhisperer/util/zipUtil.ts @@ -4,7 +4,7 @@ */ import * as vscode from 'vscode' import path from 'path' -import { tempDirPath, testGenerationLogsDir } from '../../shared/filesystemUtilities' +import { tempDirPath } from '../../shared/filesystemUtilities' import { getLogger } from '../../shared/logger/logger' import * as CodeWhispererConstants from '../models/constants' import { ToolkitError } from '../../shared/errors' @@ -21,7 +21,6 @@ import { } from '../models/errors' import { FeatureUseCase } from '../models/constants' import { ChildProcess, ChildProcessOptions } from '../../shared/utilities/processUtils' -import { ProjectZipError } from '../../amazonqTest/error' import { removeAnsi } from '../../shared/utilities/textUtilities' import { normalize } from '../../shared/utilities/pathUtils' import { ZipStream } from '../../shared/utilities/zipStream' @@ -570,56 +569,6 @@ export class ZipUtil { } } - public async generateZipTestGen(projectPath: string, initialExecution: boolean): Promise { - try { - // const repoMapFile = await LspClient.instance.getRepoMapJSON() - const zipDirPath = this.getZipDirPath(FeatureUseCase.TEST_GENERATION) - - const metadataDir = path.join(zipDirPath, 'utgRequiredArtifactsDir') - - // Create directories - const dirs = { - metadata: metadataDir, - buildAndExecuteLogDir: path.join(metadataDir, 'buildAndExecuteLogDir'), - repoMapDir: path.join(metadataDir, 'repoMapData'), - testCoverageDir: path.join(metadataDir, 'testCoverageDir'), - } - await Promise.all(Object.values(dirs).map((dir) => fs.mkdir(dir))) - - // if (await fs.exists(repoMapFile)) { - // await fs.copy(repoMapFile, path.join(dirs.repoMapDir, 'repoMapData.json')) - // await fs.delete(repoMapFile) - // } - - if (!initialExecution) { - await this.processTestCoverageFiles(dirs.testCoverageDir) - - const sourcePath = path.join(testGenerationLogsDir, 'output.log') - const targetPath = path.join(dirs.buildAndExecuteLogDir, 'output.log') - if (await fs.exists(sourcePath)) { - await fs.copy(sourcePath, targetPath) - } - } - - const zipFilePath: string = await this.zipProject(FeatureUseCase.TEST_GENERATION, projectPath, metadataDir) - const zipFileSize = (await fs.stat(zipFilePath)).size - return { - rootDir: zipDirPath, - zipFilePath: zipFilePath, - srcPayloadSizeInBytes: this._totalSize, - scannedFiles: new Set(this._pickedSourceFiles), - zipFileSizeInBytes: zipFileSize, - buildPayloadSizeInBytes: this._totalBuildSize, - lines: this._totalLines, - language: this._language, - } - } catch (error) { - getLogger().error('Zip error caused by: %s', error) - throw new ProjectZipError( - error instanceof Error ? error.message : 'Unknown error occurred during zip operation' - ) - } - } // TODO: Refactor this public async removeTmpFiles(zipMetadata: ZipMetadata, scope?: CodeWhispererConstants.CodeAnalysisScope) { const logger = getLoggerForScope(scope) diff --git a/packages/core/src/codewhisperer/views/activeStateController.ts b/packages/core/src/codewhisperer/views/activeStateController.ts index b3c991a9d38..614003d02ff 100644 --- a/packages/core/src/codewhisperer/views/activeStateController.ts +++ b/packages/core/src/codewhisperer/views/activeStateController.ts @@ -6,13 +6,9 @@ import * as vscode from 'vscode' import { LineSelection, LinesChangeEvent } from '../tracker/lineTracker' import { isTextEditor } from '../../shared/utilities/editorUtilities' -import { RecommendationService, SuggestionActionEvent } from '../service/recommendationService' import { subscribeOnce } from '../../shared/utilities/vsCodeUtils' import { Container } from '../service/serviceContainer' -import { RecommendationHandler } from '../service/recommendationHandler' import { cancellableDebounce } from '../../shared/utilities/functionUtils' -import { telemetry } from '../../shared/telemetry/telemetry' -import { TelemetryHelper } from '../util/telemetryHelper' export class ActiveStateController implements vscode.Disposable { private readonly _disposable: vscode.Disposable @@ -34,14 +30,6 @@ export class ActiveStateController implements vscode.Disposable { constructor(private readonly container: Container) { this._disposable = vscode.Disposable.from( - RecommendationService.instance.suggestionActionEvent(async (e) => { - await telemetry.withTraceId(async () => { - await this.onSuggestionActionEvent(e) - }, TelemetryHelper.instance.traceId) - }), - RecommendationHandler.instance.onDidReceiveRecommendation(async (_) => { - await this.onDidReceiveRecommendation() - }), this.container.lineTracker.onDidChangeActiveLines(async (e) => { await this.onActiveLinesChanged(e) }), @@ -70,33 +58,6 @@ export class ActiveStateController implements vscode.Disposable { await this._refresh(vscode.window.activeTextEditor) } - private async onSuggestionActionEvent(e: SuggestionActionEvent) { - if (!this._isReady) { - return - } - - this.clear(e.editor) // do we need this? - if (e.triggerType === 'OnDemand' && e.isRunning) { - // if user triggers on demand, immediately update the UI and cancel the previous debounced update if there is one - this.refreshDebounced.cancel() - await this._refresh(this._editor) - } else { - await this.refreshDebounced.promise(e.editor) - } - } - - private async onDidReceiveRecommendation() { - if (!this._isReady) { - return - } - - if (this._editor && this._editor === vscode.window.activeTextEditor) { - // receives recommendation, immediately update the UI and cacnel the debounced update if there is one - this.refreshDebounced.cancel() - await this._refresh(this._editor, false) - } - } - private async onActiveLinesChanged(e: LinesChangeEvent) { if (!this._isReady) { return @@ -147,7 +108,7 @@ export class ActiveStateController implements vscode.Disposable { if (shouldDisplay !== undefined) { await this.updateDecorations(editor, selections, shouldDisplay) } else { - await this.updateDecorations(editor, selections, RecommendationService.instance.isRunning) + await this.updateDecorations(editor, selections, true) } } diff --git a/packages/core/src/codewhisperer/views/lineAnnotationController.ts b/packages/core/src/codewhisperer/views/lineAnnotationController.ts index 8b1d38ed7ae..c449f5ab1d9 100644 --- a/packages/core/src/codewhisperer/views/lineAnnotationController.ts +++ b/packages/core/src/codewhisperer/views/lineAnnotationController.ts @@ -9,18 +9,13 @@ import { LineSelection, LinesChangeEvent } from '../tracker/lineTracker' import { isTextEditor } from '../../shared/utilities/editorUtilities' import { cancellableDebounce } from '../../shared/utilities/functionUtils' import { subscribeOnce } from '../../shared/utilities/vsCodeUtils' -import { RecommendationService } from '../service/recommendationService' import { AnnotationChangeSource, inlinehintKey } from '../models/constants' import globals from '../../shared/extensionGlobals' import { Container } from '../service/serviceContainer' import { telemetry } from '../../shared/telemetry/telemetry' import { getLogger } from '../../shared/logger/logger' -import { Commands } from '../../shared/vscode/commands2' -import { session } from '../util/codeWhispererSession' -import { RecommendationHandler } from '../service/recommendationHandler' import { runtimeLanguageContext } from '../util/runtimeLanguageContext' import { setContext } from '../../shared/vscode/setContext' -import { TelemetryHelper } from '../util/telemetryHelper' const case3TimeWindow = 30000 // 30 seconds @@ -75,13 +70,7 @@ export class AutotriggerState implements AnnotationState { static acceptedCount = 0 updateState(changeSource: AnnotationChangeSource, force: boolean): AnnotationState | undefined { - if (AutotriggerState.acceptedCount < RecommendationService.instance.acceptedSuggestionCount) { - return new ManualtriggerState() - } else if (session.recommendations.length > 0 && RecommendationHandler.instance.isSuggestionVisible()) { - return new PressTabState() - } else { - return this - } + return undefined } isNextState(state: AnnotationState | undefined): boolean { @@ -268,28 +257,6 @@ export class LineAnnotationController implements vscode.Disposable { subscribeOnce(this.container.lineTracker.onReady)(async (_) => { await this.onReady() }), - RecommendationService.instance.suggestionActionEvent(async (e) => { - await telemetry.withTraceId(async () => { - if (!this._isReady) { - return - } - - if (this._currentState instanceof ManualtriggerState) { - if (e.triggerType === 'OnDemand' && this._currentState.hasManualTrigger === false) { - this._currentState.hasManualTrigger = true - } - if ( - e.response?.recommendationCount !== undefined && - e.response?.recommendationCount > 0 && - this._currentState.hasValidResponse === false - ) { - this._currentState.hasValidResponse = true - } - } - - await this.refresh(e.editor, 'codewhisperer') - }, TelemetryHelper.instance.traceId) - }), this.container.lineTracker.onDidChangeActiveLines(async (e) => { await this.onActiveLinesChanged(e) }), @@ -300,17 +267,6 @@ export class LineAnnotationController implements vscode.Disposable { }), this.container.auth.secondaryAuth.onDidChangeActiveConnection(async () => { await this.refresh(vscode.window.activeTextEditor, 'editor') - }), - Commands.register('aws.amazonq.dismissTutorial', async () => { - const editor = vscode.window.activeTextEditor - if (editor) { - this.clear() - try { - telemetry.ui_click.emit({ elementId: `dismiss_${this._currentState.id}` }) - } catch (_) {} - await this.dismissTutorial() - getLogger().debug(`codewhisperer: user dismiss tutorial.`) - } }) ) } @@ -484,7 +440,7 @@ export class LineAnnotationController implements vscode.Disposable { source: AnnotationChangeSource, force?: boolean ): Partial | undefined { - const isCWRunning = RecommendationService.instance.isRunning + const isCWRunning = true const textOptions: vscode.ThemableDecorationAttachmentRenderOptions = { contentText: '', @@ -517,9 +473,9 @@ export class LineAnnotationController implements vscode.Disposable { this._currentState = updatedState // take snapshot of accepted session so that we can compre if there is delta -> users accept 1 suggestion after seeing this state - AutotriggerState.acceptedCount = RecommendationService.instance.acceptedSuggestionCount + AutotriggerState.acceptedCount = 0 // take snapshot of total trigger count so that we can compare if there is delta -> users accept/reject suggestions after seeing this state - TryMoreExState.triggerCount = RecommendationService.instance.totalValidTriggerCount + TryMoreExState.triggerCount = 0 textOptions.contentText = this._currentState.text() diff --git a/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts b/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts index d511bd9a5f6..632283215ab 100644 --- a/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts +++ b/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts @@ -109,7 +109,10 @@ export class SecurityIssueWebview extends VueWebview { } public generateFix() { - void vscode.commands.executeCommand('aws.amazonq.security.generateFix', this.issue, this.filePath, 'webview') + const args = [this.issue] + void this.navigateToFile()?.then(() => { + void vscode.commands.executeCommand('aws.amazonq.generateFix', ...args) + }) } public regenerateFix() { diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index ba2072eb6dc..1be0c0332f5 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -59,7 +59,6 @@ import { triggerPayloadToChatRequest } from './chatRequest/converter' import { AuthUtil } from '../../../codewhisperer/util/authUtil' import { openUrl } from '../../../shared/utilities/vsCodeUtils' import { randomUUID } from '../../../shared/crypto' -import { LspController } from '../../../amazonq/lsp/lspController' import { CodeWhispererSettings } from '../../../codewhisperer/util/codewhispererSettings' import { getSelectedCustomization } from '../../../codewhisperer/util/customizationUtil' import { getHttpStatusCode, AwsClientResponseError } from '../../../shared/errors' @@ -70,8 +69,6 @@ import { inspect } from '../../../shared/utilities/collectionUtils' import { DefaultAmazonQAppInitContext } from '../../../amazonq/apps/initContext' import globals from '../../../shared/extensionGlobals' import { MynahIconsType, MynahUIDataModel, QuickActionCommand } from '@aws/mynah-ui' -import { LspClient } from '../../../amazonq/lsp/lspClient' -import { AdditionalContextPrompt, ContextCommandItem, ContextCommandItemType } from '../../../amazonq/lsp/types' import { workspaceCommand } from '../../../amazonq/webview/ui/tabs/constants' import fs from '../../../shared/fs/fs' import { FeatureConfigProvider, Features } from '../../../shared/featureConfig' @@ -80,9 +77,6 @@ import { getUserPromptsDirectory, promptFileExtension, createSavedPromptCommandId, - aditionalContentNameLimit, - additionalContentInnerContextLimit, - workspaceChunkMaxSize, defaultContextLengths, } from '../../constants' import { ChatSession } from '../../clients/chat/v0/chat' @@ -527,7 +521,6 @@ export class ChatController { commands: [{ command: commandName, description: commandDescription }], }) } - const symbolsCmd: QuickActionCommand = contextCommand[0].commands?.[3] const promptsCmd: QuickActionCommand = contextCommand[0].commands?.[4] // Check for user prompts @@ -543,7 +536,7 @@ export class ChatController { command: path.basename(name, promptFileExtension), icon: 'magic' as MynahIconsType, id: 'prompt', - label: 'file' as ContextCommandItemType, + // label: 'file' as ContextCommandItemType, route: [userPromptsDirectory, name], })) ) @@ -559,47 +552,7 @@ export class ChatController { icon: 'list-add' as MynahIconsType, }) - const lspClientReady = await LspClient.instance.waitUntilReady() - if (lspClientReady) { - const contextCommandItems = await LspClient.instance.getContextCommandItems() - const folderCmd: QuickActionCommand = contextCommand[0].commands?.[1] - const filesCmd: QuickActionCommand = contextCommand[0].commands?.[2] - - for (const contextCommandItem of contextCommandItems) { - const wsFolderName = path.basename(contextCommandItem.workspaceFolder) - if (contextCommandItem.type === 'file') { - filesCmd.children?.[0].commands.push({ - command: path.basename(contextCommandItem.relativePath), - description: path.join(wsFolderName, contextCommandItem.relativePath), - route: [contextCommandItem.workspaceFolder, contextCommandItem.relativePath], - label: 'file' as ContextCommandItemType, - id: contextCommandItem.id, - icon: 'file' as MynahIconsType, - }) - } else if (contextCommandItem.type === 'folder') { - folderCmd.children?.[0].commands.push({ - command: path.basename(contextCommandItem.relativePath), - description: path.join(wsFolderName, contextCommandItem.relativePath), - route: [contextCommandItem.workspaceFolder, contextCommandItem.relativePath], - label: 'folder' as ContextCommandItemType, - id: contextCommandItem.id, - icon: 'folder' as MynahIconsType, - }) - } else if (contextCommandItem.symbol && symbolsCmd.children) { - symbolsCmd.children?.[0].commands.push({ - command: contextCommandItem.symbol.name, - description: `${contextCommandItem.symbol.kind}, ${path.join(wsFolderName, contextCommandItem.relativePath)}, L${contextCommandItem.symbol.range.start.line}-${contextCommandItem.symbol.range.end.line}`, - route: [contextCommandItem.workspaceFolder, contextCommandItem.relativePath], - label: 'code' as ContextCommandItemType, - id: contextCommandItem.id, - icon: 'code-block' as MynahIconsType, - }) - } - } - } - this.messenger.sendContextCommandData(contextCommand) - void LspController.instance.updateContextCommandSymbolsOnce() } private handlePromptCreate(tabID: string) { @@ -1006,7 +959,7 @@ export class ChatController { } private async resolveContextCommandPayload(triggerPayload: TriggerPayload, session: ChatSession) { - const contextCommands: ContextCommandItem[] = [] + const contextCommands: any[] = [] // Check for workspace rules to add to context const workspaceRules = await this.collectWorkspaceRules() @@ -1017,7 +970,7 @@ export class ChatController { vscode.workspace.getWorkspaceFolder(vscode.Uri.parse(rule))?.uri?.path || '' return { workspaceFolder: workspaceFolderPath, - type: 'file' as ContextCommandItemType, + type: 'file' as any, relativePath: path.relative(workspaceFolderPath, rule), } }) @@ -1029,7 +982,7 @@ export class ChatController { if (typeof context !== 'string' && context.route && context.route.length === 2) { contextCommands.push({ workspaceFolder: context.route[0] || '', - type: (context.label || '') as ContextCommandItemType, + type: (context.label || '') as any, relativePath: context.route[1] || '', id: context.id, }) @@ -1044,45 +997,6 @@ export class ChatController { return [] } workspaceFolders.sort() - const workspaceFolder = workspaceFolders[0] - for (const contextCommand of contextCommands) { - session.relativePathToWorkspaceRoot.set(contextCommand.workspaceFolder, contextCommand.workspaceFolder) - } - let prompts: AdditionalContextPrompt[] = [] - try { - prompts = await LspClient.instance.getContextCommandPrompt(contextCommands) - } catch (e) { - // todo: handle @workspace used before indexing is ready - getLogger().verbose(`Could not get context command prompts: ${e}`) - } - - triggerPayload.contextLengths.additionalContextLengths = this.telemetryHelper.getContextLengths(prompts) - for (const prompt of prompts.slice(0, 20)) { - // Add system prompt for user prompts and workspace rules - const contextType = this.telemetryHelper.getContextType(prompt) - const description = - contextType === 'rule' || contextType === 'prompt' - ? `You must follow the instructions in ${prompt.relativePath}. Below are lines ${prompt.startLine}-${prompt.endLine} of this file:\n` - : prompt.description - - // Handle user prompts outside the workspace - const relativePath = prompt.filePath.startsWith(getUserPromptsDirectory()) - ? path.basename(prompt.filePath) - : path.relative(workspaceFolder, prompt.filePath) - - const entry = { - name: prompt.name.substring(0, aditionalContentNameLimit), - description: description.substring(0, aditionalContentNameLimit), - innerContext: prompt.content.substring(0, additionalContentInnerContextLimit), - type: contextType, - relativePath: relativePath, - startLine: prompt.startLine, - endLine: prompt.endLine, - } - - triggerPayload.additionalContents.push(entry) - } - getLogger().info(`Retrieved chunks of additional context count: ${triggerPayload.additionalContents.length} `) } private async generateResponse( @@ -1130,25 +1044,6 @@ export class ChatController { if (triggerPayload.useRelevantDocuments) { triggerPayload.message = triggerPayload.message.replace(/@workspace/, '') if (CodeWhispererSettings.instance.isLocalIndexEnabled()) { - const start = performance.now() - const relevantTextDocuments = await LspController.instance.query(triggerPayload.message) - for (const relevantDocument of relevantTextDocuments) { - if (relevantDocument.text && relevantDocument.text.length > 0) { - triggerPayload.contextLengths.workspaceContextLength += relevantDocument.text.length - if (relevantDocument.text.length > workspaceChunkMaxSize) { - relevantDocument.text = relevantDocument.text.substring(0, workspaceChunkMaxSize) - getLogger().debug(`Truncating @workspace chunk: ${relevantDocument.relativeFilePath} `) - } - triggerPayload.relevantTextDocuments.push(relevantDocument) - } - } - - for (const doc of triggerPayload.relevantTextDocuments) { - getLogger().info( - `amazonq: Using workspace files ${doc.relativeFilePath}, content(partial): ${doc.text?.substring(0, 200)}, start line: ${doc.startLine}, end line: ${doc.endLine}` - ) - } - triggerPayload.projectContextQueryLatencyMs = performance.now() - start } else { this.messenger.sendOpenSettingsMessage(triggerID, tabID) return diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index 8c914686ad4..ab059ecb22d 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -40,7 +40,6 @@ import { FeatureAuthState } from '../../../../codewhisperer/util/authUtil' import { CodeScanIssue } from '../../../../codewhisperer/models/model' import { marked } from 'marked' import { JSDOM } from 'jsdom' -import { LspController } from '../../../../amazonq/lsp/lspController' import { extractCodeBlockLanguage } from '../../../../shared/markdown' import { extractAuthFollowUp } from '../../../../amazonq/util/authUtils' import { helpMessage } from '../../../../amazonq/webview/ui/texts/constants' @@ -290,11 +289,7 @@ export class Messenger { relatedContent: { title: 'Sources', content: relatedSuggestions as any }, }) } - if ( - triggerPayload.relevantTextDocuments && - triggerPayload.relevantTextDocuments.length > 0 && - LspController.instance.isIndexingInProgress() - ) { + if (triggerPayload.relevantTextDocuments && triggerPayload.relevantTextDocuments.length > 0) { this.dispatcher.sendChatMessage( new ChatMessage( { diff --git a/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts b/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts index 2d9e01db9a0..ac914e77b6b 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts @@ -2,7 +2,7 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ -import * as path from 'path' + import { UserIntent } from '@amzn/codewhisperer-streaming' import { AmazonqAddMessage, @@ -28,7 +28,6 @@ import { ResponseBodyLinkClickMessage, SourceLinkClickMessage, TriggerPayload, - AdditionalContextLengths, AdditionalContextInfo, } from './model' import { TriggerEvent, TriggerEventsStorage } from '../../storages/triggerEvents' @@ -43,9 +42,6 @@ import { supportedLanguagesList } from '../chat/chatRequest/converter' import { AuthUtil } from '../../../codewhisperer/util/authUtil' import { getSelectedCustomization } from '../../../codewhisperer/util/customizationUtil' import { undefinedIfEmpty } from '../../../shared/utilities/textUtilities' -import { AdditionalContextPrompt } from '../../../amazonq/lsp/types' -import { getUserPromptsDirectory, promptFileExtension } from '../../constants' -import { isInDirectory } from '../../../shared/filesystemUtilities' import { sleep } from '../../../shared/utilities/timeoutUtils' import { FileDiagnostic, @@ -164,40 +160,6 @@ export class CWCTelemetryHelper { telemetry.amazonq_exitFocusChat.emit({ result: 'Succeeded', passive: true }) } - public getContextType(prompt: AdditionalContextPrompt): string { - if (prompt.filePath.endsWith(promptFileExtension)) { - if (isInDirectory(path.join('.amazonq', 'rules'), prompt.relativePath)) { - return 'rule' - } else if (isInDirectory(getUserPromptsDirectory(), prompt.filePath)) { - return 'prompt' - } - } - return 'file' - } - - public getContextLengths(prompts: AdditionalContextPrompt[]): AdditionalContextLengths { - let fileContextLength = 0 - let promptContextLength = 0 - let ruleContextLength = 0 - - for (const prompt of prompts) { - const type = this.getContextType(prompt) - switch (type) { - case 'rule': - ruleContextLength += prompt.content.length - break - case 'file': - fileContextLength += prompt.content.length - break - case 'prompt': - promptContextLength += prompt.content.length - break - } - } - - return { fileContextLength, promptContextLength, ruleContextLength } - } - public async recordFeedback(message: ChatItemFeedbackMessage) { const logger = getLogger() try { 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/commands.ts b/packages/core/src/commands.ts index db038a72bbd..f69a8dd173c 100644 --- a/packages/core/src/commands.ts +++ b/packages/core/src/commands.ts @@ -46,7 +46,7 @@ import { Commands, VsCodeCommandArg, placeholder, vscodeComponent } from './shar import { isValidResponse } from './shared/wizards/wizard' import { CancellationError } from './shared/utilities/timeoutUtils' import { ToolkitError } from './shared/errors' -import { setContext } from './shared/vscode/setContext' +import { getContext, setContext } from './shared/vscode/setContext' function switchConnections(auth: Auth | TreeNode | unknown) { if (!(auth instanceof Auth)) { @@ -103,7 +103,7 @@ export function registerCommands(context: vscode.ExtensionContext) { const manageConnections = Commands.register( { id: 'aws.toolkit.auth.manageConnections', compositeKey: { 1: 'source' } }, - async (_: VsCodeCommandArg, source: AuthSource, serviceToShow?: ServiceItemId) => { + async (_: VsCodeCommandArg, source: AuthSource, serviceToShow?: ServiceItemId, blocking?: boolean) => { if (_ !== placeholder) { source = AuthSources.vscodeComponent } @@ -124,7 +124,23 @@ export function registerCommands(context: vscode.ExtensionContext) { CommonAuthWebview.authSource = source await vscode.commands.executeCommand('aws.explorer.setLoginService', serviceToShow) await setContext('aws.explorer.showAuthView', true) + + // While the auth view is open, we want to be blocking (if the command has been specified to be blocking) + const authWindowPromise = new Promise((resolve) => { + if (!blocking) { + resolve() + } + + const check = globals.clock.setInterval(() => { + if (getContext('aws.explorer.showAuthView') === false) { + clearInterval(check) + resolve() + } + }, 500) + }) + await vscode.commands.executeCommand('aws.toolkit.AmazonCommonAuth.focus') + await authWindowPromise } ) diff --git a/packages/core/src/dev/activation.ts b/packages/core/src/dev/activation.ts index 8ce0f6aab11..16b5d7e53ad 100644 --- a/packages/core/src/dev/activation.ts +++ b/packages/core/src/dev/activation.ts @@ -25,7 +25,6 @@ import { NotificationsController } from '../notifications/controller' import { DevNotificationsState } from '../notifications/types' import { QuickPickItem } from 'vscode' import { ChildProcess } from '../shared/utilities/processUtils' -import { WorkspaceLspInstaller } from '../amazonq/lsp/workspaceInstaller' interface MenuOption { readonly label: string @@ -451,12 +450,6 @@ const resettableFeatures: readonly ResettableFeature[] = [ detail: 'Resets memory/global state for the notifications panel (includes dismissed, onReceive).', executor: resetNotificationsState, }, - { - name: 'workspace lsp', - label: 'Download Lsp ', - detail: 'Resets workspace LSP', - executor: resetWorkspaceLspDownload, - }, ] as const // TODO this is *somewhat* similar to `openStorageFromInput`. If we need another @@ -545,10 +538,6 @@ async function resetNotificationsState() { await targetNotificationsController.reset() } -async function resetWorkspaceLspDownload() { - await new WorkspaceLspInstaller().resolve() -} - async function editNotifications() { const storageKey = 'aws.notifications.dev' const current = globalState.get(storageKey) ?? {} diff --git a/packages/core/src/dev/config.ts b/packages/core/src/dev/config.ts index b4df78f64b0..d5fa49b2426 100644 --- a/packages/core/src/dev/config.ts +++ b/packages/core/src/dev/config.ts @@ -10,6 +10,3 @@ export const betaUrl = { amazonq: '', toolkit: '', } - -// TO-DO: remove when releasing CSB -export const isClientSideBuildEnabled = false 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..fe83459251a 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' 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 8e759263623..a8a7855913e 100644 --- a/packages/core/src/extensionNode.ts +++ b/packages/core/src/extensionNode.ts @@ -41,6 +41,8 @@ import { activate as activateRedshift } from './awsService/redshift/activation' 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' @@ -185,6 +187,8 @@ export async function activate(context: vscode.ExtensionContext) { await activateSchemas(extContext) + await activateSagemaker(extContext) + if (!isSageMaker()) { // Amazon Q Tree setup. learnMoreAmazonQCommand.register() @@ -194,6 +198,9 @@ export async function activate(context: vscode.ExtensionContext) { await handleAmazonQInstall() } + + await activateSageMakerUnifiedStudio(context) + await activateApplicationComposer(context) await activateThreatComposerEditor(context) diff --git a/packages/core/src/feedback/vue/submitFeedback.ts b/packages/core/src/feedback/vue/submitFeedback.ts index a19f81c4079..928c0eb3249 100644 --- a/packages/core/src/feedback/vue/submitFeedback.ts +++ b/packages/core/src/feedback/vue/submitFeedback.ts @@ -41,6 +41,10 @@ export class FeedbackWebview extends VueWebview { return 'Choose a reaction (smile/frown)' } + if (message.comment.length < 188) { + return 'Please add atleast 100 characters in the template describing your issue.' + } + if (this.commentData) { message.comment = `${message.comment}\n\n${this.commentData}` } diff --git a/packages/core/src/feedback/vue/submitFeedback.vue b/packages/core/src/feedback/vue/submitFeedback.vue index 814223dc3cc..b97233ba494 100644 --- a/packages/core/src/feedback/vue/submitFeedback.vue +++ b/packages/core/src/feedback/vue/submitFeedback.vue @@ -34,6 +34,26 @@ >.
+
+ For helpful feedback, please include: +
    +
  • + Issue: A brief summary of the issue or suggestion +
  • +
  • + Reproduction Steps: Clear steps to reproduce the issue (if + applicable) +
  • +
  • + Expected vs. Actual: What you expected and what actually + happened +
  • +
+

@@ -66,7 +86,16 @@ const client = WebviewClientFactory.create() export default defineComponent({ data() { return { - comment: '', + comment: `Issue: + +Reproduction Steps: +1. +2. +3. + +Expected Behavior: + +Actual Behavior: `, sentiment: '', isSubmitting: false, error: '', diff --git a/packages/core/src/lambda/activation.ts b/packages/core/src/lambda/activation.ts index d799173697e..9b010eceff8 100644 --- a/packages/core/src/lambda/activation.ts +++ b/packages/core/src/lambda/activation.ts @@ -3,20 +3,22 @@ * SPDX-License-Identifier: Apache-2.0 */ +import * as path from 'path' import * as vscode from 'vscode' -import { Lambda } from 'aws-sdk' +import * as nls from 'vscode-nls' + +import { FunctionConfiguration } from '@aws-sdk/client-lambda' import { deleteLambda } from './commands/deleteLambda' import { uploadLambdaCommand } from './commands/uploadLambda' import { LambdaFunctionNode } from './explorer/lambdaFunctionNode' -import { downloadLambdaCommand } from './commands/downloadLambda' +import { downloadLambdaCommand, openLambdaFile } from './commands/downloadLambda' import { tryRemoveFolder } from '../shared/filesystemUtilities' -import { ExtContext } from '../shared/extensions' import { invokeRemoteLambda } from './vue/remoteInvoke/invokeLambda' import { registerSamDebugInvokeVueCommand, registerSamInvokeVueCommand } from './vue/configEditor/samInvokeBackend' 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' @@ -24,11 +26,130 @@ import { liveTailRegistry, liveTailCodeLensProvider } from '../awsService/cloudW import { getFunctionLogGroupName } from '../awsService/cloudWatchLogs/activation' import { ToolkitError, isError } from '../shared/errors' import { LogStreamFilterResponse } from '../awsService/cloudWatchLogs/wizard/liveTailLogStreamSubmenu' +import { tempDirPath } from '../shared/filesystemUtilities' +import fs from '../shared/fs/fs' +import { + confirmOutdatedChanges, + deleteFilesInFolder, + deployFromTemp, + getReadme, + openLambdaFolderForEdit, + watchForUpdates, +} from './commands/editLambda' +import { compareCodeSha, getFunctionInfo, getTempLocation, setFunctionInfo } from './utils' +import { registerLambdaUriHandler } from './uriHandlers' +import globals from '../shared/extensionGlobals' + +const localize = nls.loadMessageBundle() +import { activateRemoteDebugging } from './remoteDebugging/ldkController' +import { ExtContext } from '../shared/extensions' + +async function openReadme() { + const readmeUri = vscode.Uri.file(await getReadme()) + // We only want to do it if there's not a readme already + const isPreviewOpen = vscode.window.tabGroups.all.some((group) => + group.tabs.some((tab) => tab.label.includes('README')) + ) + if (!isPreviewOpen) { + await vscode.commands.executeCommand('markdown.showPreviewToSide', readmeUri) + } +} + +async function quickEditActivation() { + if (vscode.workspace.workspaceFolders) { + for (const workspaceFolder of vscode.workspace.workspaceFolders) { + // Making the comparison case insensitive because Windows can have `C\` or `c\` + const workspacePath = workspaceFolder.uri.fsPath.toLowerCase() + const tempPath = path.join(tempDirPath, 'lambda').toLowerCase() + if (workspacePath.includes(tempPath)) { + const name = path.basename(workspaceFolder.uri.fsPath) + const region = path.basename(path.dirname(workspaceFolder.uri.fsPath)) + + const lambda = { name, region, configuration: undefined } + + watchForUpdates(lambda, vscode.Uri.file(workspacePath)) + + await openReadme() + + // Open handler function + try { + const handler = await getFunctionInfo(lambda, 'handlerFile') + const lambdaLocation = path.join(workspacePath, handler) + await openLambdaFile(lambdaLocation, vscode.ViewColumn.One) + } catch (e) { + void vscode.window.showWarningMessage( + localize('AWS.lambda.openFile.failure', `Failed to determine handler location: ${e}`) + ) + } + + // Check if there are changes that need overwritten + try { + // Checking if there are changes that need to be overwritten + const prompt = localize( + 'AWS.lambda.download.confirmOutdatedSync', + 'There are changes to your function in the cloud since you last edited locally, do you want to overwrite your local changes?' + ) + + // Adding delay to give the authentication time to catch up + await new Promise((resolve) => globals.clock.setTimeout(resolve, 1000)) + + const overwriteChanges = !(await compareCodeSha(lambda)) + ? await confirmOutdatedChanges(prompt) + : false + if (overwriteChanges) { + // Close all open tabs from this workspace + const workspaceUri = vscode.Uri.file(workspacePath) + for (const tabGroup of vscode.window.tabGroups.all) { + const tabsToClose = tabGroup.tabs.filter( + (tab) => + tab.input instanceof vscode.TabInputText && + tab.input.uri.fsPath.startsWith(workspaceUri.fsPath) + ) + if (tabsToClose.length > 0) { + await vscode.window.tabGroups.close(tabsToClose) + } + } + + // Delete all files in the directory + await deleteFilesInFolder(workspacePath) + + // Show message to user about next steps + void vscode.window.showInformationMessage( + localize( + 'AWS.lambda.refresh.complete', + 'Local workspace cleared. Navigate to the Toolkit explorer to get fresh code from the cloud.' + ) + ) + + await setFunctionInfo(lambda, { undeployed: false }) + + // Remove workspace folder + const workspaceIndex = vscode.workspace.workspaceFolders?.findIndex( + (folder) => folder.uri.fsPath.toLowerCase() === workspacePath + ) + if (workspaceIndex !== undefined && workspaceIndex >= 0) { + vscode.workspace.updateWorkspaceFolders(workspaceIndex, 1) + } + } + } catch (e) { + void vscode.window.showWarningMessage( + localize( + 'AWS.lambda.pull.failure', + `Failed to pull latest changes from the cloud, you can still edit locally: ${e}` + ) + ) + } + } + } + } +} /** * Activates Lambda components. */ export async function activate(context: ExtContext): Promise { + void quickEditActivation() + context.extensionContext.subscriptions.push( Commands.register('aws.deleteLambda', async (node: LambdaFunctionNode | TreeNode) => { const sourceNode = getSourceNode(node) @@ -38,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, { @@ -47,6 +174,7 @@ export async function activate(context: ExtContext): Promise { source: source, }) }), + // Capture debug finished events, and delete the temporary directory if it exists vscode.debug.onDidTerminateDebugSession(async (session) => { if ( @@ -56,10 +184,12 @@ export async function activate(context: ExtContext): Promise { await tryRemoveFolder(session.configuration.baseBuildDir) } }), + Commands.register('aws.downloadLambda', async (node: LambdaFunctionNode | TreeNode) => { const sourceNode = getSourceNode(node) await downloadLambdaCommand(sourceNode) }), + Commands.register({ id: 'aws.uploadLambda', autoconnect: true }, async (arg?: unknown) => { if (arg instanceof LambdaFunctionNode) { await uploadLambdaCommand({ @@ -73,6 +203,26 @@ export async function activate(context: ExtContext): Promise { await uploadLambdaCommand() } }), + + Commands.register({ id: 'aws.quickDeployLambda' }, async (node: LambdaFunctionNode) => { + const functionName = node.configuration.FunctionName! + const region = node.regionCode + const lambda = { name: functionName, region, configuration: node.configuration } + const tempLocation = getTempLocation(functionName, region) + + if (await fs.existsDir(tempLocation)) { + await deployFromTemp(lambda, vscode.Uri.file(tempLocation)) + } + }), + + Commands.register('aws.openLambdaFile', async (path: string) => { + await openLambdaFile(path) + }), + + Commands.register('aws.lambda.openWorkspace', async (node: LambdaFunctionNode) => { + await openLambdaFolderForEdit(node.functionName, node.regionCode) + }), + Commands.register('aws.copyLambdaUrl', async (node: LambdaFunctionNode | TreeNode) => { const sourceNode = getSourceNode(node) await copyLambdaUrl(sourceNode, new DefaultLambdaClient(sourceNode.regionCode)) @@ -85,12 +235,16 @@ export async function activate(context: ExtContext): Promise { ), 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), } @@ -116,6 +270,10 @@ export async function activate(context: ExtContext): Promise { throw err } } - }) + }), + + registerLambdaUriHandler() ) + + void activateRemoteDebugging() } 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/downloadLambda.ts b/packages/core/src/lambda/commands/downloadLambda.ts index e2e1dc2be91..baf82b5b30c 100644 --- a/packages/core/src/lambda/commands/downloadLambda.ts +++ b/packages/core/src/lambda/commands/downloadLambda.ts @@ -11,7 +11,7 @@ import { LambdaFunctionNode } from '../explorer/lambdaFunctionNode' import { showConfirmationMessage } from '../../shared/utilities/messages' import { LaunchConfiguration, getReferencedHandlerPaths } from '../../shared/debug/launchConfiguration' -import { makeTemporaryToolkitFolder, fileExists, tryRemoveFolder } from '../../shared/filesystemUtilities' +import { makeTemporaryToolkitFolder, tryRemoveFolder } from '../../shared/filesystemUtilities' import * as localizedText from '../../shared/localizedText' import { getLogger } from '../../shared/logger/logger' import { HttpResourceFetcher } from '../../shared/resourcefetcher/node/httpResourceFetcher' @@ -26,9 +26,34 @@ import { DefaultLambdaClient } from '../../shared/clients/lambdaClient' import { telemetry } from '../../shared/telemetry/telemetry' import { Result, Runtime } from '../../shared/telemetry/telemetry' import { fs } from '../../shared/fs/fs' +import { LambdaFunction } from './uploadLambda' +import globals from '../../shared/extensionGlobals' + +// Workspace state key for Lambda function ARN to local path cache +const LAMBDA_ARN_CACHE_KEY = 'aws.lambda.functionArnToLocalPathCache' // eslint-disable-line @typescript-eslint/naming-convention + +async function setLambdaArnCache(functionArn: string, localPath: string): Promise { + try { + const cache: Record = globals.context.workspaceState.get(LAMBDA_ARN_CACHE_KEY, {}) + cache[functionArn] = localPath + await globals.context.workspaceState.update(LAMBDA_ARN_CACHE_KEY, cache) + getLogger().debug(`lambda: cached local path for function ARN: ${functionArn} -> ${localPath}`) + } catch (error) { + getLogger().error(`lambda: failed to cache local path for function ARN: ${functionArn}`, error) + } +} + +export function getCachedLocalPath(functionArn: string): string | undefined { + const cache: Record = globals.context.workspaceState.get(LAMBDA_ARN_CACHE_KEY, {}) + return cache[functionArn] +} export async function downloadLambdaCommand(functionNode: LambdaFunctionNode) { const result = await runDownloadLambda(functionNode) + // check if result is Result + if (result instanceof vscode.Uri) { + return + } telemetry.lambda_import.emit({ result, @@ -36,7 +61,10 @@ export async function downloadLambdaCommand(functionNode: LambdaFunctionNode) { }) } -async function runDownloadLambda(functionNode: LambdaFunctionNode): Promise { +export async function runDownloadLambda( + functionNode: LambdaFunctionNode, + returnDir: boolean = false +): Promise { const workspaceFolders = vscode.workspace.workspaceFolders || [] const functionName = functionNode.configuration.FunctionName! @@ -73,16 +101,37 @@ async function runDownloadLambda(functionNode: LambdaFunctionNode): Promise( + return await downloadLambdaInLocation( + { name: functionName, region: functionNode.regionCode, configuration: functionNode.configuration }, + downloadLocationName, + downloadLocation, + workspaceFolders, + selectedUri, + returnDir + ) +} + +export async function downloadLambdaInLocation( + lambda: LambdaFunction, + downloadLocationName: string, + downloadLocation: string, + workspaceFolders?: readonly vscode.WorkspaceFolder[], + selectedUri?: vscode.Uri, + returnDir: boolean = false +): Promise { + const result = await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, cancellable: false, title: localize( 'AWS.lambda.download.status', 'Downloading Lambda function {0} into {1}...', - functionName, + lambda.name, downloadLocationName ), }, @@ -90,8 +139,22 @@ async function runDownloadLambda(functionNode: LambdaFunctionNode): Promise { - return selectedUri === val.uri - }).length === 0 - ) { - await addFolderToWorkspace({ uri: selectedUri! }, true) - } - const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(downloadLocation))! - - await addLaunchConfigEntry(lambdaLocation, functionNode, workspaceFolder) + if (workspaceFolders) { + if ( + workspaceFolders.filter((val) => { + return selectedUri === val.uri + }).length === 0 + ) { + await addFolderToWorkspace({ uri: selectedUri! }, true) + } + const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(downloadLocation))! + await addLaunchConfigEntry(lambdaLocation, lambda, workspaceFolder) + } return 'Succeeded' } catch (e) { // failed to open handler file or add launch config. @@ -130,6 +195,12 @@ async function runDownloadLambda(functionNode: LambdaFunctionNode): Promise, - functionNode: LambdaFunctionNode, + lambda: LambdaFunction, extractLocation: string, - lambda = new DefaultLambdaClient(functionNode.regionCode) + lambdaClient = new DefaultLambdaClient(lambda.region) ): Promise { - const functionArn = functionNode.configuration.FunctionArn! + const functionArn = lambda.configuration!.FunctionArn! let tempDir: string | undefined try { tempDir = await makeTemporaryToolkitFolder() const downloadLocation = path.join(tempDir, 'function.zip') - const response = await lambda.getFunction(functionArn) + const response = await lambdaClient.getFunction(functionArn) const codeLocation = response.Code!.Location! // arbitrary increments since there's no "busy" state for progress bars @@ -176,8 +247,8 @@ async function downloadAndUnzipLambda( } } -export async function openLambdaFile(lambdaLocation: string): Promise { - if (!(await fileExists(lambdaLocation))) { +export async function openLambdaFile(lambdaLocation: string, viewColumn?: vscode.ViewColumn): Promise { + if (!(await fs.exists(lambdaLocation))) { const warning = localize( 'AWS.lambda.download.fileNotFound', 'Handler file {0} not found in downloaded function.', @@ -187,22 +258,23 @@ export async function openLambdaFile(lambdaLocation: string): Promise { void vscode.window.showWarningMessage(warning) throw new Error() } + await vscode.commands.executeCommand('workbench.action.focusFirstEditorGroup') const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(lambdaLocation)) - await vscode.window.showTextDocument(doc) + await vscode.window.showTextDocument(doc, viewColumn) } async function addLaunchConfigEntry( lambdaLocation: string, - functionNode: LambdaFunctionNode, + lambda: LambdaFunction, workspaceFolder: vscode.WorkspaceFolder ): Promise { - const handler = functionNode.configuration.Handler! + const handler = lambda.configuration!.Handler! const samDebugConfig = createCodeAwsSamDebugConfig( workspaceFolder, handler, - computeLambdaRoot(lambdaLocation, functionNode), - functionNode.configuration.Runtime! + computeLambdaRoot(lambdaLocation, lambda), + lambda.configuration!.Runtime! ) const launchConfig = new LaunchConfiguration(vscode.Uri.file(lambdaLocation)) @@ -218,8 +290,8 @@ async function addLaunchConfigEntry( * @param lambdaLocation Lambda handler file location * @param functionNode Function node */ -function computeLambdaRoot(lambdaLocation: string, functionNode: LambdaFunctionNode): string { - const lambdaDetails = getLambdaDetails(functionNode.configuration) +function computeLambdaRoot(lambdaLocation: string, lambda: LambdaFunction): string { + const lambdaDetails = getLambdaDetails(lambda.configuration!) const normalizedLocation = pathutils.normalize(lambdaLocation) const lambdaIndex = normalizedLocation.indexOf(`/${lambdaDetails.fileName}`) diff --git a/packages/core/src/lambda/commands/editLambda.ts b/packages/core/src/lambda/commands/editLambda.ts new file mode 100644 index 00000000000..5ea8169d280 --- /dev/null +++ b/packages/core/src/lambda/commands/editLambda.ts @@ -0,0 +1,285 @@ +/*! + * 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 { LambdaFunctionNode } from '../explorer/lambdaFunctionNode' +import { downloadLambdaInLocation, openLambdaFile } from './downloadLambda' +import { LambdaFunction, runUploadDirectory } from './uploadLambda' +import { + compareCodeSha, + getFunctionInfo, + getLambdaDetails, + getTempLocation, + lambdaTempPath, + setFunctionInfo, +} from '../utils' +import { showConfirmationMessage } from '../../shared/utilities/messages' +import fs from '../../shared/fs/fs' +import globals from '../../shared/extensionGlobals' +import { LambdaFunctionNodeDecorationProvider } from '../explorer/lambdaFunctionNodeDecorationProvider' +import path from 'path' +import { telemetry } from '../../shared/telemetry/telemetry' +import { ToolkitError } from '../../shared/errors' +import { getFunctionWithCredentials } from '../../shared/clients/lambdaClient' +import { getLogger } from '../../shared/logger/logger' + +const localize = nls.loadMessageBundle() + +let lastPromptTime = Date.now() - 5000 + +export function watchForUpdates(lambda: LambdaFunction, projectUri: vscode.Uri): void { + const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(projectUri, '*')) + const startTime = globals.clock.Date.now() + + watcher.onDidChange(async (fileUri) => { + await promptForSync(lambda, projectUri, fileUri) + }) + + watcher.onDidCreate(async (fileUri) => { + // When the code is downloaded and the watcher is set, this will immediately trigger the onDidCreate + // To avoid this, we must check that the file was actually created AFTER the watcher was created + if ((await fs.stat(fileUri.fsPath)).ctime < startTime) { + return + } + await promptForSync(lambda, projectUri, fileUri) + }) + + watcher.onDidDelete(async (fileUri) => { + // We don't want to sync if the whole directory has been deleted or emptied + if (fileUri.fsPath !== projectUri.fsPath) { + // Check if directory is empty before prompting for sync + try { + const entries = await fs.readdir(projectUri.fsPath) + if (entries.length > 0) { + await promptForSync(lambda, projectUri, fileUri) + } + } catch (err) { + getLogger().debug(`Failed to check Lambda directory contents: ${err}`) + } + } + }) +} + +// Creating this function for testing, can't mock the vscode.window in the tests +export async function promptDeploy() { + const confirmItem = localize('AWS.lambda.upload.sync', 'Deploy') + const cancelItem = localize('AWS.lambda.upload.noSync', 'No, thanks') + const response = await vscode.window.showInformationMessage( + localize('AWS.lambda.upload.confirmSync', 'Would you like to deploy these changes to the cloud?'), + confirmItem, + cancelItem + ) + return response === confirmItem +} + +export async function promptForSync(lambda: LambdaFunction, projectUri: vscode.Uri, fileUri: vscode.Uri) { + if (!(await fs.existsDir(projectUri.fsPath)) || globals.clock.Date.now() - lastPromptTime < 5000) { + return + } + + await setFunctionInfo(lambda, { + undeployed: true, + }) + + await LambdaFunctionNodeDecorationProvider.getInstance().addBadge( + fileUri, + vscode.Uri.from({ scheme: 'lambda', path: `${lambda.region}/${lambda.name}` }) + ) + + lastPromptTime = globals.clock.Date.now() + if (await promptDeploy()) { + await deployFromTemp(lambda, projectUri) + } +} + +export async function confirmOutdatedChanges(prompt: string): Promise { + return await showConfirmationMessage({ + prompt, + confirm: localize('AWS.lambda.upload.overwrite', 'Overwrite'), + cancel: localize('AWS.lambda.upload.noOverwrite', 'Cancel'), + }) +} + +export async function deployFromTemp(lambda: LambdaFunction, projectUri: vscode.Uri) { + return telemetry.lambda_quickDeploy.run(async () => { + const prompt = localize( + 'AWS.lambda.upload.confirmOutdatedSync', + 'There are changes to your Function in the cloud after you created this local copy, overwrite anyway?' + ) + + const isShaDifferent = !(await compareCodeSha(lambda)) + const overwriteChanges = isShaDifferent ? await confirmOutdatedChanges(prompt) : true + + if (overwriteChanges) { + // Reset the lastPrompt time because we don't want to retrigger the watcher flow + lastPromptTime = globals.clock.Date.now() + await vscode.workspace.saveAll() + try { + await runUploadDirectory(lambda, 'zip', projectUri) + } catch { + throw new ToolkitError('Failed to deploy Lambda function', { code: 'deployFailure' }) + } + await setFunctionInfo(lambda, { + lastDeployed: globals.clock.Date.now(), + undeployed: false, + }) + await LambdaFunctionNodeDecorationProvider.getInstance().removeBadge( + projectUri, + vscode.Uri.from({ scheme: 'lambda', path: `${lambda.region}/${lambda.name}` }) + ) + if (isShaDifferent) { + telemetry.record({ action: 'overwriteChanges' }) + } + } else { + telemetry.record({ action: 'cancelOverwrite' }) + } + }) +} + +export async function deleteFilesInFolder(location: string) { + const entries = await fs.readdir(location) + await Promise.all( + entries.map((entry) => fs.delete(path.join(location, entry[0]), { recursive: true, force: true })) + ) +} + +export async function editLambdaCommand(functionNode: LambdaFunctionNode) { + const region = functionNode.regionCode + const functionName = functionNode.configuration.FunctionName! + return await editLambda({ name: functionName, region, configuration: functionNode.configuration }, 'explorer') +} + +export async function overwriteChangesForEdit(lambda: LambdaFunction, downloadLocation: string) { + try { + // Clear directory contents instead of deleting to avoid Windows EBUSY errors + if (await fs.existsDir(downloadLocation)) { + await deleteFilesInFolder(downloadLocation) + } else { + await fs.mkdir(downloadLocation) + } + + await downloadLambdaInLocation(lambda, 'local', downloadLocation) + + // Watching for updates, then setting info, then removing the badges must be done in this order + // This is because the files creating can throw the watcher, which sometimes leads to changes being marked as undeployed + watchForUpdates(lambda, vscode.Uri.file(downloadLocation)) + + await setFunctionInfo(lambda, { + lastDeployed: globals.clock.Date.now(), + undeployed: false, + sha: lambda.configuration!.CodeSha256, + handlerFile: getLambdaDetails(lambda.configuration!).fileName, + }) + await LambdaFunctionNodeDecorationProvider.getInstance().removeBadge( + vscode.Uri.file(downloadLocation), + vscode.Uri.from({ scheme: 'lambda', path: `${lambda.region}/${lambda.name}` }) + ) + } catch { + throw new ToolkitError('Failed to download Lambda function', { code: 'failedDownload' }) + } +} + +export async function editLambda(lambda: LambdaFunction, source?: 'workspace' | 'explorer') { + return await telemetry.lambda_quickEditFunction.run(async () => { + telemetry.record({ source }) + const downloadLocation = getTempLocation(lambda.name, lambda.region) + + // We don't want to do anything if the folder already exists as a workspace folder, it means it's already being edited + if (vscode.workspace.workspaceFolders?.some((folder) => folder.uri.fsPath === downloadLocation)) { + return downloadLocation + } + + const prompt = localize( + 'AWS.lambda.download.confirmOutdatedSync', + 'There are changes to your function in the cloud since you last edited locally, do you want to overwrite your local changes?' + ) + + // We want to overwrite changes in the following cases: + // 1. There is no code sha locally (getCodeShaLocal returns falsy) + // 2. There is a code sha locally, it does not match the one remotely, and the user confirms they want to overwrite it + const localExists = !!(await getFunctionInfo(lambda, 'sha')) + // This record tells us if they're attempting to edit a function they've edited before + telemetry.record({ action: localExists ? 'existingEdit' : 'newEdit' }) + + const isDirectoryEmpty = (await fs.existsDir(downloadLocation)) + ? (await fs.readdir(downloadLocation)).length === 0 + : true + + const overwriteChanges = + !localExists || + isDirectoryEmpty || + (!(await compareCodeSha(lambda)) ? await confirmOutdatedChanges(prompt) : false) + + if (overwriteChanges) { + await overwriteChangesForEdit(lambda, downloadLocation) + } else if (source === 'explorer') { + // If the source is the explorer, we want to open, otherwise we just wait to open in the workspace + const lambdaLocation = path.join(downloadLocation, getLambdaDetails(lambda.configuration!).fileName) + await openLambdaFile(lambdaLocation) + watchForUpdates(lambda, vscode.Uri.file(downloadLocation)) + } + + return downloadLocation + }) +} + +export async function openLambdaFolderForEdit(name: string, region: string) { + const downloadLocation = getTempLocation(name, region) + + // Do all authentication work before opening workspace to avoid race condition + const getFunctionOutput = await getFunctionWithCredentials(region, name) + const configuration = getFunctionOutput.Configuration + + // Download and set up Lambda code before opening workspace + await editLambda( + { + name, + region, + configuration: configuration as any, + }, + 'workspace' + ) + + try { + await vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.file(downloadLocation), { + newWindow: true, + noRecentEntry: true, + }) + } catch (e) { + throw new ToolkitError(`Failed to open your function as a workspace: ${e}`, { code: 'folderOpenFailure' }) + } +} + +export async function getReadme(): Promise { + const readmeSource = path.join('resources', 'markdown', 'lambdaEdit.md') + const readmeDestination = path.join(lambdaTempPath, 'README.md') + try { + const readmeContent = await fs.readFileText(globals.context.asAbsolutePath(readmeSource)) + await fs.writeFile(readmeDestination, readmeContent) + } catch (e) { + getLogger().info(`Failed to copy content for Lambda README: ${e}`) + } + + try { + const createStackIconSource = path.join('resources', 'icons', 'aws', 'lambda', 'create-stack-light.svg') + const createStackIconDestination = path.join(lambdaTempPath, 'create-stack.svg') + await fs.copy(globals.context.asAbsolutePath(createStackIconSource), createStackIconDestination) + + // Copy VS Code built-in icons + const vscodeIconPath = path.join('resources', 'icons', 'vscode', 'light') + + const invokeIconSource = path.join(vscodeIconPath, 'run.svg') + const invokeIconDestination = path.join(lambdaTempPath, 'invoke.svg') + await fs.copy(globals.context.asAbsolutePath(invokeIconSource), invokeIconDestination) + + const deployIconSource = path.join(vscodeIconPath, 'cloud-upload.svg') + const deployIconDestination = path.join(lambdaTempPath, 'deploy.svg') + await fs.copy(globals.context.asAbsolutePath(deployIconSource), deployIconDestination) + } catch (e) { + getLogger().info(`Failed to copy content for Lambda README: ${e}`) + } + + return readmeDestination +} diff --git a/packages/core/src/lambda/commands/uploadLambda.ts b/packages/core/src/lambda/commands/uploadLambda.ts index 692d07409a2..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' @@ -277,7 +277,7 @@ export class UploadLambdaWizard extends Wizard { * @param type Whether to zip or sam build the directory * @param window Wrapper around vscode.window functionality for testing */ -async function runUploadDirectory(lambda: LambdaFunction, type: 'zip' | 'sam', parentDir: vscode.Uri) { +export async function runUploadDirectory(lambda: LambdaFunction, type: 'zip' | 'sam', parentDir: vscode.Uri) { if (type === 'sam' && lambda.configuration) { return await runUploadLambdaWithSamBuild({ ...lambda, configuration: lambda.configuration }, parentDir) } else { diff --git a/packages/core/src/lambda/explorer/cloudFormationNodes.ts b/packages/core/src/lambda/explorer/cloudFormationNodes.ts index 6fd8c4d0e96..4ef298a391e 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' @@ -78,7 +78,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 +94,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 +122,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,10 +151,9 @@ 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) - node.contextValue = contextValueCloudformationLambdaFunction + const node = new LambdaFunctionNode(parent, regionCode, configuration, contextValueCloudformationLambdaFunction) return node } diff --git a/packages/core/src/lambda/explorer/lambdaFunctionFileNode.ts b/packages/core/src/lambda/explorer/lambdaFunctionFileNode.ts new file mode 100644 index 00000000000..422de99b31f --- /dev/null +++ b/packages/core/src/lambda/explorer/lambdaFunctionFileNode.ts @@ -0,0 +1,38 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { AWSResourceNode } from '../../shared/treeview/nodes/awsResourceNode' +import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase' +import { LambdaFunctionNode } from './lambdaFunctionNode' +import { getIcon } from '../../shared/icons' +import { isCloud9 } from '../../shared/extensionUtilities' +import { LambdaFunctionFolderNode } from './lambdaFunctionFolderNode' + +export class LambdaFunctionFileNode extends AWSTreeNodeBase implements AWSResourceNode { + public constructor( + public readonly parent: LambdaFunctionNode | LambdaFunctionFolderNode, + public readonly filename: string, + public readonly path: string + ) { + super(filename) + this.iconPath = getIcon('vscode-file') + this.contextValue = 'lambdaFunctionFileNode' + this.command = !isCloud9() + ? { + command: 'aws.openLambdaFile', + title: 'Open file', + arguments: [path], + } + : undefined + } + + public get arn(): string { + return '' + } + + public get name(): string { + return '' + } +} diff --git a/packages/core/src/lambda/explorer/lambdaFunctionFolderNode.ts b/packages/core/src/lambda/explorer/lambdaFunctionFolderNode.ts new file mode 100644 index 00000000000..7c67d482666 --- /dev/null +++ b/packages/core/src/lambda/explorer/lambdaFunctionFolderNode.ts @@ -0,0 +1,60 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { AWSResourceNode } from '../../shared/treeview/nodes/awsResourceNode' +import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase' +import { LambdaFunctionNode } from './lambdaFunctionNode' +import { fs } from '../../shared/fs/fs' +import { getIcon } from '../../shared/icons' +import { makeChildrenNodes } from '../../shared/treeview/utils' +import { PlaceholderNode } from '../../shared/treeview/nodes/placeholderNode' +import { localize } from 'vscode-nls' +import path from 'path' +import { LambdaFunctionFileNode } from './lambdaFunctionFileNode' + +export class LambdaFunctionFolderNode extends AWSTreeNodeBase implements AWSResourceNode { + public constructor( + public readonly parent: LambdaFunctionNode | LambdaFunctionFolderNode, + public readonly filename: string, + public readonly path: string + ) { + super(filename, vscode.TreeItemCollapsibleState.Collapsed) + this.iconPath = getIcon('vscode-folder') + this.contextValue = 'lambdaFunctionFolderNode' + } + + public get arn(): string { + return '' + } + + public get name(): string { + return '' + } + + public override async getChildren(): Promise { + return await makeChildrenNodes({ + getChildNodes: async () => this.loadFunctionFiles(), + getNoChildrenPlaceholderNode: async () => + new PlaceholderNode(this, localize('AWS.explorerNode.s3.noObjects', '[No Objects found]')), + }) + } + + public async loadFunctionFiles(): Promise { + const nodes: AWSTreeNodeBase[] = [] + const files = await fs.readdir(this.path) + for (const file of files) { + const [fileName, type] = file + const filePath = path.join(this.path, fileName) + if (type === vscode.FileType.Directory) { + nodes.push(new LambdaFunctionFolderNode(this, fileName, filePath)) + } else { + nodes.push(new LambdaFunctionFileNode(this, fileName, filePath)) + } + } + + return nodes + } +} diff --git a/packages/core/src/lambda/explorer/lambdaFunctionNode.ts b/packages/core/src/lambda/explorer/lambdaFunctionNode.ts index 02fa8439d5c..3d2f05a99fa 100644 --- a/packages/core/src/lambda/explorer/lambdaFunctionNode.ts +++ b/packages/core/src/lambda/explorer/lambdaFunctionNode.ts @@ -3,28 +3,62 @@ * 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' import { AWSResourceNode } from '../../shared/treeview/nodes/awsResourceNode' import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase' +import { editLambdaCommand } from '../commands/editLambda' +import { fs } from '../../shared/fs/fs' +import { makeChildrenNodes } from '../../shared/treeview/utils' +import { PlaceholderNode } from '../../shared/treeview/nodes/placeholderNode' +import path from 'path' +import { localize } from 'vscode-nls' +import { LambdaFunctionFolderNode } from './lambdaFunctionFolderNode' +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 projectRoot?: vscode.Uri, + public logicalId?: string ) { - super('') + super( + `${configuration.FunctionArn}`, + isLambdaFunctionDownloadable(contextValue) + ? vscode.TreeItemCollapsibleState.Collapsed + : vscode.TreeItemCollapsibleState.None + ) this.update(configuration) + this.resourceUri = vscode.Uri.from({ scheme: 'lambda', path: `${regionCode}/${configuration.FunctionName}` }) this.iconPath = getIcon('aws-lambda-function') + 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}` + if (this.contextValue === contextValueLambdaFunction) { + this.tooltip += `${os.EOL} This function is not downloadable` + } } public get functionName(): string { @@ -46,4 +80,35 @@ export class LambdaFunctionNode extends AWSTreeNodeBase implements AWSResourceNo return this.configuration.FunctionName } + + public override async getChildren(): Promise { + if (!isLambdaFunctionDownloadable(this.contextValue)) { + return [] + } + + return await makeChildrenNodes({ + getChildNodes: async () => { + const path = await editLambdaCommand(this) + return path ? this.loadFunctionFiles(path) : [] + }, + getNoChildrenPlaceholderNode: async () => + new PlaceholderNode(this, localize('AWS.explorerNode.lambda.noFiles', '[No files found]')), + }) + } + + public async loadFunctionFiles(tmpPath: string): Promise { + const nodes: AWSTreeNodeBase[] = [] + const files = await fs.readdir(tmpPath) + for (const file of files) { + const [fileName, type] = file + const filePath = path.join(tmpPath, fileName) + if (type === vscode.FileType.Directory) { + nodes.push(new LambdaFunctionFolderNode(this, fileName, filePath)) + } else { + nodes.push(new LambdaFunctionFileNode(this, fileName, filePath)) + } + } + + return nodes + } } diff --git a/packages/core/src/lambda/explorer/lambdaFunctionNodeDecorationProvider.ts b/packages/core/src/lambda/explorer/lambdaFunctionNodeDecorationProvider.ts new file mode 100644 index 00000000000..b31de940991 --- /dev/null +++ b/packages/core/src/lambda/explorer/lambdaFunctionNodeDecorationProvider.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 { fs } from '../../shared/fs/fs' +import path from 'path' +import { getFunctionInfo } from '../utils' +import { LambdaFunction } from '../commands/uploadLambda' + +export class LambdaFunctionNodeDecorationProvider implements vscode.FileDecorationProvider { + // Make it a singleton so that it's easier to access + private static instance: LambdaFunctionNodeDecorationProvider + private readonly _onDidChangeFileDecorations = new vscode.EventEmitter() + readonly onDidChangeFileDecorations = this._onDidChangeFileDecorations.event + + private constructor() {} + + public static getInstance(): LambdaFunctionNodeDecorationProvider { + if (!LambdaFunctionNodeDecorationProvider.instance) { + LambdaFunctionNodeDecorationProvider.instance = new LambdaFunctionNodeDecorationProvider() + } + return LambdaFunctionNodeDecorationProvider.instance + } + + async provideFileDecoration(uri: vscode.Uri): Promise { + const badge = { + badge: 'M', + color: new vscode.ThemeColor('gitDecoration.modifiedResourceForeground'), + tooltip: 'This function has undeployed changes', + propagate: true, + } + + if (uri.scheme === 'lambda') { + const [region, name] = uri.path.split('/') + const lambda: LambdaFunction = { region, name } + if (await getFunctionInfo(lambda, 'undeployed')) { + badge.propagate = false + return badge + } + } else { + try { + const lambda = this.getLambdaFromPath(uri) + if (lambda && (await this.isFileModifiedAfterDeployment(uri.fsPath, lambda))) { + return badge + } + } catch { + return undefined + } + } + } + + public async addBadge(fileUri: vscode.Uri, functionUri: vscode.Uri) { + this._onDidChangeFileDecorations.fire(vscode.Uri.file(fileUri.fsPath)) + this._onDidChangeFileDecorations.fire(functionUri) + } + + public async removeBadge(fileUri: vscode.Uri, functionUri: vscode.Uri) { + // We need to propagate the badge removal down to all files in the dir + for (const path of await this.getFilePaths(fileUri.fsPath)) { + const subUri = vscode.Uri.file(path) + this._onDidChangeFileDecorations.fire(subUri) + } + this._onDidChangeFileDecorations.fire(functionUri) + } + + private async getFilePaths(basePath: string) { + const files = await fs.readdir(basePath) + const subFiles: string[] = [basePath] + for (const file of files) { + const [fileName, type] = file + const filePath = path.join(basePath, fileName) + if (type === vscode.FileType.Directory) { + subFiles.push(...(await this.getFilePaths(filePath))) + } else { + subFiles.push(filePath) + } + } + + return subFiles + } + + private getLambdaFromPath(uri: vscode.Uri): LambdaFunction { + const pathParts = uri.fsPath.split(path.sep) + const lambdaIndex = pathParts.indexOf('lambda') + if (lambdaIndex === -1 || lambdaIndex + 2 >= pathParts.length) { + throw new Error('Invalid path') + } + const region = pathParts[lambdaIndex + 1] + const name = pathParts[lambdaIndex + 2] + return { region, name } + } + + private async isFileModifiedAfterDeployment(filePath: string, lambda: LambdaFunction): Promise { + try { + const { lastDeployed, undeployed } = await getFunctionInfo(lambda) + if (!lastDeployed || !undeployed) { + return false + } + + const fileStat = await fs.stat(filePath) + return fileStat.mtime > lastDeployed + } catch { + return false + } + } +} diff --git a/packages/core/src/lambda/explorer/lambdaNodes.ts b/packages/core/src/lambda/explorer/lambdaNodes.ts index 9adff42f04d..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,12 +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 { LambdaFunctionNode } from './lambdaFunctionNode' +import { listLambdaFunctions, isHotReloadingFunction } from '../utils' +import { + contextValueLambdaFunction, + contextValueLambdaFunctionImportable, + contextValueLambdaFunctionDownloadOnly, + LambdaFunctionNode, +} from './lambdaFunctionNode' import { samLambdaImportableRuntimes } from '../models/samLambdaRuntime' - -export const contextValueLambdaFunction = 'awsRegionFunctionNode' -export const contextValueLambdaFunctionImportable = 'awsRegionFunctionNodeDownloadable' +import { isLocalStackConnection } from '../../auth/utils' /** * An AWS Explorer node representing the Lambda Service. @@ -51,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 ) @@ -68,12 +71,18 @@ export class LambdaNode extends AWSTreeNodeBase { function makeLambdaFunctionNode( parent: AWSTreeNodeBase, regionCode: string, - configuration: Lambda.FunctionConfiguration + configuration: FunctionConfiguration ): LambdaFunctionNode { - const node = new LambdaFunctionNode(parent, regionCode, configuration) - node.contextValue = samLambdaImportableRuntimes.contains(node.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 58311474d41..985e947afc9 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,7 @@ 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', + 'nodejs22.x' as Runtime, 'nodejs20.x', 'nodejs18.x', 'nodejs16.x', @@ -51,7 +51,7 @@ export function getNodeMajorVersion(version?: string): number | undefined { } export const pythonRuntimes: ImmutableSet = ImmutableSet([ - 'python3.13', + 'python3.13' as Runtime, 'python3.12', 'python3.11', 'python3.10', @@ -68,7 +68,10 @@ export const javaRuntimes: ImmutableSet = ImmutableSet([ 'java21', ]) export const dotNetRuntimes: ImmutableSet = ImmutableSet(['dotnet6', 'dotnet8']) -export const rubyRuntimes: ImmutableSet = ImmutableSet(['ruby3.2', 'ruby3.3']) +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,14 +94,24 @@ export const deprecatedRuntimes: ImmutableSet = ImmutableSet([ 'ruby2.7', ]) const defaultRuntimes = ImmutableMap([ - [RuntimeFamily.NodeJS, 'nodejs22.x'], - [RuntimeFamily.Python, 'python3.13'], + [RuntimeFamily.NodeJS, 'nodejs22.x' as Runtime], + [RuntimeFamily.Python, 'python3.13' as Runtime], [RuntimeFamily.DotNet, 'dotnet8'], [RuntimeFamily.Go, 'go1.x'], [RuntimeFamily.Java, 'java21'], [RuntimeFamily.Ruby, 'ruby3.3'], ]) +export const mapFamilyToDebugType = ImmutableMap([ + [RuntimeFamily.NodeJS, 'node'], + [RuntimeFamily.Python, 'python'], + [RuntimeFamily.DotNet, 'csharp'], + [RuntimeFamily.Go, 'go'], + [RuntimeFamily.Java, 'java'], + [RuntimeFamily.Ruby, 'ruby'], + [RuntimeFamily.Unknown, 'unknown'], +]) + export const samZipLambdaRuntimes: ImmutableSet = ImmutableSet.union([ nodeJsRuntimes, pythonRuntimes, @@ -110,7 +123,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', @@ -125,14 +138,16 @@ export const samArmLambdaRuntimes: ImmutableSet = ImmutableSet const cloud9SupportedRuntimes: ImmutableSet = ImmutableSet.union([nodeJsRuntimes, pythonRuntimes]) // only interpreted languages are importable as compiled languages won't provide a useful artifact for editing. -export const samLambdaImportableRuntimes: ImmutableSet = ImmutableSet.union([nodeJsRuntimes, pythonRuntimes]) +export const samLambdaImportableRuntimes: ImmutableSet = ImmutableSet.union([ + nodeJsRuntimes, + pythonRuntimes, + rubyRuntimes, +]) export function samLambdaCreatableRuntimes(cloud9: boolean = isCloud9()): ImmutableSet { 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])]) @@ -158,7 +173,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)) { @@ -234,7 +249,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 new file mode 100644 index 00000000000..020d13d5ef2 --- /dev/null +++ b/packages/core/src/lambda/remoteDebugging/ldkClient.ts @@ -0,0 +1,481 @@ +/*! + * 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 { + 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, getLambdaDebugUserAgent } from './utils' +import { ToolkitError } from '../../shared/errors' +import * as nls from 'vscode-nls' + +const localize = nls.loadMessageBundle() + +export function isTunnelInfo(data: TunnelInfo): data is TunnelInfo { + return ( + typeof data === 'object' && + data !== null && + typeof data.tunnelID === 'string' && + typeof data.sourceToken === 'string' && + typeof data.destinationToken === 'string' + ) +} + +export interface TunnelInfo { + tunnelID: string + sourceToken: string + destinationToken: string +} + +async function callUpdateFunctionConfiguration( + lambda: DefaultLambdaClient, + config: FunctionConfiguration, + waitForUpdate: boolean +): Promise { + // Update function configuration back to original values + return await lambda.updateFunctionConfiguration( + { + FunctionName: config.FunctionName!, + Timeout: config.Timeout, + Layers: config.Layers?.map((layer) => layer.Arn!).filter(Boolean) || [], + Environment: { + Variables: config.Environment?.Variables ?? {}, + }, + }, + { + maxRetries: 5, + initialDelayMs: 2000, + backoffMultiplier: 2, + waitForUpdate: waitForUpdate, + } + ) +} + +export class LdkClient { + static #instance: LdkClient + private localProxy: LocalProxy | undefined + private static instanceCreating = false + private lambdaClientCache: Map = new Map() + private iotSTClientCache: Map = new Map() + + constructor() {} + + public static get instance() { + if (this.#instance !== undefined) { + return this.#instance + } + if (this.instanceCreating) { + getLogger().warn( + localize( + 'AWS.lambda.ldkClient.multipleInstancesError', + 'Attempt to create multiple LdkClient instances simultaneously' + ) + ) + } + // Set flag to prevent recursive instance creation + this.instanceCreating = true + try { + const self = (this.#instance = new this()) + return self + } finally { + this.instanceCreating = false + } + } + + /** + * Get or create a cached Lambda client for the specified region + */ + private getLambdaClient(region: string): DefaultLambdaClient { + if (!this.lambdaClientCache.has(region)) { + this.lambdaClientCache.set(region, getLambdaClientWithAgent(region, getLambdaDebugUserAgent())) + } + return this.lambdaClientCache.get(region)! + } + + private getIoTSTClient(region: string): IoTSecureTunnelingClient { + if (!this.iotSTClientCache.has(region)) { + this.iotSTClientCache.set(region, getIoTSTClientWithAgent(region)) + } + return this.iotSTClientCache.get(region)! + } + /** + * Clean up all resources held by this client + * Should be called when the extension is deactivated + */ + public dispose(): void { + if (this.localProxy) { + this.localProxy.stop() + this.localProxy = undefined + } + // Clear the Lambda client cache + this.iotSTClientCache.clear() + this.lambdaClientCache.clear() + } + + // Create or reuse tunnel + async createOrReuseTunnel(region: string): Promise { + try { + // Get VSCode UUID using getClientId from telemetry.utils.ts + const vscodeUuid = getClientId(globals.globalState) + + // Create IoTSecureTunneling client + const iotSecureTunneling = this.getIoTSTClient(region) + + // Define tunnel identifier + const tunnelIdentifier = `RemoteDebugging+${vscodeUuid}` + const timeoutInMinutes = 720 + // List existing tunnels + const listTunnelsResponse = await iotSecureTunneling.send(new ListTunnelsCommand({})) + + // Find tunnel with our identifier + const existingTunnel = listTunnelsResponse.tunnelSummaries?.find( + (tunnel) => tunnel.description === tunnelIdentifier && tunnel.status?.toLowerCase() === 'open' + ) + + if (existingTunnel && existingTunnel.tunnelId) { + const timeCreated = existingTunnel?.createdAt ? new Date(existingTunnel.createdAt) : new Date() + const expiryTime = new Date(timeCreated.getTime() + timeoutInMinutes * 60 * 1000) + const currentTime = new Date() + const minutesRemaining = (expiryTime.getTime() - currentTime.getTime()) / (60 * 1000) + + if (minutesRemaining >= 15) { + // Rotate access tokens for the existing tunnel + const rotateResponse = await this.refreshTunnelTokens(existingTunnel.tunnelId, region) + + return rotateResponse + } else { + // Close tunnel if less than 15 minutes remaining + await iotSecureTunneling.send( + new CloseTunnelCommand({ + tunnelId: existingTunnel.tunnelId, + delete: false, + }) + ) + + getLogger().info(`Closed tunnel ${existingTunnel.tunnelId} with less than 15 minutes remaining`) + } + } + + // Create new tunnel + const openTunnelResponse = await iotSecureTunneling.send( + new OpenTunnelCommand({ + description: tunnelIdentifier, + timeoutConfig: { + maxLifetimeTimeoutMinutes: timeoutInMinutes, // 12 hours + }, + destinationConfig: { + services: ['WSS'], + }, + }) + ) + + getLogger().info(`Created new tunnel with ID: ${openTunnelResponse.tunnelId}`) + + return { + tunnelID: openTunnelResponse.tunnelId || '', + sourceToken: openTunnelResponse.sourceAccessToken || '', + destinationToken: openTunnelResponse.destinationAccessToken || '', + } + } catch (error) { + throw ToolkitError.chain(error, 'Error creating/reusing tunnel') + } + } + + // Refresh tunnel tokens + async refreshTunnelTokens(tunnelId: string, region: string): Promise { + try { + const iotSecureTunneling = this.getIoTSTClient(region) + const rotateResponse = await iotSecureTunneling.send( + new RotateTunnelAccessTokenCommand({ + tunnelId: tunnelId, + clientMode: 'ALL', + }) + ) + + return { + tunnelID: tunnelId, + sourceToken: rotateResponse.sourceAccessToken || '', + destinationToken: rotateResponse.destinationAccessToken || '', + } + } catch (error) { + throw ToolkitError.chain(error, 'Error refreshing tunnel tokens') + } + } + + async getFunctionDetail(functionArn: string): Promise { + try { + const region = getRegionFromArn(functionArn) + if (!region) { + getLogger().error( + localize( + 'AWS.lambda.ldkClient.couldNotDetermineRegion', + 'Could not determine region from Lambda ARN' + ) + ) + return undefined + } + const client = this.getLambdaClient(region) + const configuration = (await client.getFunction(functionArn)).Configuration as FunctionConfiguration + // get function detail + // return function detail + return configuration + } catch (error) { + getLogger().warn(`Error getting function detail:${error}`) + return undefined + } + } + + // Create debug deployment to given lambda function + // save a snapshot of the current config to global : aws.lambda.remoteDebugContext + // we are 1: changing function timeout to 15 minute + // 2: adding the ldk layer LDK_LAYER_ARN_X86_64 or LDK_LAYER_ARN_ARM64 (ignore if already added, fail if 5 layer already there) + // 3: adding two param to lambda environment variable + // {AWS_LAMBDA_EXEC_WRAPPER:/opt/bin/ldk_wrapper, AWS_LDK_DESTINATION_TOKEN: destinationToken } + async createDebugDeployment( + config: FunctionConfiguration, + destinationToken: string, + lambdaTimeout: number, + shouldPublishVersion: boolean, + ldkLayerArn: string, + progress: vscode.Progress<{ message?: string | undefined; increment?: number | undefined }> + ): Promise { + try { + if (!config.FunctionArn || !config.FunctionName) { + throw new Error(localize('AWS.lambda.ldkClient.functionArnMissing', 'Function ARN is missing')) + } + const region = getRegionFromArn(config.FunctionArn ?? '') + if (!region) { + throw new Error( + localize( + 'AWS.lambda.ldkClient.couldNotDetermineRegion', + 'Could not determine region from Lambda ARN' + ) + ) + } + + // fix out of bound timeout + if (lambdaTimeout && (lambdaTimeout > 900 || lambdaTimeout <= 0)) { + lambdaTimeout = 900 + } + + // Inform user about the changes that will be made + + progress.report({ message: localize('AWS.lambda.ldkClient.applyingChanges', 'Applying changes...') }) + + // Determine architecture and select appropriate layer + + const layers = config.Layers || [] + + // Check if LDK layer is already added + const ldkLayerExists = layers.some( + (layer) => layer.Arn?.includes('LDKLayerX86') || layer.Arn?.includes('LDKLayerArm64') + ) + + // Check if we have room to add a layer (max 5) + if (!ldkLayerExists && layers.length >= 5) { + throw new Error( + localize( + 'AWS.lambda.ldkClient.cannotAddLdkLayer', + 'Cannot add LDK layer: Lambda function already has 5 layers' + ) + ) + } + // Create updated layers list + const updatedLayers = ldkLayerExists + ? layers.map((layer) => layer.Arn!).filter(Boolean) + : [...layers.map((layer) => layer.Arn!).filter(Boolean), ldkLayerArn] + + // Create updated environment variables + const currentEnv = config.Environment?.Variables || {} + const updatedEnv: { [key: string]: string } = { + ...currentEnv, + AWS_LAMBDA_EXEC_WRAPPER: '/opt/bin/ldk_wrapper', + AWS_LAMBDA_DEBUG_ON_LATEST: shouldPublishVersion ? 'false' : 'true', + AWS_LDK_DESTINATION_TOKEN: destinationToken, + } + if (currentEnv['AWS_LAMBDA_EXEC_WRAPPER']) { + 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) + + // Update function configuration + if (!config.FunctionArn || !config.FunctionName) { + throw new Error('Function ARN is missing') + } + + // Create a temporary config for the update + const updateConfig: FunctionConfiguration = { + FunctionName: config.FunctionName, + Timeout: lambdaTimeout ?? 900, // 15 minutes + Layers: updatedLayers.map((arn) => ({ Arn: arn })), + Environment: { + Variables: updatedEnv, + }, + } + + await callUpdateFunctionConfiguration(lambda, updateConfig, true) + + // publish version + let version = '$Latest' + if (shouldPublishVersion) { + // should somehow return version for debugging + const versionResp = await lambda.publishVersion(config.FunctionName, { waitForUpdate: true }) + version = versionResp.Version ?? '' + // remove debug deployment in a non-blocking way + void Promise.resolve( + callUpdateFunctionConfiguration(lambda, config, false).then(() => { + progress.report({ + message: localize( + 'AWS.lambda.ldkClient.debugDeploymentCompleted', + 'Debug deployment completed successfully' + ), + }) + }) + ) + } + return version + } catch (error) { + getLogger().error(`Error creating debug deployment: ${error}`) + if (error instanceof Error) { + throw new ToolkitError(`Failed to create debug deployment: ${error.message}`) + } + return 'Failed' + } + } + + // Remove debug deployment from the given lambda function + // use the snapshot we took before create debug deployment + // 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: FunctionConfiguration, check: boolean = true): Promise { + try { + if (!config.FunctionArn || !config.FunctionName) { + throw new Error('Function ARN is missing') + } + const region = getRegionFromArn(config.FunctionArn ?? '') + if (!region) { + throw new Error('Could not determine region from Lambda ARN') + } + + if (check) { + const currentConfig = await this.getFunctionDetail(config.FunctionArn) + if ( + currentConfig?.Timeout === config?.Timeout && + currentConfig?.Layers?.length === config?.Layers?.length + ) { + // nothing to remove + return true + } + } + + // Create Lambda client using AWS SDK + const lambda = this.getLambdaClient(region) + + // Update function configuration back to original values + await callUpdateFunctionConfiguration(lambda, config, false) + + return true + } catch (error) { + // no need to raise, even this failed we want the following to execute + throw ToolkitError.chain(error, 'Error removing debug deployment') + } + } + + async deleteDebugVersion(functionArn: string, qualifier: string) { + try { + const region = getRegionFromArn(functionArn) + if (!region) { + throw new Error('Could not determine region from Lambda ARN') + } + const lambda = this.getLambdaClient(region) + await lambda.deleteFunction(functionArn, qualifier) + return true + } catch (error) { + getLogger().error('Error deleting debug version: %O', error) + return false + } + } + + // Start proxy with better resource management + async startProxy(region: string, sourceToken: string, port: number = 0): Promise { + try { + getLogger().info(`Starting direct proxy for region:${region}`) + + // Clean up any existing proxy thoroughly + if (this.localProxy) { + getLogger().info('Stopping existing proxy before starting a new one') + this.localProxy.stop() + this.localProxy = undefined + + // Small delay to ensure resources are released + await new Promise((resolve) => setTimeout(resolve, 100)) + } + + // Create and start a new local proxy + this.localProxy = new LocalProxy() + + // Start the proxy and get the assigned port + const localPort = await this.localProxy.start(region, sourceToken, port) + getLogger().info(`Local proxy started successfully on port ${localPort}`) + return true + } catch (error) { + getLogger().error(`Failed to start proxy: ${error}`) + if (this.localProxy) { + this.localProxy.stop() + this.localProxy = undefined + } + throw ToolkitError.chain(error, 'Failed to start proxy') + } + } + + // Stop proxy with proper cleanup and reference handling + async stopProxy(): Promise { + try { + getLogger().info(`Stopping proxy`) + + if (this.localProxy) { + // Ensure proper resource cleanup + this.localProxy.stop() + + // Force delete the reference to allow GC + this.localProxy = undefined + + getLogger().info('Local proxy stopped successfully') + } else { + getLogger().info('No active local proxy to stop') + } + + return true + } catch (error) { + throw ToolkitError.chain(error, 'Error stopping proxy') + } + } +} + +// Helper function to extract region from ARN +export function getRegionFromArn(arn: string | undefined): string | undefined { + if (!arn) { + return undefined + } + const parts = arn.split(':') + return parts.length >= 4 ? parts[3] : undefined +} diff --git a/packages/core/src/lambda/remoteDebugging/ldkController.ts b/packages/core/src/lambda/remoteDebugging/ldkController.ts new file mode 100644 index 00000000000..5dfbd653dc1 --- /dev/null +++ b/packages/core/src/lambda/remoteDebugging/ldkController.ts @@ -0,0 +1,811 @@ +/*! + * 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' +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 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() + +// Map debug types to their corresponding VS Code extension IDs +const mapDebugTypeToExtensionId = new Map([ + ['python', ['ms-python.python']], + ['java', ['redhat.java', 'vscjava.vscode-java-debug']], + ['node', ['ms-vscode.js-debug']], +]) + +const mapExtensionToBackup = new Map([['ms-vscode.js-debug', 'ms-vscode.js-debug-nightly']]) + +// Helper function to create a human-readable diff message +function createDiffMessage( + 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' + + message += + '1. Timeout: ' + + (currentConfig.Timeout || 'default') + + ' seconds → ' + + (config.Timeout || 'default') + + ' seconds\n' + + message += '2. Layers: ' + const hasLdkLayer = currentConfig.Layers?.some( + (layer) => layer.Arn?.includes('LDKLayerX86') || layer.Arn?.includes('LDKLayerArm64') + ) + + message += hasLdkLayer ? 'Remove LDK layer\n' : 'No Change\n' + + message += '3. Environment Variables: Remove AWS_LAMBDA_EXEC_WRAPPER and AWS_LDK_DESTINATION_TOKEN\n' + + return message +} + +/** + * Attempts to revert an existing debug configuration if one exists + * @returns true if revert was successful or no config exists, false if revert failed or user chose not to revert + */ +export async function revertExistingConfig(): Promise { + try { + // Check if a debug context exists from a previous session + const savedConfig = getLambdaSnapshot() + + if (!savedConfig) { + // No existing config to revert + return true + } + + // clear the snapshot for it's corrupted + if (!savedConfig.FunctionArn || !savedConfig.FunctionName) { + logger.error('Function ARN or Function Name is missing, cannot revert') + void (await persistLambdaSnapshot(undefined)) + return true + } + + // compare with current config + const currentConfig = await LdkClient.instance.getFunctionDetail(savedConfig.FunctionArn) + // could be permission issues, or user has deleted previous function, we should remove the snapshot + if (!currentConfig) { + logger.error('Failed to get current function state, cannot revert') + void (await persistLambdaSnapshot(undefined)) + return true + } + + if ( + currentConfig?.Timeout === savedConfig?.Timeout && + currentConfig?.Layers?.length === savedConfig?.Layers?.length + ) { + // No changes needed, remove the snapshot + void (await persistLambdaSnapshot(undefined)) + return true + } + + // Create a diff message to show what will be changed + const diffMessage = currentConfig + ? createDiffMessage(savedConfig, currentConfig, true) + : 'Failed to get current function state' + + const response = await showConfirmationMessage({ + prompt: localize( + 'AWS.lambda.remoteDebug.revertPreviousDeployment', + 'A previous debug deployment was detected for {0}. Would you like to revert those changes before proceeding?\n\n{1}', + savedConfig.FunctionName, + diffMessage + ), + confirm: localize('AWS.lambda.remoteDebug.revert', 'Revert'), + cancel: localize('AWS.lambda.remoteDebug.dontShowAgain', "Don't show again"), + type: 'warning', + }) + + if (!response) { + // User chose not to revert, remove the snapshot + void (await persistLambdaSnapshot(undefined)) + return true + } + + await LdkClient.instance.removeDebugDeployment(savedConfig, false) + await persistLambdaSnapshot(undefined) + void showMessage( + 'info', + localize( + 'AWS.lambda.remoteDebug.successfullyReverted', + 'Successfully reverted changes to {0}', + savedConfig.FunctionName + ) + ) + + return true + } catch (error) { + throw ToolkitError.chain(error, `Error in revertExistingConfig`) + } +} + +export async function activateRemoteDebugging(): Promise { + try { + globals.context.subscriptions.push( + Commands.register('aws.lambda.remoteDebugging.clearSnapshot', async () => { + void (await persistLambdaSnapshot(undefined)) + }) + ) + } catch (error) { + logger.error(`Error in registering clearSnapshot command:${error}`) + } + + try { + logger.info('Remote debugging is initiated') + + // Use the revertExistingConfig function to handle any existing debug configurations + await revertExistingConfig() + + // Initialize RemoteDebugController to ensure proper startup state + RemoteDebugController.instance.ensureCleanState() + } catch (error) { + // show warning + void vscode.window.showWarningMessage(`Error in activateRemoteDebugging: ${error}`) + logger.error(`Error in activateRemoteDebugging:${error}`) + } +} + +/** + * 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 { + // 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) { + getLogger().warn(`Failed to auto-detect CDK outFile: ${error}`) + } + + return undefined +} + +/** + * Helper function to check if a string is a valid VSCode glob pattern + */ +function isVscodeGlob(pattern: string): boolean { + // Check for common glob patterns: *, **, ?, [], {} + return /[*?[\]{}]/.test(pattern) +} + +/** + * Helper function to validate source map files exist for given outFiles patterns + */ +async function validateSourceMapFiles(outFiles: string[]): Promise { + const allAreGlobs = outFiles.every((pattern) => isVscodeGlob(pattern)) + if (!allAreGlobs) { + return false + } + + try { + let jsfileCount = 0 + let mapfileCount = 0 + const jsFiles = await glob(outFiles, { ignore: 'node_modules/**' }) + + for (const file of jsFiles) { + if (file.includes('js')) { + jsfileCount += 1 + } + if (file.includes('.map')) { + mapfileCount += 1 + } + } + + return jsfileCount === 0 || mapfileCount === 0 ? false : true + } catch (error) { + getLogger().warn(`Error validating source map files: ${error}`) + return false + } +} + +function processOutFiles(outFiles: string[], localRoot: string): string[] { + const processedOutFiles: string[] = [] + + for (let outFile of outFiles) { + if (!outFile.includes('*')) { + // add * in the end + outFile = path.join(outFile, '*') + } + if (!path.isAbsolute(outFile)) { + // Find which workspace contains the localRoot path + const workspaceFolders = vscode.workspace.workspaceFolders + if (workspaceFolders) { + let matchingWorkspace: vscode.WorkspaceFolder | undefined + + // Check if localRoot is within any workspace + for (const workspace of workspaceFolders) { + const absoluteLocalRoot = path.resolve(localRoot) + const workspacePath = workspace.uri.fsPath + + if (absoluteLocalRoot.startsWith(workspacePath)) { + matchingWorkspace = workspace + break + } + } + + if (matchingWorkspace) { + // Join workspace folder with the relative outFile path + processedOutFiles.push(path.join(matchingWorkspace.uri.fsPath, outFile)) + } else { + // If no matching workspace found, use the original outFile + processedOutFiles.push(outFile) + } + } else { + // No workspace folders, use the original outFile + processedOutFiles.push(outFile) + } + } else { + // Already absolute path, use as is + processedOutFiles.push(outFile) + } + } + return processedOutFiles +} + +async function getVscodeDebugConfig( + functionConfig: FunctionConfiguration, + debugConfig: DebugConfig +): Promise { + // Parse and validate otherDebugParams if provided + let additionalParams: Record = {} + if (debugConfig.otherDebugParams) { + try { + const parsed = JSON.parse(debugConfig.otherDebugParams) + if (typeof parsed === 'object' && !Array.isArray(parsed)) { + additionalParams = parsed + getLogger().info('Additional debug parameters parsed successfully: %O ', additionalParams) + } else { + void vscode.window.showWarningMessage( + localize( + 'AWS.lambda.remoteDebug.invalidDebugParams', + 'Other Debug Parameters must be a valid JSON object. The parameter will be ignored.' + ) + ) + getLogger().warn(`Invalid otherDebugParams format: expected object, got ${typeof parsed}`) + } + } catch (error) { + void vscode.window.showWarningMessage( + localize( + 'AWS.lambda.remoteDebug.failedToParseDebugParams', + 'Failed to parse Other Debug Parameters as JSON: {0}. The parameter will be ignored.', + error instanceof Error ? error.message : 'Invalid JSON' + ) + ) + getLogger().warn(`Failed to parse otherDebugParams as JSON: ${error}`) + } + } + + const debugSessionName = `Debug ${functionConfig.FunctionArn!.split(':').pop()}` + + // Define debugConfig before the try block + 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 /), + // check local root path is located in which workspace. Then join workspace Folder with outFiles + + // Update debugConfig with processed outFiles + 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) + + if (hasSourceMaps) { + // support mapping common sam cli location + additionalParams['sourceMapPathOverrides'] = { + ...additionalParams['sourceMapPathOverrides'], + '?:*/T/?:*/*': path.join(debugConfig.localRoot, '*'), + } + debugConfig.localRoot = debugConfig.outFiles[0].split('*')[0] + } else { + debugConfig.sourceMap = false + debugConfig.outFiles = undefined + await showMessage( + 'warn', + localize( + 'AWS.lambda.remoteDebug.outFileNotFound', + 'outFiles not valid or no js and map file found in outFiles, debug will continue without sourceMap support' + ) + ) + } + } + vsCodeDebugConfig = { + type: debugType, + request: 'attach', + name: debugSessionName, + address: 'localhost', + port: debugConfig.port, + localRoot: debugConfig.localRoot, + remoteRoot: debugConfig.remoteRoot, + skipFiles: debugConfig.skipFiles, + sourceMaps: debugConfig.sourceMap, + outFiles: debugConfig.outFiles, + continueOnAttach: debugConfig.outFiles ? false : true, + stopOnEntry: false, + timeout: 60000, + ...additionalParams, // Merge additional debug parameters + } + break + case 'python': + vsCodeDebugConfig = { + type: debugType, + request: 'attach', + name: debugSessionName, + port: debugConfig.port, + cwd: debugConfig.localRoot, + pathMappings: [ + { + localRoot: debugConfig.localRoot, + remoteRoot: debugConfig.remoteRoot, + }, + ], + justMyCode: debugConfig.justMyCode ?? true, + ...additionalParams, // Merge additional debug parameters + } + break + case 'java': + vsCodeDebugConfig = { + type: debugType, + request: 'attach', + name: debugSessionName, + hostName: 'localhost', + port: debugConfig.port, + sourcePaths: [debugConfig.localRoot], + projectName: debugConfig.projectName, + timeout: 60000, + ...additionalParams, // Merge additional debug parameters + } + break + default: + throw new ToolkitError(`Unsupported debug type: ${debugType}`) + } + getLogger().info('VS Code debug configuration: %O', vsCodeDebugConfig) + return vsCodeDebugConfig +} + +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) { + return this.#instance + } + + const self = (this.#instance = new this()) + return self + } + + constructor() {} + + /** + * Ensures the controller is in a clean state at startup or before a new operation + */ + public ensureCleanState(): void { + this.isDebugging = false + this.qualifier = undefined + + // Clean up any leftover disposables + for (const [key, disposable] of this.debugSessionDisposables.entries()) { + try { + disposable.dispose() + } catch (e) { + // Ignore errors during startup cleanup + } + this.debugSessionDisposables.delete(key) + } + } + + 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 { + // deprecated runtime + return false + } + } + + public supportRuntimeRemoteDebug(runtime: Runtime | undefined): boolean { + if (!runtime) { + return false + } + try { + return ['node', 'python', 'java'].includes(mapFamilyToDebugType.get(getFamily(runtime)) ?? '') + } catch { + return false + } + } + + public async installDebugExtension(runtime: Runtime | undefined): Promise { + if (!runtime) { + throw new ToolkitError('Runtime is undefined') + } + + const debugType = mapFamilyToDebugType.get(getFamily(runtime)) + if (!debugType) { + throw new ToolkitError(`Debug type is undefined for runtime ${runtime}`) + } + // Install needed debug extension based on runtime + const extensions = mapDebugTypeToExtensionId.get(debugType) + if (extensions) { + for (const extension of extensions) { + const extensionObj = vscode.extensions.getExtension(extension) + const backupExtensionObj = vscode.extensions.getExtension(mapExtensionToBackup.get(extension) ?? '') + + if (!extensionObj && !backupExtensionObj) { + // Extension is not installed, install it + const choice = await showConfirmationMessage({ + prompt: localize( + 'AWS.lambda.remoteDebug.extensionNotInstalled', + 'You need to install the {0} extension to debug {1} functions. Would you like to install it now?', + extension, + debugType + ), + confirm: localize('AWS.lambda.remoteDebug.install', 'Install'), + cancel: localize('AWS.lambda.remoteDebug.cancel', 'Cancel'), + type: 'warning', + }) + if (!choice) { + return false + } + await vscode.commands.executeCommand('workbench.extensions.installExtension', extension) + if (vscode.extensions.getExtension(extension) === undefined) { + return false + } + } + } + } + + if (debugType === 'java' && !(await findJavaPath())) { + // jvm not available + const choice = await showConfirmationMessage({ + prompt: localize( + 'AWS.lambda.remoteDebug.jvmNotInstalled', + 'You need to install a JVM to debug Java functions. Would you like to install it now?' + ), + confirm: localize('AWS.lambda.remoteDebug.install', 'Install'), + cancel: localize('AWS.lambda.remoteDebug.continueAnyway', 'Continue Anyway'), + type: 'warning', + }) + // open https://developers.redhat.com/products/openjdk/download + if (choice) { + await vscode.env.openExternal( + vscode.Uri.parse('https://developers.redhat.com/products/openjdk/download') + ) + return false + } + } + // passed all checks + return true + } + + 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 + } + + await telemetry.lambda_remoteDebugStart.run(async (span) => { + // Create a copy of debugConfig without functionName and functionArn for telemetry + const debugConfigForTelemetry: Partial = { ...debugConfig } + debugConfigForTelemetry.functionName = undefined + debugConfigForTelemetry.functionArn = undefined + debugConfigForTelemetry.localRoot = undefined + + span.record({ + source: this.debugTypeSource, + passive: false, + action: JSON.stringify(debugConfigForTelemetry), + }) + this.lastDebugStartTime = Date.now() + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Setting up debug session', + cancellable: false, + }, + async (progress) => { + // Reset state before starting + this.ensureCleanState() + + getLogger().info(`Starting debugger for ${functionArn}`) + + const region = getRegionFromArn(functionArn) + if (!region) { + throw new ToolkitError('Could not determine region from Lambda ARN') + } + + // Check if runtime / region is supported for remote debugging + 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...' }) + const revertResult = await revertExistingConfig() + + // If revert failed and user didn't choose to ignore, abort the deployment + if (revertResult === false) { + return + } + try { + // Anything fails before this point doesn't requires reverting + this.isDebugging = true + + // the following will contain changes that requires reverting. + // Create a snapshot of lambda config before debug + // 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 + // get function config again in case anything changed + const functionConfig = await LdkClient.instance.getFunctionDetail(functionArn) + if (!functionConfig?.Runtime || !functionConfig?.FunctionArn) { + throw new ToolkitError('Could not retrieve Lambda function configuration') + } + await persistLambdaSnapshot(functionConfig) + + // Record runtime in telemetry + span.record({ + runtimeString: functionConfig.Runtime as any, + }) + + 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)}`) + + await this.debugger?.waitForSetup(progress, functionConfig, region) + + progress.report({ message: 'Starting debugger...' }) + // Start debugging in a non-blocking way + void Promise.resolve(vscode.debug.startDebugging(undefined, vscodeDebugConfig)).then( + async (debugStarted) => { + if (!debugStarted) { + // this could be triggered by another stop debugging, let's check state before stopping. + throw new ToolkitError('Failed to start debug session') + } + } + ) + + const debugSessionEndDisposable = vscode.debug.onDidTerminateDebugSession(async (session) => { + if (session.name === vscodeDebugConfig.name) { + void (await this.stopDebugging()) + } + }) + + await this.debugger?.waitForFunctionUpdates(progress) + + // Store the disposable + this.debugSessionDisposables.set(functionConfig.FunctionArn, debugSessionEndDisposable) + progress.report({ + message: `Debug session setup completed for ${functionConfig.FunctionArn.split(':').pop()}`, + }) + } catch (error) { + try { + await this.stopDebugging() + } catch (errStop) { + getLogger().error( + 'encountered following error when stopping debug for failed debug session:' + ) + getLogger().error(errStop as Error) + } + + throw ToolkitError.chain(error, 'Error StartDebugging') + } + } + ) + }) + } + + public async stopDebugging(): Promise { + await telemetry.lambda_remoteDebugStop.run(async (span) => { + if (!this.isDebugging) { + void showMessage( + 'info', + localize('AWS.lambda.remoteDebug.debugNotInProgress', 'Debug is not in progress') + ) + return + } + // use sessionDuration to record debug duration + span.record({ + sessionDuration: this.lastDebugStartTime === 0 ? 0 : Date.now() - this.lastDebugStartTime, + source: this.debugTypeSource, + }) + try { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Stopping debug session', + cancellable: false, + }, + async (progress) => { + progress.report({ message: 'Stopping debugging...' }) + + // First attempt to clean up resources from Lambda + const savedConfig = getLambdaSnapshot() + if (!savedConfig?.FunctionArn) { + getLogger().error('No saved configuration found during cleanup') + throw new ToolkitError('No saved configuration found during cleanup') + } + + const disposable = this.debugSessionDisposables.get(savedConfig.FunctionArn) + if (disposable) { + disposable.dispose() + this.debugSessionDisposables.delete(savedConfig.FunctionArn) + } + await this.debugger?.cleanup(savedConfig) + + progress.report({ message: `Debug session stopped` }) + } + ) + void showMessage( + 'info', + localize('AWS.lambda.remoteDebug.debugSessionStopped', 'Debug session stopped') + ) + } catch (error) { + throw ToolkitError.chain(error, 'error when stopping remote debug') + } finally { + this.isDebugging = false + } + }) + } +} diff --git a/packages/core/src/lambda/remoteDebugging/ldkLayers.ts b/packages/core/src/lambda/remoteDebugging/ldkLayers.ts new file mode 100644 index 00000000000..f0c5dff2c02 --- /dev/null +++ b/packages/core/src/lambda/remoteDebugging/ldkLayers.ts @@ -0,0 +1,46 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +interface RegionAccountMapping { + [region: string]: string +} + +// Map region to account ID +export const regionToAccount: RegionAccountMapping = { + 'us-east-1': '166855510987', + 'ap-northeast-1': '435951944084', + 'us-west-1': '397974708477', + 'us-west-2': '116489046076', + 'us-east-2': '372632330791', + 'ca-central-1': '816313119386', + 'eu-west-1': '020236748984', + 'eu-west-2': '199003954714', + 'eu-west-3': '490913546906', + 'eu-central-1': '944487268028', + 'eu-north-1': '351516301086', + 'ap-southeast-1': '812073016575', + 'ap-southeast-2': '185226997092', + 'ap-northeast-2': '241511115815', + 'ap-south-1': '926022987530', + 'sa-east-1': '313162186107', + 'ap-east-1': '416298298123', + 'me-south-1': '511027370648', + 'me-central-1': '766358817862', +} + +// Global layer version +const globalLayerVersion = 2 + +export function getRemoteDebugLayerForArch(region: string, arch: string): string | undefined { + const account = regionToAccount[region] + + if (!account) { + return undefined + } + + const layerName = arch === 'x86_64' ? 'LDKLayerX86' : 'LDKLayerArm64' + + return `arn:aws:lambda:${region}:${account}:layer:${layerName}:${globalLayerVersion}` +} diff --git a/packages/core/src/lambda/remoteDebugging/localProxy.ts b/packages/core/src/lambda/remoteDebugging/localProxy.ts new file mode 100644 index 00000000000..8b228deeb1a --- /dev/null +++ b/packages/core/src/lambda/remoteDebugging/localProxy.ts @@ -0,0 +1,901 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as net from 'net' +import WebSocket from 'ws' +import * as crypto from 'crypto' +import { getLogger } from '../../shared/logger/logger' +import { v4 as uuidv4 } from 'uuid' +import * as protobuf from 'protobufjs' + +const logger = getLogger() + +// Define the message types from the protocol +enum MessageType { + UNKNOWN = 0, + DATA = 1, + STREAM_START = 2, + STREAM_RESET = 3, + SESSION_RESET = 4, + SERVICE_IDS = 5, + CONNECTION_START = 6, + CONNECTION_RESET = 7, +} + +// Interface for tunnel info +export interface TunnelInfo { + tunnelId: string + sourceToken: string + destinationToken: string +} + +// Interface for TCP connection +interface TcpConnection { + socket: net.Socket + streamId: number + connectionId: number +} + +/** + * LocalProxy class that handles WebSocket connection to IoT secure tunneling + * and sets up a TCP adapter as a local proxy + */ +export class LocalProxy { + private ws: WebSocket.WebSocket | undefined = undefined + private tcpServer: net.Server | undefined = undefined + private tcpConnections: Map = new Map() + private isConnected: boolean = false + private reconnectAttempts: number = 0 + private maxReconnectAttempts: number = 10 + private reconnectInterval: number = 2500 // 2.5 seconds + private pingInterval: NodeJS.Timeout | undefined = undefined + private serviceId: string = 'WSS' + private currentStreamId: number = 1 + private nextConnectionId: number = 1 + private localPort: number = 0 + private region: string = '' + private accessToken: string = '' + private Message: protobuf.Type | undefined = undefined + private clientToken: string = '' + private eventHandlers: { [key: string]: any[] } = {} + private isDisposed: boolean = false + + constructor() { + void this.loadProtobufDefinition() + } + + // Define the protobuf schema as a string constant + private static readonly protobufSchema = ` + syntax = "proto3"; + + package com.amazonaws.iot.securedtunneling; + + message Message { + Type type = 1; + int32 streamId = 2; + bool ignorable = 3; + bytes payload = 4; + string serviceId = 5; + repeated string availableServiceIds = 6; + uint32 connectionId = 7; + + enum Type { + UNKNOWN = 0; + DATA = 1; + STREAM_START = 2; + STREAM_RESET = 3; + SESSION_RESET = 4; + SERVICE_IDS = 5; + CONNECTION_START = 6; + CONNECTION_RESET = 7; + } + }` + + /** + * Load the protobuf definition from the embedded schema string + */ + private async loadProtobufDefinition(): Promise { + try { + if (this.Message) { + // Already loaded, don't parse again + return + } + + const root = protobuf.parse(LocalProxy.protobufSchema).root + this.Message = root.lookupType('com.amazonaws.iot.securedtunneling.Message') + + if (!this.Message) { + throw new Error('Failed to load Message type from protobuf definition') + } + + logger.debug('Protobuf definition loaded successfully') + } catch (error) { + logger.error(`Error loading protobuf definition:${error}`) + throw error + } + } + + /** + * Start the local proxy + * @param region AWS region + * @param sourceToken Source token for the tunnel + * @param port Local port to listen on + */ + public async start(region: string, sourceToken: string, port: number = 0): Promise { + // Reset disposal state when starting + this.isDisposed = false + + this.region = region + this.accessToken = sourceToken + + try { + // Start TCP server first + this.localPort = await this.startTcpServer(port) + + // Then connect to WebSocket + await this.connectWebSocket() + + return this.localPort + } catch (error) { + logger.error(`Failed to start local proxy:${error}`) + this.stop() + throw error + } + } + + /** + * Stop the local proxy and clean up all resources + */ + public stop(): void { + if (this.isDisposed) { + logger.debug('LocalProxy already stopped, skipping duplicate stop call') + return + } + + logger.debug('Stopping LocalProxy and cleaning up resources') + + // Cancel any pending reconnect timeouts + if (this.eventHandlers['reconnectTimeouts']) { + for (const timeoutId of this.eventHandlers['reconnectTimeouts']) { + clearTimeout(timeoutId as NodeJS.Timeout) + } + } + + this.stopPingInterval() + this.closeWebSocket() + this.closeTcpServer() + + // Reset all state + this.clientToken = '' + this.isConnected = false + this.reconnectAttempts = 0 + this.currentStreamId = 1 + this.nextConnectionId = 1 + this.localPort = 0 + this.region = '' + this.accessToken = '' + + // Mark as disposed to prevent duplicate stop calls + this.isDisposed = true + + // Clear any remaining event handlers reference + this.eventHandlers = {} + } + + /** + * Start the TCP server + * @param port Port to listen on (0 for random port) + * @returns The port the server is listening on + */ + private startTcpServer(port: number): Promise { + return new Promise((resolve, reject) => { + try { + this.tcpServer = net.createServer((socket) => { + this.handleNewTcpConnection(socket) + }) + + this.tcpServer.on('error', (err) => { + logger.error(`TCP server error:${err}`) + }) + + this.tcpServer.listen(port, '127.0.0.1', () => { + const address = this.tcpServer?.address() as net.AddressInfo + this.localPort = address.port + logger.debug(`TCP server listening on port ${this.localPort}`) + resolve(this.localPort) + }) + } catch (error) { + logger.error(`Failed to start TCP server:${error}`) + reject(error) + } + }) + } + + /** + * Close the TCP server and all connections + */ + private closeTcpServer(): void { + if (this.tcpServer) { + logger.debug('Closing TCP server and connections') + + // Remove all listeners from the server + this.tcpServer.removeAllListeners('error') + this.tcpServer.removeAllListeners('connection') + this.tcpServer.removeAllListeners('listening') + + // Close all TCP connections with proper error handling + for (const connection of this.tcpConnections.values()) { + try { + // Remove all listeners before destroying + connection.socket.removeAllListeners('data') + connection.socket.removeAllListeners('error') + connection.socket.removeAllListeners('close') + connection.socket.destroy() + } catch (err) { + logger.error(`Error closing TCP connection: ${err}`) + } + } + this.tcpConnections.clear() + + // Close the server with proper error handling and timeout + try { + // Set a timeout in case server.close() hangs + const serverCloseTimeout = setTimeout(() => { + logger.warn('TCP server close timed out, forcing closure') + this.tcpServer = undefined + }, 5000) + + this.tcpServer.close(() => { + clearTimeout(serverCloseTimeout) + logger.debug('TCP server closed successfully') + this.tcpServer = undefined + }) + } catch (err) { + logger.error(`Error closing TCP server: ${err}`) + this.tcpServer = undefined + } + } + } + + /** + * Handle a new TCP connection with proper resource management + * @param socket The TCP socket + */ + private handleNewTcpConnection(socket: net.Socket): void { + if (!this.isConnected || this.isDisposed) { + logger.warn('WebSocket not connected or proxy disposed, rejecting TCP connection') + socket.destroy() + return + } + + const connectionId = this.nextConnectionId++ + const streamId = this.currentStreamId + + logger.debug(`New TCP connection: ${connectionId}`) + + // Track event handlers for this connection + const handlers: { [event: string]: (...args: any[]) => void } = {} + + // Data handler + const dataHandler = (data: Buffer) => { + this.sendData(streamId, connectionId, data) + } + socket.on('data', dataHandler) + handlers.data = dataHandler + + // Error handler + const errorHandler = (err: Error) => { + logger.error(`TCP connection ${connectionId} error: ${err}`) + this.sendConnectionReset(streamId, connectionId) + + // Cleanup handlers on error + this.cleanupSocketHandlers(socket, handlers) + } + socket.on('error', errorHandler) + handlers.error = errorHandler + + // Close handler + const closeHandler = () => { + logger.debug(`TCP connection ${connectionId} closed`) + + // Remove from connections map and send reset + this.tcpConnections.delete(connectionId) + this.sendConnectionReset(streamId, connectionId) + + // Cleanup handlers on close + this.cleanupSocketHandlers(socket, handlers) + } + socket.on('close', closeHandler) + handlers.close = closeHandler + + // Set a timeout to close idle connections after 10 minutes + const idleTimeout = setTimeout( + () => { + if (this.tcpConnections.has(connectionId)) { + logger.debug(`Closing idle TCP connection ${connectionId}`) + socket.destroy() + } + }, + 10 * 60 * 1000 + ) + + // Clear timeout on socket close + socket.once('close', () => { + clearTimeout(idleTimeout) + }) + + // Store the connection + const connection: TcpConnection = { + socket, + streamId, + connectionId, + } + this.tcpConnections.set(connectionId, connection) + + // Send StreamStart for the first connection, ConnectionStart for subsequent ones + if (connectionId === 1) { + this.sendStreamStart(streamId, connectionId) + } else { + this.sendConnectionStart(streamId, connectionId) + } + } + + /** + * Helper method to clean up socket event handlers + * @param socket The socket to clean up + * @param handlers The handlers to remove + */ + private cleanupSocketHandlers(socket: net.Socket, handlers: { [event: string]: (...args: any[]) => void }): void { + try { + if (handlers.data) { + socket.removeListener('data', handlers.data as (...args: any[]) => void) + } + if (handlers.error) { + socket.removeListener('error', handlers.error as (...args: any[]) => void) + } + if (handlers.close) { + socket.removeListener('close', handlers.close as (...args: any[]) => void) + } + } catch (error) { + logger.error(`Error cleaning up socket handlers: ${error}`) + } + } + + /** + * Connect to the WebSocket server with proper event tracking + */ + private async connectWebSocket(): Promise { + if (this.ws) { + this.closeWebSocket() + } + + // Reset for new connection + this.isDisposed = false + + return new Promise((resolve, reject) => { + try { + const url = `wss://data.tunneling.iot.${this.region}.amazonaws.com:443/tunnel?local-proxy-mode=source` + + if (!this.clientToken) { + this.clientToken = uuidv4().replace(/-/g, '') + } + + this.ws = new WebSocket.WebSocket(url, ['aws.iot.securetunneling-3.0'], { + headers: { + 'access-token': this.accessToken, + 'client-token': this.clientToken, + }, + handshakeTimeout: 30000, // 30 seconds + }) + + // Track event listeners for proper cleanup + this.eventHandlers['wsOpen'] = [] + this.eventHandlers['wsMessage'] = [] + this.eventHandlers['wsClose'] = [] + this.eventHandlers['wsError'] = [] + this.eventHandlers['wsPing'] = [] + this.eventHandlers['wsPong'] = [] + + // Open handler + const openHandler = () => { + logger.debug('WebSocket connected') + this.isConnected = true + this.reconnectAttempts = 0 + this.startPingInterval() + resolve() + } + this.ws.on('open', openHandler) + this.eventHandlers['wsOpen'].push(openHandler) + + // Message handler + const messageHandler = (data: WebSocket.RawData) => { + this.handleWebSocketMessage(data) + } + this.ws.on('message', messageHandler) + this.eventHandlers['wsMessage'].push(messageHandler) + + // Close handler + const closeHandler = (code: number, reason: Buffer) => { + logger.debug(`WebSocket closed: ${code} ${reason.toString()}`) + this.isConnected = false + this.stopPingInterval() + + // Only attempt reconnect if we haven't explicitly stopped + if (!this.isDisposed) { + void this.attemptReconnect() + } + } + this.ws.on('close', closeHandler) + this.eventHandlers['wsClose'].push(closeHandler) + + // Error handler + const errorHandler = (err: Error) => { + logger.error(`WebSocket error: ${err}`) + reject(err) + } + this.ws.on('error', errorHandler) + this.eventHandlers['wsError'].push(errorHandler) + + // Ping handler + const pingHandler = (data: Buffer) => { + // Respond to ping with pong + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.pong(data) + } + } + this.ws.on('ping', pingHandler) + this.eventHandlers['wsPing'].push(pingHandler) + + // Pong handler + const pongHandler = () => { + logger.debug('Received pong') + } + this.ws.on('pong', pongHandler) + this.eventHandlers['wsPong'].push(pongHandler) + + // Set connection timeout + const connectionTimeout = setTimeout(() => { + if (this.ws && this.ws.readyState !== WebSocket.OPEN) { + logger.error('WebSocket connection timed out') + this.closeWebSocket() + reject(new Error('WebSocket connection timed out')) + } + }, 35000) // 35 seconds (slightly longer than handshake timeout) + + // Add a handler to clear the timeout on successful connection + this.ws.once('open', () => { + clearTimeout(connectionTimeout) + }) + } catch (error) { + logger.error(`Failed to connect WebSocket: ${error}`) + this.isConnected = false + reject(error) + } + }) + } + + /** + * Close the WebSocket connection with proper cleanup + */ + private closeWebSocket(): void { + if (this.ws) { + try { + logger.debug('Closing WebSocket connection') + + // Remove all event listeners before closing + this.ws.removeAllListeners('open') + this.ws.removeAllListeners('message') + this.ws.removeAllListeners('close') + this.ws.removeAllListeners('error') + this.ws.removeAllListeners('ping') + this.ws.removeAllListeners('pong') + + // Try to close gracefully first + if (this.ws.readyState === WebSocket.OPEN) { + // Set timeout in case close hangs + const closeTimeout = setTimeout(() => { + logger.warn('WebSocket close timed out, forcing termination') + if (this.ws) { + try { + this.ws.terminate() + } catch (e) { + // Ignore errors on terminate after timeout + } + this.ws = undefined + } + }, 1000) + + // Try graceful closure first + this.ws.close(1000, 'Normal Closure') + + // Set up a handler to clear the timeout if close works normally + this.ws.once('close', () => { + clearTimeout(closeTimeout) + }) + } else { + // If not open, just terminate + this.ws.terminate() + } + } catch (error) { + logger.error(`Error closing WebSocket: ${error}`) + } finally { + this.ws = undefined + } + } + } + + /** + * Start the ping interval to keep the connection alive + */ + private startPingInterval(): void { + this.stopPingInterval() + + // Send ping every 30 seconds to keep the connection alive + this.pingInterval = setInterval(() => { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + logger.debug('Sending ping') + try { + this.ws.ping(crypto.randomBytes(16)) + } catch (error) { + logger.error(`Error sending ping: ${error}`) + } + } else { + // If websocket is no longer open, stop the interval + this.stopPingInterval() + } + }, 30000) + } + + /** + * Stop the ping interval with better error handling + */ + private stopPingInterval(): void { + try { + if (this.pingInterval) { + clearInterval(this.pingInterval) + this.pingInterval = undefined + logger.debug('Ping interval stopped') + } + } catch (error) { + logger.error(`Error stopping ping interval: ${error}`) + this.pingInterval = undefined + } + } + + /** + * Attempt to reconnect to the WebSocket server with better resource management + */ + private async attemptReconnect(): Promise { + if (this.isDisposed) { + logger.debug('LocalProxy is disposed, not attempting reconnect') + return + } + + if (!this.clientToken) { + logger.debug('stop retrying, ws closed manually') + return + } + + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + logger.error('Max reconnect attempts reached') + // Clean up resources when max attempts reached + this.stop() + return + } + + this.reconnectAttempts++ + const delay = this.reconnectInterval * Math.pow(1.5, this.reconnectAttempts - 1) + + logger.debug(`Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts})`) + + // Use a tracked timeout that we can clear if needed + const reconnectTimeoutId = setTimeout(() => { + if (!this.isDisposed) { + void this.connectWebSocket().catch((err) => { + logger.error(`Reconnect failed: ${err}`) + }) + } else { + logger.debug('Reconnect cancelled because LocalProxy was disposed') + } + }, delay) + + // Store the timeout ID so it can be cleared if stop() is called + if (!this.eventHandlers['reconnectTimeouts']) { + this.eventHandlers['reconnectTimeouts'] = [] + } + this.eventHandlers['reconnectTimeouts'].push(reconnectTimeoutId) + } + + /** + * Handle a WebSocket message + * @param data The message data + */ + private handleWebSocketMessage(data: WebSocket.RawData): void { + try { + // Handle binary data + if (Buffer.isBuffer(data)) { + let offset = 0 + + // Process all messages in the buffer + while (offset < data.length) { + // Read the 2-byte length prefix + if (offset + 2 > data.length) { + logger.error('Incomplete message length prefix') + break + } + + const messageLength = data.readUInt16BE(offset) + offset += 2 + + // Check if we have the complete message + if (offset + messageLength > data.length) { + logger.error('Incomplete message data') + break + } + + // Extract the message data + const messageData = data.slice(offset, offset + messageLength) + offset += messageLength + + // Decode and process the message + this.processMessage(messageData) + } + } else { + logger.warn('Received non-buffer WebSocket message') + } + } catch (error) { + logger.error(`Error handling WebSocket message:${error}`) + } + } + + /** + * Process a decoded message + * @param messageData The message data + */ + private processMessage(messageData: Buffer): void { + try { + if (!this.Message) { + logger.error('Protobuf Message type not loaded') + return + } + + // Decode the message + const message = this.Message.decode(messageData) + + // Process based on message type + const typedMessage = message as any + switch (typedMessage.type) { + case MessageType.DATA: + this.handleDataMessage(message) + break + + case MessageType.STREAM_RESET: + this.handleStreamReset(message) + break + + case MessageType.CONNECTION_RESET: + this.handleConnectionReset(message) + break + + case MessageType.SESSION_RESET: + this.handleSessionReset() + break + + case MessageType.SERVICE_IDS: + this.handleServiceIds(message) + break + + default: + logger.debug(`Received message of type ${typedMessage.type}`) + break + } + } catch (error) { + logger.error(`Error processing message:${error}`) + } + } + + /** + * Handle a DATA message + * @param message The message + */ + private handleDataMessage(message: any): void { + const { streamId, connectionId, payload } = message + + // Validate stream ID + if (streamId !== this.currentStreamId) { + logger.warn(`Received data for invalid stream ID: ${streamId}, current: ${this.currentStreamId}`) + return + } + + // Find the connection + const connection = this.tcpConnections.get(connectionId || 1) + if (!connection) { + logger.warn(`Received data for unknown connection ID: ${connectionId}`) + return + } + + logger.debug(`Received data for connection ${connectionId} in stream ${streamId}`) + + // Write data to the TCP socket + if (connection.socket.writable) { + connection.socket.write(Buffer.from(payload)) + } + } + + /** + * Handle a STREAM_RESET message + * @param message The message + */ + private handleStreamReset(message: any): void { + const { streamId } = message + + logger.debug(`Received STREAM_RESET for stream ${streamId}`) + + // Close all connections for this stream + for (const [connectionId, connection] of this.tcpConnections.entries()) { + if (connection.streamId === streamId) { + connection.socket.destroy() + this.tcpConnections.delete(connectionId) + } + } + } + + /** + * Handle a CONNECTION_RESET message + * @param message The message + */ + private handleConnectionReset(message: any): void { + const { streamId, connectionId } = message + + logger.debug(`Received CONNECTION_RESET for connection ${connectionId} in stream ${streamId}`) + + // Close the specific connection + const connection = this.tcpConnections.get(connectionId) + if (connection) { + connection.socket.destroy() + this.tcpConnections.delete(connectionId) + } + } + + /** + * Handle a SESSION_RESET message + */ + private handleSessionReset(): void { + logger.debug('Received SESSION_RESET') + + // Close all connections + for (const connection of this.tcpConnections.values()) { + connection.socket.destroy() + } + this.tcpConnections.clear() + + // Increment stream ID for new connections + this.currentStreamId++ + } + + /** + * Handle a SERVICE_IDS message + * @param message The message + */ + private handleServiceIds(message: any): void { + const { availableServiceIds } = message + + logger.debug(`Received SERVICE_IDS: ${availableServiceIds}`) + + // Validate service IDs + if (Array.isArray(availableServiceIds) && availableServiceIds.length > 0) { + // Use the first service ID + this.serviceId = availableServiceIds[0] + } + } + + /** + * Send a message over the WebSocket + * @param messageType The message type + * @param streamId The stream ID + * @param connectionId The connection ID + * @param payload The payload + */ + private sendMessage(messageType: MessageType, streamId: number, connectionId: number, payload?: Buffer): void { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + logger.warn('WebSocket not connected, cannot send message') + return + } + + if (!this.Message) { + logger.error('Protobuf Message type not loaded') + return + } + + try { + // Create the message + const message = { + type: messageType, + streamId, + connectionId, + serviceId: this.serviceId, + } + + // Add payload if provided + const typedMessage: any = message + if (payload) { + typedMessage.payload = payload + } + + // Verify and encode the message + const err = this.Message.verify(message) + if (err) { + throw new Error(`Invalid message: ${err}`) + } + + const encodedMessage = this.Message.encode(this.Message.create(message)).finish() + + // Create the frame with 2-byte length prefix + const frameLength = encodedMessage.length + const frame = Buffer.alloc(2 + frameLength) + + // Write the length prefix + frame.writeUInt16BE(frameLength, 0) + + // Copy the encoded message + Buffer.from(encodedMessage).copy(frame, 2) + + // Send the frame + if (this.ws?.readyState === WebSocket.OPEN) { + this.ws.send(frame) + } else { + logger.warn('WebSocket connection lost before sending message') + } + } catch (error) { + logger.error(`Error sending message: ${error}`) + } + } + + /** + * Send a STREAM_START message + * @param streamId The stream ID + * @param connectionId The connection ID + */ + private sendStreamStart(streamId: number, connectionId: number): void { + logger.debug(`Sending STREAM_START for stream ${streamId}, connection ${connectionId}`) + this.sendMessage(MessageType.STREAM_START, streamId, connectionId) + } + + /** + * Send a CONNECTION_START message + * @param streamId The stream ID + * @param connectionId The connection ID + */ + private sendConnectionStart(streamId: number, connectionId: number): void { + logger.debug(`Sending CONNECTION_START for stream ${streamId}, connection ${connectionId}`) + this.sendMessage(MessageType.CONNECTION_START, streamId, connectionId) + } + + /** + * Send a CONNECTION_RESET message + * @param streamId The stream ID + * @param connectionId The connection ID + */ + private sendConnectionReset(streamId: number, connectionId: number): void { + logger.debug(`Sending CONNECTION_RESET for stream ${streamId}, connection ${connectionId}`) + this.sendMessage(MessageType.CONNECTION_RESET, streamId, connectionId) + } + + /** + * Send data over the WebSocket + * @param streamId The stream ID + * @param connectionId The connection ID + * @param data The data to send + */ + private sendData(streamId: number, connectionId: number, data: Buffer): void { + // Split data into chunks if it exceeds the maximum payload size (63kb) + const maxChunkSize = 63 * 1024 // 63kb + + for (let offset = 0; offset < data.length; offset += maxChunkSize) { + const chunk = data.slice(offset, offset + maxChunkSize) + this.sendMessage(MessageType.DATA, streamId, connectionId, chunk) + } + } +} 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 new file mode 100644 index 00000000000..8f2ea862556 --- /dev/null +++ b/packages/core/src/lambda/remoteDebugging/utils.ts @@ -0,0 +1,42 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IoTSecureTunnelingClient } from '@aws-sdk/client-iotsecuretunneling' +import { DefaultLambdaClient } from '../../shared/clients/lambdaClient' +import { getUserAgent } from '../../shared/telemetry/util' +import globals from '../../shared/extensionGlobals' + +const customUserAgentBase = 'LAMBDA-DEBUG/1.0.0' + +export function getLambdaClientWithAgent(region: string, customUserAgent?: string): DefaultLambdaClient { + if (!customUserAgent) { + customUserAgent = getLambdaUserAgent() + } + return new DefaultLambdaClient(region, customUserAgent) +} + +// Example user agent: +// LAMBDA-DEBUG/1.0.0 AWS-Toolkit-For-VSCode/testPluginVersion Visual-Studio-Code/1.102.2 ClientId/11111111-1111-1111-1111-111111111111 +export function getLambdaDebugUserAgent(): string { + return `${customUserAgentBase} ${getLambdaUserAgent()}` +} + +// Example user agent: +// AWS-Toolkit-For-VSCode/testPluginVersion Visual-Studio-Code/1.102.2 ClientId/11111111-1111-1111-1111-111111111111 +export function getLambdaUserAgent(): string { + return `${getUserAgent({ includePlatform: true, includeClientId: true })}` +} + +export function getIoTSTClientWithAgent(region: string): IoTSecureTunnelingClient { + const customUserAgent = `${customUserAgentBase} ${getUserAgent({ includePlatform: true, includeClientId: true })}` + return globals.sdkClientBuilderV3.createAwsService({ + serviceClient: IoTSecureTunnelingClient, + clientOptions: { + userAgent: [[customUserAgent]], + region, + }, + userAgent: false, + }) +} diff --git a/packages/core/src/lambda/uriHandlers.ts b/packages/core/src/lambda/uriHandlers.ts new file mode 100644 index 00000000000..8ae1d7b8c35 --- /dev/null +++ b/packages/core/src/lambda/uriHandlers.ts @@ -0,0 +1,58 @@ +/*! + * 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 { SearchParams } from '../shared/vscode/uriHandler' +import { openLambdaFolderForEdit } from './commands/editLambda' +import { showConfirmationMessage } from '../shared/utilities/messages' +import globals from '../shared/extensionGlobals' +import { telemetry } from '../shared/telemetry/telemetry' +import { ToolkitError } from '../shared/errors' + +const localize = nls.loadMessageBundle() + +export function registerLambdaUriHandler() { + async function openFunctionHandler(params: ReturnType) { + await telemetry.lambda_uriHandler.run(async () => { + try { + if (params.isCfn === 'true') { + const response = await showConfirmationMessage({ + prompt: localize( + 'AWS.lambda.open.confirmInStack', + 'The function you are attempting to open is in a CloudFormation stack. Editing the function code could lead to stack drift.' + ), + confirm: localize('AWS.lambda.open.confirmStack', 'Confirm'), + cancel: localize('AWS.lambda.open.cancelStack', 'Cancel'), + }) + if (!response) { + return + } + } + await openLambdaFolderForEdit(params.functionName, params.region) + } catch (e) { + throw new ToolkitError(`Unable to get function ${params.functionName} in region ${params.region}: ${e}`) + } + }) + } + + return vscode.Disposable.from( + globals.uriHandler.onPath('/lambda/load-function', openFunctionHandler, parseOpenParams) + ) +} + +// Sample url: +// vscode://AmazonWebServices.aws-toolkit-vscode/lambda/load-function?functionName=fnf-func-1®ion=us-east-1&isCfn=true +export function parseOpenParams(query: SearchParams) { + return { + functionName: query.getOrThrow( + 'functionName', + localize('AWS.lambda.open.missingName', 'A function name must be provided') + ), + region: query.getOrThrow('region', localize('AWS.lambda.open.missingRegion', 'A region must be provided')), + isCfn: query.get('isCfn'), + } +} diff --git a/packages/core/src/lambda/utils.ts b/packages/core/src/lambda/utils.ts index 79054e02cac..9b8ffcb884a 100644 --- a/packages/core/src/lambda/utils.ts +++ b/packages/core/src/lambda/utils.ts @@ -6,17 +6,21 @@ import * as nls from 'vscode-nls' 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 { LambdaClient } from '../shared/clients/lambdaClient' +import { DefaultLambdaClient, LambdaClient } from '../shared/clients/lambdaClient' import { getFamily, getNodeMajorVersion, RuntimeFamily } from './models/samLambdaRuntime' import { getLogger } from '../shared/logger/logger' import { HttpResourceFetcher } from '../shared/resourcefetcher/httpResourceFetcher' import { FileResourceFetcher } from '../shared/resourcefetcher/fileResourceFetcher' import { sampleRequestManifestPath } from './constants' import globals from '../shared/extensionGlobals' +import { tempDirPath } from '../shared/filesystemUtilities' +import { LambdaFunction } from './commands/uploadLambda' +import { fs } from '../shared/fs/fs' export async function* listCloudFormationStacks(client: CloudFormationClient): AsyncIterableIterator { // TODO: this 'loading' message needs to go under each regional entry @@ -32,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...') ) @@ -46,12 +50,29 @@ export async function* listLambdaFunctions(client: LambdaClient): AsyncIterableI } } +export async function* listLayerVersions( + client: LambdaClient, + name: string +): AsyncIterableIterator { + const status = vscode.window.setStatusBarMessage( + localize('AWS.message.statusBar.loading.lambda', 'Loading Lambda Layer Versions...') + ) + + try { + yield* client.listLayerVersions(name) + } finally { + if (status) { + status.dispose() + } + } +} + /** * Returns filename and function name corresponding to a Lambda.FunctionConfiguration * 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 } { @@ -70,6 +91,9 @@ export function getLambdaDetails(configuration: Lambda.FunctionConfiguration): { } break } + case RuntimeFamily.Ruby: + runtimeExtension = 'rb' + break default: throw new Error(`Toolkit does not currently support imports for runtime: ${configuration.Runtime}`) } @@ -124,3 +148,67 @@ async function getSampleRequestManifest(): Promise { } return httpResp.text() } + +function getInfoLocation(lambda: LambdaFunction): string { + return path.join(getTempRegionLocation(lambda.region), `.${lambda.name}`) +} + +export async function getCodeShaLive(lambda: LambdaFunction): Promise { + const lambdaClient = new DefaultLambdaClient(lambda.region) + const func = await lambdaClient.getFunction(lambda.name) + return func.Configuration?.CodeSha256 +} + +export async function compareCodeSha(lambda: LambdaFunction): Promise { + const local = await getFunctionInfo(lambda, 'sha') + const remote = await getCodeShaLive(lambda) + getLogger().info(`local: ${local}, remote: ${remote}`) + return local === remote +} + +export interface FunctionInfo { + lastDeployed?: number + undeployed?: boolean + sha?: string + handlerFile?: string +} + +export async function getFunctionInfo(lambda: LambdaFunction, field?: K) { + try { + const data = JSON.parse(await fs.readFileText(getInfoLocation(lambda))) + getLogger().debug('Data returned from getFunctionInfo for %s: %O', lambda.name, data) + return field ? data[field] : data + } catch { + return field ? undefined : {} + } +} + +export async function setFunctionInfo(lambda: LambdaFunction, info: Partial) { + try { + const existing = await getFunctionInfo(lambda) + const updated: FunctionInfo = { + lastDeployed: info.lastDeployed ?? existing.lastDeployed, + undeployed: info.undeployed ?? true, + sha: info.sha ?? (await getCodeShaLive(lambda)), + handlerFile: info.handlerFile ?? existing.handlerFile, + } + await fs.writeFile(getInfoLocation(lambda), JSON.stringify(updated)) + } catch (err) { + getLogger().warn(`codesha: unable to save information at key "${lambda.name}: %s"`, err) + } +} + +export const lambdaTempPath = path.join(tempDirPath, 'lambda') + +export function getTempRegionLocation(region: string) { + return path.join(lambdaTempPath, region) +} + +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/remoteInvoke/invokeLambda.ts b/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts index aafde327d7f..c56f43ae199 100644 --- a/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts +++ b/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts @@ -3,11 +3,10 @@ * 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' -import { DefaultLambdaClient, LambdaClient } from '../../../shared/clients/lambdaClient' +import { LambdaClient } from '../../../shared/clients/lambdaClient' import * as picker from '../../../shared/ui/picker' import { ExtContext } from '../../../shared/extensions' @@ -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 } from '../../../shared/telemetry/telemetry' +import { telemetry, Runtime as TelemetryRuntime } from '../../../shared/telemetry/telemetry' +import { Runtime } from '@aws-sdk/client-lambda' import { runSamCliRemoteTestEvents, SamCliRemoteTestEventsParameters, @@ -29,6 +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 { 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, getLambdaDebugUserAgent } from '../../remoteDebugging/utils' +import { isLocalStackConnection } from '../../../auth/utils' +import { getRemoteDebugLayer } from '../../remoteDebugging/remoteLambdaDebugger' const localize = nls.loadMessageBundle() @@ -48,19 +58,70 @@ export interface InitialData { Source?: string StackName?: string LogicalId?: 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 | undefined + localRootPath: string + remoteRootPath: string + shouldPublishVersion: boolean + lambdaTimeout: number + otherDebugParams: string +} + +// Debug state sub-interface +export interface DebugState { + isDebugging: boolean + debugTimer: number | undefined + debugTimeRemaining: number + showDebugTimer: boolean + handlerFileAvailable: boolean + remoteDebuggingEnabled: boolean +} + +// Runtime-specific debug settings sub-interface +export interface RuntimeDebugSettings { + // Node.js specific + sourceMapEnabled: boolean + skipFiles: string + outFiles: string | undefined + // Python specific + justMyCode: boolean + // Java specific + projectName: string +} + +// UI state sub-interface +export interface UIState { + isCollapsed: boolean + extraRegionInfo: string +} + +// Payload/Event handling sub-interface +export interface PayloadData { + sampleText: string } export interface RemoteInvokeData { initialData: InitialData - selectedSampleRequest: string - sampleText: string - selectedFile: string - selectedFilePath: string - selectedTestEvent: string - payload: string - showNameInput: boolean - newTestEventName: string - selectedFunction: string + debugConfig: DebugConfiguration + debugState: DebugState + runtimeSettings: RuntimeDebugSettings + uiState: UIState + payloadData: PayloadData +} + +// Event types for communicating state between backend and frontend +export type StateChangeEvent = { + isDebugging?: boolean } interface SampleQuickPickItem extends vscode.QuickPickItem { filename: string @@ -70,50 +131,204 @@ export class RemoteInvokeWebview extends VueWebview { public static readonly sourcePath: string = 'src/lambda/vue/remoteInvoke/index.js' public readonly id = 'remoteInvoke' + // Event emitter for state changes that need to be synchronized with the frontend + public readonly onStateChange = new vscode.EventEmitter() + + // Backend timer that will continue running even when the webview loses focus + private debugTimerHandle: NodeJS.Timeout | undefined + private debugTimeRemaining: number = 0 + private isInvoking: boolean = false + private debugging: boolean = false + private watcherDisposable: vscode.Disposable | undefined + private fileWatcherDisposable: vscode.Disposable | undefined + private handlerFileAvailable: boolean = false + private isStartingDebug: boolean = false + private handlerFile: string | undefined public constructor( private readonly channel: vscode.OutputChannel, private readonly client: LambdaClient, + private readonly clientDebug: LambdaClient, private readonly data: InitialData ) { super(RemoteInvokeWebview.sourcePath) } public init() { + this.watcherDisposable = vscode.debug.onDidTerminateDebugSession(async (session: vscode.DebugSession) => { + this.resetServerState() + }) return this.data } - public async invokeLambda(input: string, source?: string): Promise { - let result: Result = 'Succeeded' + public resetServerState() { + this.stopDebugTimer() + this.debugging = false + this.isInvoking = false + this.isStartingDebug = false + this.onStateChange.fire({ + isDebugging: false, + }) + } - this.channel.show() - this.channel.appendLine('Loading response...') + public async disposeServer() { + this.watcherDisposable?.dispose() + this.fileWatcherDisposable?.dispose() + if (this.debugging && RemoteDebugController.instance.isDebugging) { + await this.stopDebugging() + } + this.dispose() + } - try { - const funcResponse = await this.client.invoke(this.data.FunctionArn, input) - 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 }) + private setupFileWatcher() { + // Dispose existing watcher if any + this.fileWatcherDisposable?.dispose() + + if (!this.data.LocalRootPath) { + return + } + + // Create a file system watcher for the local root path + const pattern = new vscode.RelativePattern(this.data.LocalRootPath, '**/*') + const watcher = vscode.workspace.createFileSystemWatcher(pattern) + + // Set up event handlers for file changes + const handleFileChange = async () => { + const result = await showMessage( + 'info', + localize( + 'AWS.lambda.remoteInvoke.codeChangesDetected', + 'Code changes detected in the local directory. Would you like to update the Lambda function {0}@{1}?', + this.data.FunctionName, + this.data.FunctionRegion + ), + ['Yes', 'No'] + ) + + if (result === 'Yes') { + try { + if (this.data.LambdaFunctionNode && this.data.LocalRootPath) { + const lambdaFunction = { + name: this.data.FunctionName, + region: this.data.FunctionRegion, + configuration: this.data.LambdaFunctionNode.configuration, + } + await runUploadDirectory(lambdaFunction, 'zip', vscode.Uri.file(this.data.LocalRootPath)) + } + } catch (error) { + throw ToolkitError.chain( + error, + localize('AWS.lambda.remoteInvoke.updateFailed', 'Failed to update Lambda function') + ) + } + } + } + + // Listen for file changes, creations, and deletions + watcher.onDidChange(handleFileChange) + watcher.onDidCreate(handleFileChange) + watcher.onDidDelete(handleFileChange) + + // Store the disposable so we can clean it up later + this.fileWatcherDisposable = watcher + } + + // Method to start the backend timer + public startDebugTimer() { + // Clear any existing timer + this.stopDebugTimer() + + this.debugTimeRemaining = 60 + + // Create a new timer that ticks every second + this.debugTimerHandle = setInterval(async () => { + this.debugTimeRemaining-- + + // When timer reaches zero, stop debugging + if (this.debugTimeRemaining <= 0) { + await this.handleTimerExpired() + } + }, 1000) + } + + // Method to stop the timer + public stopDebugTimer() { + if (this.debugTimerHandle) { + clearInterval(this.debugTimerHandle) + this.debugTimerHandle = undefined + this.debugTimeRemaining = 0 + } + } + + // Handler for timer expiration + private async handleTimerExpired() { + await this.stopDebugging() + } + + public async invokeLambda(input: string, source?: string, remoteDebugEnabled: boolean = false): Promise { + let qualifier: string | undefined = undefined + // if debugging, focus on the first editor + if (remoteDebugEnabled && RemoteDebugController.instance.isDebugging) { + await vscode.commands.executeCommand('workbench.action.focusFirstEditorGroup') + qualifier = RemoteDebugController.instance.qualifier + } + + this.isInvoking = true + + // If debugging is active, reset the timer during invoke + if (RemoteDebugController.instance.isDebugging) { + this.stopDebugTimer() } + + this.channel.show() + this.channel.appendLine('Loading response...') + await telemetry.lambda_invokeRemote.run(async (span) => { + try { + const funcResponse = remoteDebugEnabled + ? await this.clientDebug.invoke(this.data.FunctionArn, input, qualifier) + : await this.client.invoke(this.data.FunctionArn, input, qualifier) + const logs = funcResponse.LogResult ? decodeBase64(funcResponse.LogResult) : '' + const decodedPayload = funcResponse.Payload ? new TextDecoder().decode(funcResponse.Payload) : '' + const payload = decodedPayload || 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('') + } 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() + } + this.channel.show() + } + }) } public async promptFile() { const fileLocations = await vscode.window.showOpenDialog({ - openLabel: 'Open', + openLabel: localize('AWS.lambda.remoteInvoke.open', 'Open'), }) if (!fileLocations || fileLocations.length === 0) { @@ -129,8 +344,72 @@ export class RemoteInvokeWebview extends VueWebview { } } catch (e) { getLogger().error('readFileSync: Failed to read file at path %s %O', fileLocations[0].fsPath, e) - throw ToolkitError.chain(e, 'Failed to read selected file') + throw ToolkitError.chain( + e, + localize('AWS.lambda.remoteInvoke.failedToReadFile', 'Failed to read selected file') + ) + } + } + + public async promptFolder(): Promise { + const fileLocations = await vscode.window.showOpenDialog({ + openLabel: localize('AWS.lambda.remoteInvoke.open', 'Open'), + canSelectFolders: true, + canSelectFiles: false, + canSelectMany: false, + }) + + if (!fileLocations || fileLocations.length === 0) { + return undefined } + this.data.LocalRootPath = fileLocations[0].fsPath + // try to find the handler file in this folder, open it if not opened already + if (!(await this.tryOpenHandlerFile())) { + const warning = localize( + 'AWS.lambda.remoteInvoke.handlerFileNotFound', + 'Handler {0} not found in selected location. Please select the folder that contains the copy of your handler file', + this.data.LambdaFunctionNode?.configuration.Handler + ) + getLogger().warn(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 + } + // init path or node not available + if (!this.data.LocalRootPath || !this.data.LambdaFunctionNode) { + return false + } + + 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 && !isHotReloadingFunction(this.data.LambdaFunctionNode?.configuration.CodeSha256)) { + this.setupFileWatcher() + } + await openLambdaFile(handlerFile.fsPath) + this.handlerFile = handlerFile.fsPath + return true } public async loadFile(fileLocations: string) { @@ -152,7 +431,10 @@ export class RemoteInvokeWebview extends VueWebview { } } catch (e) { getLogger().error('readFileSync: Failed to read file at path %s %O', fileLocation.fsPath, e) - throw ToolkitError.chain(e, 'Failed to read selected file') + throw ToolkitError.chain( + e, + localize('AWS.lambda.remoteInvoke.failedToReadFile', 'Failed to read selected file') + ) } } @@ -161,22 +443,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) } @@ -225,12 +651,242 @@ export class RemoteInvokeWebview extends VueWebview { return sample } catch (err) { getLogger().error('Error getting manifest data..: %O', err as Error) - throw ToolkitError.chain(err, 'getting manifest data') + throw ToolkitError.chain( + err, + localize('AWS.lambda.remoteInvoke.gettingManifestData', 'getting manifest data') + ) } } -} -const Panel = VueWebview.compilePanel(RemoteInvokeWebview) + // 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 TelemetryRuntime | undefined, source: 'RemoteDebug' }) + try { + if (this.data.LambdaFunctionNode) { + const output = await runDownloadLambda(this.data.LambdaFunctionNode, true) + if (output instanceof vscode.Uri) { + this.data.LocalRootPath = output.fsPath + this.handlerFileAvailable = true + this.setupFileWatcher() + + return output.fsPath + } + } else { + getLogger().error( + localize( + 'AWS.lambda.remoteInvoke.lambdaFunctionNodeUndefined', + 'LambdaFunctionNode is undefined' + ) + ) + } + return undefined + } catch (error) { + throw ToolkitError.chain( + error, + localize('AWS.lambda.remoteInvoke.failedToDownloadCode', 'Failed to download remote code') + ) + } + }) + } + + // this serves as a lock for invoke + public checkReadyToInvoke(): boolean { + if (this.isInvoking) { + void showMessage( + 'warn', + localize( + 'AWS.lambda.remoteInvoke.invokeInProgress', + 'A remote invoke is already in progress, please wait for previous invoke, or remove debug setup' + ) + ) + return false + } + if (this.isStartingDebug) { + 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 + } + + // this check is run when user click remote invoke with remote debugging checked + public async checkReadyToDebug(config: DebugConfig): Promise { + if (!this.data.LambdaFunctionNode) { + return false + } + + if (!this.handlerFileAvailable) { + const result = await showConfirmationMessage({ + prompt: localize( + 'AWS.lambda.remoteInvoke.handlerFileNotLocated', + 'The handler file cannot be located in the specified Local Root Path. As a result, remote debugging will not pause at breakpoints.' + ), + confirm: 'Continue Anyway', + cancel: 'Cancel', + type: 'warning', + }) + if (!result) { + return false + } + } + // check if snapstart is on and we are publishing a version + if ( + config.shouldPublishVersion && + this.data.LambdaFunctionNode.configuration.SnapStart?.ApplyOn === 'PublishedVersions' + ) { + const result = await showConfirmationMessage({ + prompt: localize( + 'AWS.lambda.remoteInvoke.snapstartWarning', + "This function has Snapstart enabled. If you use Remote Debug with the 'publish version' setting, you'll experience notable delays. For faster debugging, consider disabling the 'publish version' option." + ), + confirm: 'Continue Anyway', + cancel: 'Cancel', + type: 'warning', + }) + if (!result) { + // didn't confirm + getLogger().warn( + localize('AWS.lambda.remoteInvoke.userCanceledSnapstart', 'User canceled Snapstart confirm') + ) + return false + } + } + + // ready to debug + return true + } + + public async startDebugging(config: DebugConfig): Promise { + if (!this.data.LambdaFunctionNode) { + return false + } + if (!(await this.checkReadyToDebug(config))) { + return false + } + this.isStartingDebug = true + try { + 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( + e, + localize('AWS.lambda.remoteInvoke.failedToStartDebugging', 'Failed to start debugging') + ) + } finally { + this.isStartingDebug = false + } + + this.startDebugTimer() + this.debugging = this.isLDKDebugging() + return this.debugging + } + + public async stopDebugging(): Promise { + if (this.isLDKDebugging()) { + this.resetServerState() + await RemoteDebugController.instance.stopDebugging() + } + this.debugging = this.isLDKDebugging() + return this.debugging + } + + public isLDKDebugging(): boolean { + return RemoteDebugController.instance.isDebugging + } + + public isWebViewDebugging(): boolean { + return this.debugging + } + + public getIsInvoking(): boolean { + return this.isInvoking + } + + public getDebugTimeRemaining(): number { + return this.debugTimeRemaining + } + + public getLocalPath(): string { + return this.data.LocalRootPath ?? '' + } + + public getHandlerAvailable(): boolean { + return this.handlerFileAvailable + } + + // 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: this.data.isLambdaRemote ? 'webview' : 'webviewLocalStack', + }) + if (!this.debugging && RemoteDebugController.instance.isDebugging) { + // another debug session in progress + const result = await showConfirmationMessage({ + prompt: localize( + 'AWS.lambda.remoteInvoke.debugSessionActive', + 'A remote debug session is already active. Stop that for this new session?' + ), + confirm: 'Stop Previous Session', + cancel: 'Cancel', + type: 'warning', + }) + + if (result) { + // Stop the previous session + if (await this.stopDebugging()) { + getLogger().error( + localize( + 'AWS.lambda.remoteInvoke.failedToStopPreviousSession', + 'Failed to stop previous debug session.' + ) + ) + return false + } + } else { + // user canceled, Do nothing + return false + } + } + + const result = await RemoteDebugController.instance.installDebugExtension(this.data.Runtime) + if (!result && result === false) { + // install failed + return false + } + + await revertExistingConfig() + + // Check if the function ARN is in the cache and try to open handler file + const cachedPath = getCachedLocalPath(this.data.FunctionArn) + // only check cache if not comming from appbuilder + if (cachedPath && !this.data.LambdaFunctionNode?.localDir) { + getLogger().debug( + `lambda: found cached local path for function ARN: ${this.data.FunctionArn} -> ${cachedPath}` + ) + await this.tryOpenHandlerFile(cachedPath, true) + } + + // this is comming from appbuilder + if (this.data.LambdaFunctionNode?.localDir) { + await this.tryOpenHandlerFile(undefined, false) + } + + return true + }) + } +} export async function invokeRemoteLambda( context: ExtContext, @@ -247,19 +903,47 @@ export async function invokeRemoteLambda( } ) { const inputs = await getSampleLambdaPayloads() - const resource: any = params.functionNode + const resource: LambdaFunctionNode = params.functionNode const source: string = params.source || 'AwsExplorerRemoteInvoke' - const client = new DefaultLambdaClient(resource.regionCode) - const wv = new Panel(context.extensionContext, context.outputChannel, client, { + const client = getLambdaClientWithAgent(resource.regionCode) + const clientDebug = getLambdaClientWithAgent(resource.regionCode, getLambdaDebugUserAgent()) + + const Panel = VueWebview.compilePanel(RemoteInvokeWebview) + + // Initialize support and debugging capabilities + const runtime = resource.configuration.Runtime + const region = resource.regionCode + 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, clientDebug, { FunctionName: resource.configuration.FunctionName ?? '', FunctionArn: resource.configuration.FunctionArn ?? '', FunctionRegion: resource.regionCode, InputSamples: inputs, TestEvents: [], Source: source, + Runtime: runtime, + LocalRootPath: params.functionNode.localDir, + LambdaFunctionNode: params.functionNode, + 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') - await wv.show({ + const activePanel = await wv.show({ title: localize('AWS.invokeLambda.title', 'Invoke Lambda {0}', resource.configuration.FunctionName), + viewColumn: vscode.ViewColumn.Beside, + }) + + activePanel.onDidDispose(async () => { + await wv.server.disposeServer() }) } diff --git a/packages/core/src/lambda/vue/remoteInvoke/remoteInvoke.css b/packages/core/src/lambda/vue/remoteInvoke/remoteInvoke.css index 99f124e6b0c..c96291b26ae 100644 --- a/packages/core/src/lambda/vue/remoteInvoke/remoteInvoke.css +++ b/packages/core/src/lambda/vue/remoteInvoke/remoteInvoke.css @@ -1,6 +1,9 @@ +/* Container and Layout */ .Icontainer { margin-inline: auto; - margin-top: 5rem; + margin-top: 2rem; + width: 100%; + min-width: 650px; } h1 { @@ -8,56 +11,106 @@ h1 { margin-bottom: 20px; } +/* Remove fixed width for divs to allow responsive behavior */ div { - width: 521px; + width: 100%; } -.form-row { - display: grid; - grid-template-columns: 150px 1fr; +/* VSCode Settings Style Layout */ +.vscode-setting-item { margin-bottom: 10px; + padding: 5px 0; } -.form-row-select { - width: 387px; - height: 28px; - border: 1px; - border-radius: 5px; - gap: 4px; - padding: 2px 8px; -} - -.dynamic-span { - white-space: nowrap; - text-overflow: initial; - overflow: scroll; - width: 381px; - height: 28px; - font-weight: 500; - font-size: 13px; - line-height: 15.51px; + +.setting-header { + display: flex; + align-items: center; + margin-bottom: 8px; +} + +.setting-title { + font-weight: 600; + font-size: 14px; + margin: 0; +} + +.setting-body { + display: flex; + align-items: flex-start; + gap: 8px; +} + +.setting-description { + flex: 1; +} + +.setting-description info-wrap, +.setting-description info { + display: block; + margin-bottom: 4px; +} + +.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; } -.form-row-event-select { - width: 244px; - height: 28px; - margin-bottom: 15px; - margin-left: 8px; +.setting-input { + flex-grow: 1; + margin-right: 2px; } -.payload-options { +/* 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; +} + +/* 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; } +info { + text-wrap-mode: nowrap; +} + +/* Form Elements */ span, -select, -.payload-options { +select { display: block; } @@ -65,68 +118,121 @@ textarea { color: var(--vscode-settings-textInputForeground); background: var(--vscode-settings-textInputBackground); border: 1px solid var(--vscode-settings-textInputBorder); + width: 100%; + box-sizing: border-box; + 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 { + +.button-theme-inline { + padding: 4px 6px; 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) { + +.button-theme-inline:hover:not(:disabled) { background: var(--vscode-button-secondaryHoverBackground); cursor: pointer; } -.payload-options-buttons { +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Payload Section Styles */ +.payload-button-group { display: flex; - align-items: center; - margin-top: 10px; + gap: 5px; margin-bottom: 10px; } -.radio-selector { - width: 15px; - height: 15px; - border-radius: 50%; +.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; } -.label-selector { - padding-left: 7px; +.collapsible-header, +.collapsible-content { + max-width: 96%; +} + +.collapsible-header { + padding: 8px 12px; + background-color: var(--vscode-sideBarSectionHeader-background); + cursor: pointer; 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; +.collapsible-content { + padding: 10px; + border-top: 1px solid var(--vscode-widget-border); } -.formfield { - display: flex; - align-items: center; - margin-bottom: 0.5rem; +/* Validation and Error Styles */ +.input-error { + border: 1px solid var(--vscode-inputValidation-errorBorder) !important; + background-color: var(--vscode-inputValidation-errorBackground) !important; +} + +.error-message { + color: var(--vscode-inputValidation-errorForeground); + font-size: 12px; + margin-top: 4px; + font-weight: 400; + line-height: 1.2; +} + +/* Checkbox and Status Styles */ +.remote-debug-checkbox { + width: 18px !important; + height: 18px !important; + accent-color: var(--vscode-checkbox-foreground); + border: 2px solid var(--vscode-checkbox-border) !important; + border-radius: 3px !important; + background-color: var(--vscode-checkbox-background) !important; + cursor: pointer; +} + +.remote-debug-checkbox:checked { + background-color: var(--vscode-checkbox-selectBackground) !important; + border-color: var(--vscode-checkbox-selectBorder) !important; +} + +.remote-debug-checkbox:disabled { + opacity: 0.6; + cursor: not-allowed; + border-color: var(--vscode-checkbox-border) !important; + background-color: var(--vscode-input-background) !important; +} + +.remote-debug-checkbox:focus { + outline: 2px solid var(--vscode-focusBorder); + outline-offset: 2px; } diff --git a/packages/core/src/lambda/vue/remoteInvoke/remoteInvoke.vue b/packages/core/src/lambda/vue/remoteInvoke/remoteInvoke.vue index 8309fce6990..62a3de7ce09 100644 --- a/packages/core/src/lambda/vue/remoteInvoke/remoteInvoke.vue +++ b/packages/core/src/lambda/vue/remoteInvoke/remoteInvoke.vue @@ -6,124 +6,333 @@ diff --git a/packages/core/src/lambda/vue/remoteInvoke/remoteInvokeFrontend.ts b/packages/core/src/lambda/vue/remoteInvoke/remoteInvokeFrontend.ts index 301b47603e9..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 = { @@ -21,102 +21,338 @@ const defaultInitialData = { TestEvents: [], FunctionStack: '', Source: '', + LambdaFunctionNode: undefined, + supportCodeDownload: true, + runtimeSupportsRemoteDebug: true, + remoteDebugLayer: '', + isLambdaRemote: true, } export default defineComponent({ - async created() { - this.initialData = (await client.init()) ?? this.initialData - if (this.initialData.FunctionArn && this.initialData.FunctionRegion) { - this.initialData.TestEvents = await client.listRemoteTestEvents( - this.initialData.FunctionArn, - this.initialData.FunctionRegion - ) - } - }, - data(): RemoteInvokeData { return { initialData: { ...defaultInitialData }, - selectedSampleRequest: '', - sampleText: '{}', - selectedFile: '', - selectedFilePath: '', - payload: 'sampleEvents', - selectedTestEvent: '', - showNameInput: false, - newTestEventName: '', - selectedFunction: 'selectedFunction', + debugConfig: { + debugPort: undefined, + localRootPath: '', + remoteRootPath: '/var/task', + shouldPublishVersion: true, + lambdaTimeout: 900, + otherDebugParams: '', + }, + debugState: { + isDebugging: false, + debugTimer: undefined, + debugTimeRemaining: 60, + showDebugTimer: false, + handlerFileAvailable: false, + remoteDebuggingEnabled: false, + }, + runtimeSettings: { + sourceMapEnabled: true, + skipFiles: '/var/runtime/node_modules/**/*.js,/**', + justMyCode: true, + projectName: '', + outFiles: undefined, + }, + uiState: { + isCollapsed: true, + extraRegionInfo: '', + }, + payloadData: { + sampleText: '{}', + }, } }, + + async created() { + // Initialize data from backend + this.initialData = (await client.init()) ?? this.initialData + this.debugConfig.localRootPath = this.initialData.LocalRootPath ?? '' + + // Register for state change events from the backend + void client.onStateChange(async () => { + await this.syncStateFromWorkspace() + }) + + // Check for existing session state and load it + await this.syncStateFromWorkspace() + }, + + computed: { + // Auto-adjust textarea rows based on content + textareaRows(): number { + if (!this.payloadData.sampleText) { + return 5 // Default minimum rows + } + + // Count line breaks to determine basic row count + const lineCount = this.payloadData.sampleText.split('\n').length + let additionalLine = 0 + for (const line of this.payloadData.sampleText.split('\n')) { + if (line.length > 60) { + additionalLine += Math.floor(line.length / 60) + } + } + + // Use the larger of line count or estimated lines, with min 5 and max 20 + const calculatedRows = lineCount + additionalLine + return Math.max(5, Math.min(50, calculatedRows)) + }, + + // Validation computed properties + debugPortError(): string { + if (this.debugConfig.debugPort !== null && this.debugConfig.debugPort !== undefined) { + const port = Number(this.debugConfig.debugPort) + if (isNaN(port) || port < 1 || port > 65535) { + return 'Debug port must be between 1 and 65535' + } + } + return '' + }, + + otherDebugParamsError(): string { + if (this.debugConfig.otherDebugParams && this.debugConfig.otherDebugParams.trim() !== '') { + try { + JSON.parse(this.debugConfig.otherDebugParams) + } catch (error) { + return 'Other Debug Params must be a valid JSON object' + } + } + return '' + }, + + lambdaTimeoutError(): string { + if (this.debugConfig.lambdaTimeout !== undefined) { + const timeout = Number(this.debugConfig.lambdaTimeout) + if (isNaN(timeout) || timeout < 1 || timeout > 900) { + return 'Timeout override must be between 1 and 900 seconds' + } + } + return '' + }, + + // user can override the default provided layer and bring their own layer + // this is useful to support function with code signing config + lambdaLayerError(): string { + if (this.initialData.remoteDebugLayer && this.initialData.remoteDebugLayer.trim() !== '') { + const layerArn = this.initialData.remoteDebugLayer.trim() + + // Validate Lambda layer ARN format + // Expected format: arn:aws:lambda:region:account-id:layer:layer-name:version + const layerArnRegex = /^arn:aws:lambda:[a-z0-9-]+:\d{12}:layer:[a-zA-Z0-9-_]+:\d+$/ + + if (!layerArnRegex.test(layerArn)) { + return 'Layer ARN must be in the format: arn:aws:lambda:::layer::' + } + + // Extract region from ARN to validate it matches the function region + const arnParts = layerArn.split(':') + if (arnParts.length >= 4) { + const layerRegion = arnParts[3] + if (this.initialData.FunctionRegion && layerRegion !== this.initialData.FunctionRegion) { + return `Layer region (${layerRegion}) must match function region (${this.initialData.FunctionRegion})` + } + } + } + return '' + }, + }, + methods: { - async newSelection() { - const eventData = { - name: this.selectedTestEvent, - region: this.initialData.FunctionRegion, - arn: this.initialData.FunctionArn, + // Runtime detection computed properties based on the runtime string + hasRuntimePrefix(prefix: string): boolean { + const runtime = this.initialData.Runtime || '' + return runtime.startsWith(prefix) + }, + // 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() + this.debugState.handlerFileAvailable = await client.getHandlerAvailable() + // Get current session state + + if (this.debugState.isDebugging) { + // Update invoke button state based on session + const isInvoking = await client.getIsInvoking() + + // If debugging is active and we're not showing the timer, + // calculate and show remaining time + this.clearDebugTimer() + if (this.debugState.isDebugging && !isInvoking) { + await this.startDebugTimer() + } + } else { + this.clearDebugTimer() + // no debug session + } + } catch (error) { + console.error('Failed to sync state from workspace:', error) } - const resp = await client.getRemoteTestEvents(eventData) - this.sampleText = JSON.stringify(JSON.parse(resp), undefined, 4) }, async saveEvent() { - const eventData = { - name: this.newTestEventName, - event: this.sampleText, - region: this.initialData.FunctionRegion, - arn: this.initialData.FunctionArn, - } - await client.createRemoteTestEvents(eventData) - this.showNameInput = false - this.newTestEventName = '' - this.selectedTestEvent = eventData.name - this.initialData.TestEvents = await client.listRemoteTestEvents( - this.initialData.FunctionArn, - this.initialData.FunctionRegion - ) + 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 + ) + } }, async promptForFileLocation() { const resp = await client.promptFile() if (resp) { - this.selectedFile = resp.selectedFile - this.selectedFilePath = resp.selectedFilePath + // Populate the textarea with file content + this.payloadData.sampleText = resp.sample } }, - onFileChange(event: Event) { - const input = event.target as HTMLInputElement - if (input.files && input.files.length > 0) { - const file = input.files[0] - this.selectedFile = file.name - - // Use Blob.text() to read the file as text - file.text() - .then((text) => { - this.sampleText = text - }) - .catch((error) => { - console.error('Error reading file:', error) - }) + async promptForFolderLocation() { + const resp = await client.promptFolder() + if (resp) { + this.debugConfig.localRootPath = resp + this.debugState.handlerFileAvailable = await client.getHandlerAvailable() } }, - showNameField() { - if (this.initialData.FunctionRegion || this.initialData.FunctionRegion) { - this.showNameInput = true + async debugPreCheck() { + if (!this.debugState.remoteDebuggingEnabled) { + // don't check if unchecking + this.debugState.remoteDebuggingEnabled = false + if (this.debugState.isDebugging) { + await client.stopDebugging() + } + } else { + // check when user is checking box + this.debugState.remoteDebuggingEnabled = await client.debugPreCheck() + this.debugConfig.localRootPath = await client.getLocalPath() + this.debugState.handlerFileAvailable = await client.getHandlerAvailable() } }, async sendInput() { - let event = '' - - if (this.payload === 'sampleEvents' || this.payload === 'savedEvents') { - event = this.sampleText - } else if (this.payload === 'localFile') { - if (this.selectedFile && this.selectedFilePath) { - const resp = await client.loadFile(this.selectedFilePath) - if (resp) { - event = resp.sample + // Tell the backend to set the button state. This state is maintained even if webview loses focus + if (this.debugState.remoteDebuggingEnabled) { + // check few outof bound issue + if ( + this.debugConfig.lambdaTimeout && + (this.debugConfig.lambdaTimeout > 900 || this.debugConfig.lambdaTimeout < 0) + ) { + this.debugConfig.lambdaTimeout = 900 + } + if ( + this.debugConfig.debugPort && + (this.debugConfig.debugPort > 65535 || this.debugConfig.debugPort <= 0) + ) { + this.debugConfig.debugPort = 9229 + } + + // acquire invoke lock + if (this.debugState.remoteDebuggingEnabled && !(await client.checkReadyToInvoke())) { + 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 ?? defaultPort, + sourceMap: this.runtimeSettings.sourceMapEnabled, + localRoot: this.debugConfig.localRootPath, + shouldPublishVersion: this.debugConfig.shouldPublishVersion, + remoteRoot: + this.debugConfig.remoteRootPath !== '' ? this.debugConfig.remoteRootPath : '/var/task', + skipFiles: (this.runtimeSettings.skipFiles !== '' + ? this.runtimeSettings.skipFiles + : '/**' + ).split(','), + justMyCode: this.runtimeSettings.justMyCode, + projectName: this.runtimeSettings.projectName, + otherDebugParams: this.debugConfig.otherDebugParams, + 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 + return } } + this.debugState.showDebugTimer = false + } + + await client.invokeLambda( + this.payloadData.sampleText, + this.initialData.Source, + this.debugState.remoteDebuggingEnabled + ) + await this.syncStateFromWorkspace() + }, + + async removeDebugSetup() { + this.debugState.isDebugging = await client.stopDebugging() + }, + + async startDebugTimer() { + this.debugState.debugTimeRemaining = await client.getDebugTimeRemaining() + if (this.debugState.debugTimeRemaining <= 0) { + return + } + this.debugState.showDebugTimer = true + this.debugState.debugTimer = window.setInterval(() => { + this.debugState.debugTimeRemaining-- + if (this.debugState.debugTimeRemaining <= 0) { + this.clearDebugTimer() + } + }, 1000) as number | undefined + }, + + clearDebugTimer() { + if (this.debugState.debugTimer) { + window.clearInterval(this.debugState.debugTimer) + this.debugState.debugTimeRemaining = 0 + this.debugState.debugTimer = undefined + this.debugState.showDebugTimer = false + } + }, + + toggleCollapsible() { + this.uiState.isCollapsed = !this.uiState.isCollapsed + }, + + async openHandler() { + await client.tryOpenHandlerFile() + }, + + async openHandlerWithDelay() { + const preValue = this.debugConfig.localRootPath + // user is inputting the dir, only try to open dir if user stopped typing for 1 second + await new Promise((resolve) => setTimeout(resolve, 1000)) + if (preValue !== this.debugConfig.localRootPath) { + return + } + // try open if user stop input for 1 second + await client.tryOpenHandlerFile(this.debugConfig.localRootPath) + this.debugState.handlerFileAvailable = await client.getHandlerAvailable() + }, + + async downloadRemoteCode() { + try { + const path = await client.downloadRemoteCode() + if (path) { + this.debugConfig.localRootPath = path + this.debugState.handlerFileAvailable = await client.getHandlerAvailable() + } + } catch (error) { + console.error('Failed to download remote code:', error) } - await client.invokeLambda(event, this.initialData.Source) }, loadSampleEvent() { @@ -125,7 +361,7 @@ export default defineComponent({ if (!sample) { return } - this.sampleText = JSON.stringify(JSON.parse(sample), undefined, 4) + this.payloadData.sampleText = JSON.stringify(JSON.parse(sample), undefined, 4) }, (e) => { console.error('client.getSamplePayload failed: %s', (e as Error).message) @@ -134,19 +370,28 @@ export default defineComponent({ }, async loadRemoteTestEvents() { - const shouldLoadEvents = - this.payload === 'savedEvents' && - this.initialData.FunctionArn && - this.initialData.FunctionRegion && - !this.initialData.TestEvents - - 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) } }, }, + mixins: [saveData], }) 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/commonAuthViewProvider.ts b/packages/core/src/login/webview/commonAuthViewProvider.ts index f805d7cf759..8ad5c71bcd1 100644 --- a/packages/core/src/login/webview/commonAuthViewProvider.ts +++ b/packages/core/src/login/webview/commonAuthViewProvider.ts @@ -148,6 +148,10 @@ export class CommonAuthViewProvider implements WebviewViewProvider { const entrypoint = serverHostname !== undefined ? Uri.parse(serverHostname).with({ path: `/${this.source}` }) : scriptUri + // Get Vue.js from dist/libs directory + const vueUri = Uri.joinPath(assetsPath, 'dist', 'libs', 'vue.min.js') + const vueScript = webview.asWebviewUri(vueUri) + return ` @@ -158,7 +162,7 @@ export class CommonAuthViewProvider implements WebviewViewProvider { Base View Extension - + diff --git a/packages/core/src/login/webview/vue/login.vue b/packages/core/src/login/webview/vue/login.vue index 312aa18029b..7973844f4d4 100644 --- a/packages/core/src/login/webview/vue/login.vue +++ b/packages/core/src/login/webview/vue/login.vue @@ -239,6 +239,11 @@
IAM Credentials:
Credentials will be added to the appropriate ~/.aws/ files
+ Learn More
Profile Name
The identifier for these credentials
= 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..7fefd2eb44a --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/activation.ts @@ -0,0 +1,23 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +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' + +export async function activate(extensionContext: vscode.ExtensionContext): 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(extensionContext) + } + await activateExplorer(extensionContext) +} diff --git a/packages/core/src/sagemakerunifiedstudio/auth/model.ts b/packages/core/src/sagemakerunifiedstudio/auth/model.ts new file mode 100644 index 00000000000..6e60fa20e96 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/auth/model.ts @@ -0,0 +1,68 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SsoProfile, SsoConnection } from '../../auth/connection' + +/** + * Scope for SageMaker Unified Studio authentication + */ +export const scopeSmus = 'datazone:domain:access' + +/** + * SageMaker Unified Studio profile extending the base SSO profile + */ +export interface SmusProfile extends SsoProfile { + readonly domainUrl: string + readonly domainId: string +} + +/** + * SageMaker Unified Studio connection extending the base SSO connection + */ +export interface SmusConnection extends SmusProfile, SsoConnection { + readonly id: string + readonly label: string +} + +/** + * 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 = [scopeSmus] +): SmusProfile & { readonly scopes: string[] } { + return { + scopes, + type: 'sso', + startUrl, + ssoRegion: region, + domainUrl, + domainId, + } +} + +/** + * Checks if a connection is a valid SageMaker Unified Studio connection + * @param conn Connection to check + * @returns True if the connection is a valid SMUS connection + */ +export function isValidSmusConnection(conn?: any): conn is SmusConnection { + if (!conn || conn.type !== 'sso') { + return false + } + // Check if the connection has the required SMUS scope + const hasScope = Array.isArray(conn.scopes) && conn.scopes.includes(scopeSmus) + // Check if the connection has the required SMUS properties + const hasSmusProps = 'domainUrl' in conn && 'domainId' in conn + return !!hasScope && !!hasSmusProps +} 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..f060e6477ab --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/auth/providers/connectionCredentialsProvider.ts @@ -0,0 +1,243 @@ +/*! + * 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' + +/** + * 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() + private credentialCache?: { + credentials: AWS.Credentials + expiresAt: Date + } + + constructor( + private readonly smusAuthProvider: SmusAuthenticationProvider, + private readonly connectionId: 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('SMUS Connection: 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(`SMUS Connection: 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 { + const datazoneClient = await DataZoneClient.getInstance(this.smusAuthProvider) + const getConnectionResponse = await datazoneClient.getConnection({ + domainIdentifier: this.smusAuthProvider.getDomainId(), + identifier: this.connectionId, + withSecret: true, + }) + + this.logger.debug(`SMUS Connection: 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(`SMUS Connection: 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..968749a9c9c --- /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() + 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(`SMUS DER: 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(`SMUS DER: Using cached DER credentials for domain ${this.domainId}`) + return this.credentialCache.credentials + } + + this.logger.debug(`SMUS DER: 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(`SMUS DER: 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(`SMUS DER: 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(`SMUS DER: 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(`SMUS DER: 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(`SMUS DER: 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(`SMUS DER: Credential expires at ${credentialExpiresAt.toISOString()}`) + } else { + this.logger.debug(`SMUS DER: Invalid credential expiration date, using default`) + } + } else { + this.logger.debug(`SMUS DER: 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('SMUS DER: 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(`SMUS DER: Invalidating cached DER credentials for domain ${this.domainId}`) + // Clear cache to force fresh fetch on next getCredentials() call + this.credentialCache = undefined + this.logger.debug(`SMUS DER: Successfully invalidated DER credentials cache for domain ${this.domainId}`) + } + /** + * Disposes of the provider and cleans up resources + */ + public dispose(): void { + this.logger.debug(`SMUS DER: Disposing DER credentials provider for domain ${this.domainId}`) + this.invalidate() + this.logger.debug(`SMUS DER: 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..5eb42e1fd5f --- /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 { DataZoneClient } from '../../shared/client/datazoneClient' +import { SmusAuthenticationProvider } from './smusAuthenticationProvider' +import { CredentialType } from '../../../shared/telemetry/telemetry' +import { SmusCredentialExpiry, validateCredentialFields } from '../../shared/smusUtils' +import { loadMappings, saveMappings } from '../../../awsService/sagemaker/credentialMapping' + +/** + * 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() + 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(`SMUS Project: Getting credentials for project ${this.projectId}`) + + // Check cache first (10-minute expiry) + if (this.credentialCache && this.credentialCache.expiresAt > new Date()) { + this.logger.debug(`SMUS Project: Using cached project credentials for project ${this.projectId}`) + return this.credentialCache.credentials + } + + this.logger.debug(`SMUS Project: Fetching project credentials from API for project ${this.projectId}`) + + try { + const dataZoneClient = await DataZoneClient.getInstance(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('SMUS Project: 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('SMUS Project: 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(`SMUS Project: SSH refresh already active for project ${this.projectId}`) + return + } + + this.logger.info(`SMUS Project: 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(`SMUS Project: 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(`SMUS Project: 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(`SMUS Project: 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..6c0f204cbd3 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/auth/providers/smusAuthenticationProvider.ts @@ -0,0 +1,742 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +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 } from '../../shared/smusUtils' +import { createSmusProfile, isValidSmusConnection, SmusConnection } from '../model' +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 { fromIni } from '@aws-sdk/credential-providers' +import { randomUUID } from '../../../shared/crypto' +import { DefaultStsClient } from '../../../shared/clients/stsClient' +import { DataZoneClient } from '../../shared/client/datazoneClient' + +/** + * 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) +} +const authClassName = 'SmusAuthenticationProvider' + +/** + * Authentication provider for SageMaker Unified Studio + * Manages authentication state and credentials for SMUS + */ +export class SmusAuthenticationProvider { + private readonly logger = getLogger() + public readonly onDidChangeActiveConnection = this.secondaryAuth.onDidChangeActiveConnection + 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() + + public constructor( + public readonly auth = Auth.instance, + public readonly secondaryAuth = getSecondaryAuth( + auth, + 'smus', + 'SageMaker Unified Studio', + isValidSmusConnection + ) + ) { + this.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 all clients in client store when connection changes + ConnectionClientStore.getInstance().clearAll() + await setSmusConnectedContext(this.isConnected()) + await setSmusSpaceEnvironmentContext(SmusUtils.isInSmusSpaceEnvironment()) + this.onDidChangeEmitter.fire() + }) + + // Set initial context in case event does not trigger + void setSmusConnectedContext(this.isConnectionValid()) + void setSmusSpaceEnvironmentContext(SmusUtils.isInSmusSpaceEnvironment()) + } + + /** + * Stops SSH credential refresh for all projects + * Called when SMUS connection changes or extension deactivates + */ + public stopAllSshCredentialRefresh(): void { + this.logger.debug('SMUS Auth: Stopping SSH credential refresh for all projects') + for (const provider of this.projectCredentialProvidersCache.values()) { + provider.stopProactiveCredentialRefresh() + } + } + + /** + * Gets the active connection + */ + public get activeConnection() { + if (getContext('aws.smus.inSmusSpaceEnvironment')) { + const resourceMetadata = getResourceMetadata()! + if (resourceMetadata.AdditionalMetadata!.DataZoneDomainRegion) { + return { + domainId: resourceMetadata.AdditionalMetadata!.DataZoneDomainId!, + ssoRegion: resourceMetadata.AdditionalMetadata!.DataZoneDomainRegion!, + // The following fields won't be needed in SMUS space environment + // Craft the domain url with known information + // Use randome id as placeholder + domainUrl: `https://${resourceMetadata.AdditionalMetadata!.DataZoneDomainId!}.sagemaker.${resourceMetadata.AdditionalMetadata!.DataZoneDomainRegion!}.on.aws/`, + id: randomUUID(), + } + } else { + throw new ToolkitError('Domain region not found in metadata file.') + } + } + return this.secondaryAuth.activeConnection + } + + /** + * 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 + * Uses a promise to prevent multiple simultaneous restore calls + */ + public async restore() { + await this.secondaryAuth.restoreConnection() + } + + /** + * Authenticates with SageMaker Unified Studio using a domain URL + * @param domainUrl The SageMaker Unified Studio domain URL + * @returns Promise resolving to the connection + */ + @withTelemetryContext({ name: 'connectToSmus', class: authClassName }) + public async connectToSmus(domainUrl: string): Promise { + const logger = getLogger() + + 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: 'InvalidDomainUrl' }) + } + + logger.info(`SMUS: 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(`SMUS: 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('SMUS: Using existing valid connection') + + // Use the existing 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 + } + + // If connection is invalid or expired, reauthenticate + if (connectionState === 'invalid') { + logger.info('SMUS: Existing 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(`SMUS: 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 + } + } + + // No existing connection found, create a new one + logger.info('SMUS: 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(`SMUS: 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 + } catch (e) { + throw ToolkitError.chain(e, 'Failed to connect to SageMaker Unified Studio', { + code: 'FailedToConnect', + }) + } + } + + /** + * 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: SsoConnection) { + try { + return await this.auth.reauthenticate(conn) + } 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: SsoConnection): 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() + + if (!this.activeConnection) { + throw new ToolkitError('No active SMUS connection available', { code: SmusErrorCodes.NoActiveConnection }) + } + + try { + const accessToken = await this.auth.getSsoAccessToken(this.activeConnection) + logger.debug(`SMUS: Successfully retrieved SSO access token for connection ${this.activeConnection.id}`) + + return accessToken + } catch (err) { + logger.error( + `SMUS: Failed to retrieve SSO access token for connection ${this.activeConnection.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 ${this.activeConnection.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() + + if (!this.activeConnection) { + throw new ToolkitError('No active SMUS connection available', { code: SmusErrorCodes.NoActiveConnection }) + } + + logger.debug(`SMUS: Getting project provider for project ${projectId}`) + + // Check if we already have a cached provider for this project + if (this.projectCredentialProvidersCache.has(projectId)) { + logger.debug('SMUS: Using cached project provider') + return this.projectCredentialProvidersCache.get(projectId)! + } + + logger.debug('SMUS: 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('SMUS: 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() + + if (!this.activeConnection) { + throw new ToolkitError('No active SMUS connection available', { code: SmusErrorCodes.NoActiveConnection }) + } + + const cacheKey = `${this.activeConnection.domainId}:${projectId}:${connectionId}` + logger.debug(`SMUS: Getting connection provider for connection ${connectionId}`) + + // Check if we already have a cached provider for this connection + if (this.connectionCredentialProvidersCache.has(cacheKey)) { + logger.debug('SMUS: Using cached connection provider') + return this.connectionCredentialProvidersCache.get(cacheKey)! + } + + logger.debug('SMUS: Creating new connection provider') + // Create a new connection provider and cache it + const connectionProvider = new ConnectionCredentialsProvider(this, connectionId) + this.connectionCredentialProvidersCache.set(cacheKey, connectionProvider) + + logger.debug('SMUS: 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 }) + } + return this.activeConnection.domainId + } + + /** + * 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 }) + } + return this.activeConnection.domainUrl + } + + /** + * 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() + + // Return cached value if available + if (this.cachedDomainAccountId) { + logger.debug('SMUS: 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') + + // Get DER credentials provider + const derCredProvider = 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 derCredProvider.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() + + // Return cached value if available + if (this.cachedProjectAccountIds.has(projectId)) { + logger.debug(`SMUS: 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 DataZoneClient.getInstance(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.') + } + } + + if (!this.activeConnection) { + throw new ToolkitError('No active SMUS connection available', { code: SmusErrorCodes.NoActiveConnection }) + } + return this.activeConnection.ssoRegion + } + + /** + * 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() + + if (getContext('aws.smus.inSmusSpaceEnvironment')) { + // When in SMUS space, DomainExecutionRoleCreds can be found in config file + // Read the credentials from credential profile DomainExecutionRoleCreds + const credentials = fromIni({ profile: 'DomainExecutionRoleCreds' }) + return { + getCredentials: async () => await credentials(), + } + } + + if (!this.activeConnection) { + throw new ToolkitError('No active SMUS connection available', { code: SmusErrorCodes.NoActiveConnection }) + } + + // Create a cache key based on the connection details + const cacheKey = `${this.activeConnection.ssoRegion}:${this.activeConnection.domainId}` + + logger.debug(`SMUS: Getting credentials provider for cache key: ${cacheKey}`) + + // Check if we already have a cached provider + if (this.credentialsProviderCache.has(cacheKey)) { + logger.debug('SMUS: Using cached credentials provider') + return this.credentialsProviderCache.get(cacheKey) + } + + logger.debug('SMUS: Creating new credentials provider') + + // Create a new provider and cache it + const provider = new DomainExecRoleCredentialsProvider( + this.activeConnection.domainUrl, + this.activeConnection.domainId, + this.activeConnection.ssoRegion, + async () => await this.getAccessToken() + ) + + this.credentialsProviderCache.set(cacheKey, provider) + logger.debug('SMUS: 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() + logger.debug('SMUS: 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(`SMUS: Invalidated credentials for cache key: ${cacheKey}`) + } catch (err) { + logger.warn(`SMUS: 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(`SMUS: Invalidated connection credentials for cache key: ${cacheKey}`) + } catch (err) { + logger.warn(`SMUS: Failed to invalidate connection credentials for cache key ${cacheKey}: %s`, err) + } + } + + // Clear cached domain account ID + this.cachedDomainAccountId = undefined + logger.debug('SMUS: Cleared cached domain account ID') + + // Clear cached project account IDs + this.cachedProjectAccountIds.clear() + logger.debug('SMUS: Cleared cached project account IDs') + } + + /** + * Invalidates all project cached credentials + */ + public async invalidateAllProjectCredentialsInCache(): Promise { + const logger = getLogger() + logger.debug('SMUS: 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(`SMUS: Invalidated project credentials for project: ${projectId}`) + } catch (err) { + logger.warn(`SMUS: Failed to invalidate project credentials for project ${projectId}: %s`, err) + } + } + } + + /** + * Stops SSH credential refresh and cleans up resources + */ + public dispose(): void { + this.logger.debug('SMUS Auth: 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() + + this.logger.debug('SMUS Auth: 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()) + } +} 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..8f0998e295f --- /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() + + 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..901c2e5a60f --- /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().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().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().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().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..80654b64ac0 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/connectionMagicsSelector/services/notebookStateManager.ts @@ -0,0 +1,420 @@ +/*! + * 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().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().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().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().error('WorkspaceEdit failed to apply') + } + } catch (error) { + getLogger().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().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..8a686b48654 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/activation.ts @@ -0,0 +1,142 @@ +/*! + * 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 { DataZoneClient } from '../shared/client/datazoneClient' +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 { setupUserActivityMonitoring } from '../../awsService/sagemaker/sagemakerSpace' +import { telemetry } from '../../shared/telemetry/telemetry' +import { isSageMaker } from '../../shared/extensionUtilities' +import { recordSpaceTelemetry } from '../shared/telemetry' + +export async function activate(extensionContext: vscode.ExtensionContext): Promise { + // Initialize the SMUS authentication provider + const logger = getLogger() + logger.debug('SMUS: 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('SMUS: 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(), + smusLearnMoreCommand.register(), + smusSignOutCommand.register(), + 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.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) => { + await recordSpaceTelemetry(span, node) + await stopSpace(node.resource, extensionContext, node.resource.sageMakerClient) + }) + }), + + vscode.commands.registerCommand( + 'aws.smus.openRemoteConnection', + async (node: SagemakerUnifiedStudioSpaceNode) => { + if (!validateNode(node)) { + return + } + await telemetry.smus_openRemoteConnection.run(async (span) => { + await recordSpaceTelemetry(span, node) + await openRemoteConnect(node.resource, extensionContext, node.resource.sageMakerClient) + }) + } + ), + + vscode.commands.registerCommand('aws.smus.reauthenticate', async (connection?: any) => { + if (connection) { + try { + await smusAuthProvider.reauthenticate(connection) + // Refresh the tree view after successful reauthentication + treeDataProvider.refresh() + // Show success message + void vscode.window.showInformationMessage( + 'Successfully reauthenticated with SageMaker Unified Studio' + ) + } catch (error) { + // Show error message if reauthentication fails + void vscode.window.showErrorMessage(`Failed to reauthenticate: ${error}`) + logger.error('SMUS: 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/lakehouseStrategy.ts b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/lakehouseStrategy.ts new file mode 100644 index 00000000000..546a73135c6 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/lakehouseStrategy.ts @@ -0,0 +1,587 @@ +/*! + * 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 { GlueCatalog, GlueCatalogClient } from '../../shared/client/glueCatalogClient' +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 { + getLabel, + isLeafNode, + getIconForNodeType, + getTooltip, + createColumnTreeItem, + getColumnType, + createErrorItem, +} 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' + +/** + * Lakehouse data node for SageMaker Unified Studio + */ +export class LakehouseNode implements TreeNode { + private childrenNodes: TreeNode[] | undefined + private isLoading = false + private readonly logger = getLogger() + + 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 + void vscode.window.showErrorMessage(errorMessage) + 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() + + // 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 errorMessage = (awsDataCatalogResult.reason as Error).message + void vscode.window.showErrorMessage(errorMessage) + errors.push(createErrorItem(errorMessage, 'aws-data-catalog', node.id) as LakehouseNode) + } + + if (catalogsResult.status === 'rejected') { + const errorMessage = (catalogsResult.reason as Error).message + void vscode.window.showErrorMessage(errorMessage) + errors.push(createErrorItem(errorMessage, 'catalogs', node.id) as LakehouseNode) + } + + 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 + void vscode.window.showErrorMessage(errorMessage) + 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: GlueCatalog + children?: GlueCatalog[] +} + +/** + * 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: GlueCatalog[]): 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 + return createCatalogNode(parentCatalog.CatalogId || '', parentCatalog, glueClient, parent, false) + }) +} + +/** + * Creates a catalog node + */ +function createCatalogNode( + catalogId: string, + catalog: GlueCatalog, + glueClient: GlueClient, + parent: LakehouseNode, + isParent: boolean = false +): LakehouseNode { + const logger = getLogger() + + 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 () => [] // Placeholder, will be overridden for parent catalogs with children + : 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 + void vscode.window.showErrorMessage(errorMessage) + 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() + + 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 + void vscode.window.showErrorMessage(errorMessage) + 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() + + 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}`) + 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..af0d7cfbbac --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/redshiftStrategy.ts @@ -0,0 +1,1038 @@ +/*! + * 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 { GlueCatalog } from '../../shared/client/glueCatalogClient' +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() + + 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 + void vscode.window.showErrorMessage(errorMessage) + 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() + 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: GlueCatalog[] = [] + 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 errorMessage = `Failed to fetch databases - ${databasesResult.reason?.message || databasesResult.reason}.` + void vscode.window.showErrorMessage(errorMessage) + allNodes.push(createErrorItem(errorMessage, 'databases', node.id) as RedshiftNode) + } 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 errorMessage = `Failed to fetch catalogs - ${catalogsResult.reason?.message || catalogsResult.reason}` + void vscode.window.showErrorMessage(errorMessage) + allNodes.push(createErrorItem(errorMessage, 'catalogs', node.id) as RedshiftNode) + } 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() + 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() + + 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 + void vscode.window.showErrorMessage(errorMessage) + return [createErrorItem(errorMessage, 'schemas', node.id) as RedshiftNode] + } + } + ) +} + +/** + * Creates a schema node + */ +function createSchemaNode(schemaName: string, connectionConfig: ConnectionConfig, parent: RedshiftNode): RedshiftNode { + const logger = getLogger() + + 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 + void vscode.window.showErrorMessage(errorMessage) + 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() + + 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 + void vscode.window.showErrorMessage(errorMessage) + 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 + void vscode.window.showErrorMessage(errorMessage) + 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 + void vscode.window.showErrorMessage(errorMessage) + 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 + void vscode.window.showErrorMessage(errorMessage) + 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..4106a0b4889 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/s3Strategy.ts @@ -0,0 +1,599 @@ +/*! + * 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 { 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() + 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 + void vscode.window.showErrorMessage(errorMessage) + 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() + + // 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 + } + + // 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 (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 + void vscode.window.showErrorMessage(errorMessage) + 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 + void vscode.window.showErrorMessage(errorMessage) + 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 + void vscode.window.showErrorMessage(errorMessage) + 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() + + 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 + void vscode.window.showErrorMessage(errorMessage) + 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 + } + + // Parse S3 URI: s3://bucket-name/prefix/path/ + const uriWithoutPrefix = s3Uri.replace('s3://', '') + // 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() + 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}`) + 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() + 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 + void vscode.window.showErrorMessage(errorMessage) + 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..ff25f64cf74 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioAuthInfoNode.ts @@ -0,0 +1,90 @@ +/*! + * 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' + +/** + * 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() + }) + } + + public getTreeItem(): vscode.TreeItem { + // 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) { + const conn = this.authProvider.activeConnection + domainId = conn.domainId || 'Unknown' + region = conn.ssoRegion || 'Unknown' + } + + // Create display based on connection status + let label: string + let iconPath: vscode.ThemeIcon + let tooltip: string + + if (isConnected && isValid) { + label = `Domain: ${domainId}` + iconPath = new vscode.ThemeIcon('key', new vscode.ThemeColor('charts.green')) + tooltip = `Connected to SageMaker Unified Studio\nDomain ID: ${domainId}\nRegion: ${region}\nStatus: Connected` + } else if (isConnected && !isValid) { + label = `Domain: ${domainId} (Expired) - Click to reauthenticate` + iconPath = new vscode.ThemeIcon('warning', new vscode.ThemeColor('charts.yellow')) + tooltip = `Connection to SageMaker Unified Studio has expired\nDomain ID: ${domainId}\nRegion: ${region}\nStatus: Expired - Click to reauthenticate` + } 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' + } + + const item = new vscode.TreeItem(label, vscode.TreeItemCollapsibleState.None) + + // Add region as description (appears to the right) if connected + if (isConnected) { + item.description = region + } + + // 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 + 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..01293e7e523 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioComputeNode.ts @@ -0,0 +1,66 @@ +/*! + * 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' + +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) { + 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..969efa9823d --- /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() + 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..a04377f0133 --- /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 { DataZoneClient } from '../../shared/client/datazoneClient' + +// 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 DataZoneClient.getInstance(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..4294a3e42f4 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioDataNode.ts @@ -0,0 +1,250 @@ +/*! + * 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 { DataZoneClient, 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, NO_DATA_FOUND_MESSAGE } from './types' +import { SmusAuthenticationProvider } from '../../auth/providers/smusAuthenticationProvider' + +/** + * 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() + 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 DataZoneClient.getInstance(this.authProvider) + 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}`) + void vscode.window.showErrorMessage(errorMessage) + 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 + 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) + } + + // 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 datazoneClient = await DataZoneClient.getInstance(this.authProvider) + 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 + ) + + const s3ConnectionNode = createS3ConnectionNode( + connection, + connectionCredentialsProvider, + getConnectionResponse.location?.awsRegion || region + ) + + const accessGrantNodes = await createS3AccessGrantNodes( + connection, + connectionCredentialsProvider, + getConnectionResponse.location?.awsRegion || region, + getConnectionResponse.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}`) + void vscode.window.showErrorMessage(errorMessage) + return [createErrorItem(errorMessage, `s3-${connection.connectionId}`, this.id)] + } + } + + private async createRedshiftNode( + project: DataZoneProject, + connection: DataZoneConnection, + region: string + ): Promise { + try { + const datazoneClient = await DataZoneClient.getInstance(this.authProvider) + 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}`) + void vscode.window.showErrorMessage(errorMessage) + return createErrorItem(errorMessage, `redshift-${connection.connectionId}`, this.id) + } + } + + private async createLakehouseNode( + project: DataZoneProject, + connection: DataZoneConnection, + region: string + ): Promise { + try { + const datazoneClient = await DataZoneClient.getInstance(this.authProvider) + 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 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}`) + void vscode.window.showErrorMessage(errorMessage) + 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 () => { + const s3Nodes: TreeNode[] = [] + for (const connection of s3Connections) { + const nodes = await this.createS3Node(project, connection, region) + s3Nodes.push(...nodes) + } + return s3Nodes + }, + 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..8097ceed9e7 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioProjectNode.ts @@ -0,0 +1,242 @@ +/*! + * 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' + +/** + * 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() + 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') + 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(), + }) + + // Skip access check if we're in SMUS space environment (already in project space) + if (!getContext('aws.smus.inSmusSpaceEnvironment')) { + 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, + }, + ] + } + } + + 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 DataZoneClient.getInstance(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 any 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) + 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 + } + } + + private async fetchProjectName(): Promise { + if (!this.project || !getContext('aws.smus.inSmusSpaceEnvironment')) { + return + } + + try { + const dzClient = await DataZoneClient.getInstance(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') + } + const projectProvider = await this.authProvider.getProjectCredentialProvider(this.project.id) + this.logger.info(`Successfully obtained project credentials provider for project ${this.project.id}`) + const 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..db3f6959969 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioRootNode.ts @@ -0,0 +1,463 @@ +/*! + * 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 { DataZoneClient, DataZoneProject } 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 { SmusAuthenticationProvider } from '../../auth/providers/smusAuthenticationProvider' +import { ToolkitError } from '../../../../src/shared/errors' +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() + 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 + 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(`SMUS Root Node: 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('SMUS Root Node: Connection is expired, showing reauthentication prompt') + // Show reauthentication prompt to user + void this.authProvider.showReauthenticationPrompt(activeConnection as any) + 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() + 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', () => async () => { + const logger = getLogger() + return telemetry.smus_login.run(async (span) => { + try { + // Get DataZoneClient instance for URL validation + + // Show domain URL input dialog + const domainUrl = await vscode.window.showInputBox({ + title: 'SageMaker Unified Studio Authentication', + prompt: 'Enter your SageMaker Unified Studio Domain URL', + placeHolder: 'https://.sagemaker..on.aws', + validateInput: (value) => SmusUtils.validateDomainUrl(value), + }) + + if (!domainUrl) { + // User cancelled + logger.debug('User cancelled domain URL input') + throw new ToolkitError('User cancelled domain URL input', { + cancelled: true, + code: SmusErrorCodes.UserCancelled, + }) + } + + // Show a simple status bar message instead of progress dialog + vscode.window.setStatusBarMessage('Connecting to SageMaker Unified Studio...', 10000) + + try { + // Get the authentication provider instance + const authProvider = SmusAuthenticationProvider.fromContext() + + // Connect to SMUS using the authentication provider + const connection = await authProvider.connectToSmus(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 = connection.ssoRegion + + logger.info(`Connected to SageMaker Unified Studio domain: ${domainId} in region ${region}`) + await recordAuthTelemetry(span, authProvider, domainId, region) + + // Show success message + void vscode.window.showInformationMessage( + `Successfully connected to SageMaker Unified Studio domain: ${domainId}` + ) + + // Clear the status bar message + vscode.window.setStatusBarMessage('Connected to SageMaker Unified Studio', 3000) + + // 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}`) + } + } 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, + }) + } + } catch (err) { + const isUserCancelled = err instanceof ToolkitError && err.code === SmusErrorCodes.UserCancelled + if (!isUserCancelled) { + void vscode.window.showErrorMessage( + `SageMaker Unified Studio: Failed to initiate login: ${(err as Error).message}` + ) + } + logger.error('Failed to initiate login: %s', (err as Error).message) + throw new ToolkitError('Failed to initiate login.', { + cause: err as Error, + code: (err as Error).name, + }) + } + }) +}) + +/** + * Command to sign out from SageMaker Unified Studio + */ +export const smusSignOutCommand = Commands.declare('aws.smus.signOut', () => async () => { + const logger = getLogger() + 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 + } + + // Get connection details for logging + const activeConnection = authProvider.activeConnection + const domainId = activeConnection?.domainId + const region = activeConnection?.ssoRegion + + // Show status message + vscode.window.setStatusBarMessage('Signing out from SageMaker Unified Studio...', 5000) + await recordAuthTelemetry(span, authProvider, domainId, region) + + // Delete the connection (this will also invalidate tokens and clear cache) + if (activeConnection) { + await authProvider.secondaryAuth.deleteConnection() + logger.info(`Signed out from SageMaker Unified Studio${domainId}`) + } + + // Show success message + void vscode.window.showInformationMessage('Successfully signed out from SageMaker Unified Studio.') + + // Clear the status bar message + vscode.window.setStatusBarMessage('Signed out from SageMaker Unified Studio', 3000) + + // 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() +} + +export async function selectSMUSProject(projectNode?: SageMakerUnifiedStudioProjectNode) { + const logger = getLogger() + + 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 client = await DataZoneClient.getInstance(authProvider) + logger.debug('DataZone client instance obtained successfully') + + const allProjects = await client.fetchAllProjects() + const items = createProjectQuickPickItems(allProjects) + + if (items.length === 0) { + logger.info('No projects found in the domain') + void vscode.window.showInformationMessage('No projects found in the domain') + await showQuickPick([{ label: 'No projects found', detail: '', description: '', data: {} }]) + return + } + + const selectedProject = await showQuickPick(items) + const accountId = await authProvider.getDomainAccountId() + span.record({ + 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 + + if (isAccessDenied(error)) { + await showQuickPick([ + { + label: '$(error)', + description: "You don't have permissions to view projects. Please contact your administrator", + }, + ]) + return + } + + logger.error('Failed to select project: %s', error.message) + void vscode.window.showErrorMessage(`Failed to select project: ${error.message}`) + } + }) +} 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..a5421716d8f --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpacesParentNode.ts @@ -0,0 +1,235 @@ +/*! + * 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 { DataZoneClient } from '../../shared/client/datazoneClient' +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/sagemakerParentNode' +import { SagemakerUnifiedStudioSpaceNode } from './sageMakerUnifiedStudioSpaceNode' +import { PollingSet } from '../../../shared/utilities/pollingSet' +import { SmusAuthenticationProvider } from '../../auth/providers/smusAuthenticationProvider' +import { SmusUtils } from '../../shared/smusUtils' +import { getIcon } from '../../../shared/icons' +import { PENDING_NODE_POLLING_INTERVAL_MS } from './utils' + +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() + 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() + } + 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, + }, + ] + } + + 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('SMUS: Getting DataZone client instance') + const datazoneClient = await DataZoneClient.getInstance(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().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() + } + } + + private async updateChildren(): Promise { + const datazoneClient = await DataZoneClient.getInstance(this.authProvider) + // Will be of format: 'ABCA4NU3S7PEOLDQPLXYZ:user-12345678-d061-70a4-0bf2-eeee67a6ab12' + const userId = await datazoneClient.getUserId() + const ssoUserProfileId = 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 + const filteredSpaceApps = new Map() + for (const [key, app] of spaceApps.entries()) { + const userProfile = app.OwnershipSettingsSummary?.OwnerUserProfileName + if (ssoUserProfileId === 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..a94d25fccc4 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/types.ts @@ -0,0 +1,207 @@ +/*! + * 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)$/ + +// 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]' 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..32924ad3d9f --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/utils.ts @@ -0,0 +1,391 @@ +/*! + * 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, +} from './types' +import { DataZoneConnection } from '../../shared/client/datazoneClient' + +/** + * 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 + +/** + * 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) + ) { + 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') + } + + // 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 + } +} + +/** + * 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 !!connection?.props?.glueProperties + } + return false +} 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..edf317f6479 --- /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().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..88d08c93b86 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/shared/client/credentialsAdapter.ts @@ -0,0 +1,60 @@ +/*! + * 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' + +/** + * Adapts a ConnectionCredentialsProvider (SDK v3) to work with SDK v2's CredentialProviderChain + */ +export function adaptConnectionCredentialsProvider( + connectionCredentialsProvider: ConnectionCredentialsProvider +): 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().debug('Attempting to get credentials from ConnectionCredentialsProvider') + + connectionCredentialsProvider + .getCredentials() + .then((creds) => { + getLogger().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().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..ffa0e7bfbf3 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/shared/client/datazoneClient.ts @@ -0,0 +1,792 @@ +/*! + * 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 type { SmusAuthenticationProvider } from '../../auth/providers/smusAuthenticationProvider' +import { DefaultStsClient } from '../../../shared/clients/stsClient' + +/** + * Represents a DataZone project + */ +export interface DataZoneProject { + id: string + name: string + description?: string + domainId: 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 + } +} + +// Constants for DataZone environment configuration +const toolingBlueprintName = 'Tooling' +const sageMakerProviderName = 'Amazon SageMaker' + +/** + * Client for interacting with AWS DataZone API with DER credential support + * + * This client integrates with SmusAuthenticationProvider to provide authenticated + * DataZone operations using Domain Execution Role (DER) credentials. + * + * One instance per connection/domainId is maintained to avoid duplication. + */ +export class DataZoneClient { + /** + * 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, + } + } + + private datazoneClient: DataZone | undefined + private static instances = new Map() + private readonly logger = getLogger() + + private constructor( + private readonly authProvider: SmusAuthenticationProvider, + private readonly domainId: string, + private readonly region: string + ) {} + + /** + * Gets an authenticated DataZoneClient instance using DER credentials + * One instance per connection/domainId is maintained + * @param authProvider The SMUS authentication provider + * @returns Promise resolving to authenticated DataZoneClient instance + */ + public static async getInstance(authProvider: SmusAuthenticationProvider): Promise { + const logger = getLogger() + + if (!authProvider.isConnected()) { + throw new Error('SMUS authentication provider is not connected') + } + + const activeConnection = authProvider.activeConnection! + const instanceKey = `${activeConnection.domainId}:${activeConnection.ssoRegion}` + + logger.debug(`DataZoneClient: Getting instance for domain: ${instanceKey}`) + + // Check if we already have an instance for this domain/region + if (DataZoneClient.instances.has(instanceKey)) { + const existingInstance = DataZoneClient.instances.get(instanceKey)! + logger.debug('DataZoneClient: Using existing instance') + return existingInstance + } + + // Create new instance + logger.debug('DataZoneClient: Creating new instance') + const instance = new DataZoneClient(authProvider, activeConnection.domainId, activeConnection.ssoRegion) + DataZoneClient.instances.set(instanceKey, instance) + + // Set up cleanup when connection changes + const disposable = authProvider.onDidChangeActiveConnection(() => { + logger.debug(`DataZoneClient: Connection changed, cleaning up instance for: ${instanceKey}`) + DataZoneClient.instances.delete(instanceKey) + instance.datazoneClient = undefined + disposable.dispose() + }) + + logger.info(`DataZoneClient: Created instance for domain ${activeConnection.domainId}`) + return instance + } + + /** + * Disposes all instances and cleans up resources + */ + public static dispose(): void { + const logger = getLogger() + logger.debug('DataZoneClient: Disposing all instances') + + for (const [key, instance] of DataZoneClient.instances.entries()) { + instance.datazoneClient = undefined + logger.debug(`DataZoneClient: Disposed instance for: ${key}`) + } + + DataZoneClient.instances.clear() + } + + /** + * 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: toolingBlueprintName, + }) + + 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?.find((env) => env.name === toolingBlueprintName) + 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 { + this.logger.debug('DataZoneClient: Creating authenticated DataZone client with DER credentials') + + const credentialsProvider = async () => { + const credentials = await (await this.authProvider.getDerCredentialsProvider()).getCredentials() + return { + accessKeyId: credentials.accessKeyId, + secretAccessKey: credentials.secretAccessKey, + sessionToken: credentials.sessionToken, + expiration: credentials.expiration, + } + } + + this.datazoneClient = new DataZone({ + region: this.region, + credentials: credentialsProvider, + }) + this.logger.debug('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, + 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 + } + + /** + * 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) + + // 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, + } + + 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) + + return { + connectionId: connection.connectionId || '', + name: connection.name || '', + description: '', + type: connection.type || '', + domainId, + environmentId, + projectId, + props: connection.props || {}, + location, + } + }) + 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: toolingBlueprintName, + }) + } 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?.find((env) => env.name === toolingBlueprintName) + 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 domainId The DataZone domain identifier + * @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 logger = getLogger() + + const datazoneClient = await DataZoneClient.getInstance(this.authProvider) + if (!datazoneClient) { + throw new Error('DataZone client is not initialized') + } + + const toolingEnvId = await datazoneClient + .getToolingEnvironmentId(datazoneClient.getDomainId(), projectId) + .catch((err) => { + logger.error('Failed to get tooling environment ID for project %s', projectId) + throw new Error(`Failed to get tooling environment ID: ${err.message}`) + }) + + if (!toolingEnvId) { + throw new Error('No default environment found for project') + } + + return await datazoneClient.getEnvironmentDetails(toolingEnvId) + } + + public async getUserId(): Promise { + const derCredProvider = await this.authProvider.getDerCredentialsProvider() + this.logger.debug(`Calling STS GetCallerIdentity using DER credentials of ${this.getDomainId()}`) + const stsClient = new DefaultStsClient(this.getRegion(), await derCredProvider.getCredentials()) + const callerIdentity = await stsClient.getCallerIdentity() + this.logger.debug(`Retrieved caller identity, UserId: ${callerIdentity.UserId}`) + return callerIdentity.UserId + } +} 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..bbd3c440478 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/shared/client/glueCatalogClient.ts @@ -0,0 +1,136 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Service } from 'aws-sdk' +import globals from '../../../shared/extensionGlobals' +import { getLogger } from '../../../shared/logger/logger' +import * as GlueCatalogApi from './gluecatalogapi' +import apiConfig = require('./gluecatalogapi.json') +import { ServiceConfigurationOptions } from 'aws-sdk/lib/service' +import { ConnectionCredentialsProvider } from '../../auth/providers/connectionCredentialsProvider' +import { adaptConnectionCredentialsProvider } from './credentialsAdapter' + +/** + * Represents a Glue catalog + */ +export type GlueCatalog = GlueCatalogApi.Types.Catalog + +/** + * Client for interacting with Glue Catalog API + */ +export class GlueCatalogClient { + private glueClient: GlueCatalogApi | undefined + private static instance: GlueCatalogClient | undefined + private readonly logger = getLogger() + + 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: GlueCatalog[]; 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, + }) + .promise() + + const catalogs: GlueCatalog[] = 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 provided credentials + this.glueClient = (await globals.sdkClientBuilder.createAwsService( + Service, + { + apiConfig: apiConfig, + region: this.region, + credentialProvider: adaptConnectionCredentialsProvider(this.connectionCredentialsProvider), + } as ServiceConfigurationOptions, + undefined, + false + )) as GlueCatalogApi + } else { + // Use the SDK client builder for default credentials + this.glueClient = (await globals.sdkClientBuilder.createAwsService( + Service, + { + apiConfig: apiConfig, + region: this.region, + } as ServiceConfigurationOptions, + undefined, + false + )) as GlueCatalogApi + } + + 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..15034a488cf --- /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() + + 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/gluecatalogapi.json b/packages/core/src/sagemakerunifiedstudio/shared/client/gluecatalogapi.json new file mode 100644 index 00000000000..ecd3705c096 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/shared/client/gluecatalogapi.json @@ -0,0 +1,2695 @@ +{ + "version": "2.0", + "metadata": { + "apiVersion": "2022-07-26", + "auth": ["aws.auth#sigv4"], + "endpointPrefix": "glue", + "jsonVersion": "1.1", + "protocol": "json", + "protocols": ["json"], + "serviceFullName": "Glue Private Service", + "serviceId": "GlueCatalogAPI", + "signatureVersion": "v4", + "signingName": "glue", + "targetPrefix": "AWSGlue", + "uid": "gluecatalogapi-2022-07-26" + }, + "operations": { + "DescribeConnectionType": { + "name": "DescribeConnectionType", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "DescribeConnectionTypeRequest" + }, + "output": { + "shape": "DescribeConnectionTypeResponse" + }, + "errors": [ + { + "shape": "InternalServiceException" + }, + { + "shape": "InvalidInputException" + }, + { + "shape": "AccessDeniedException" + }, + { + "shape": "ValidationException" + } + ] + }, + "GetCatalog": { + "name": "GetCatalog", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "GetCatalogRequest" + }, + "output": { + "shape": "GetCatalogResponse" + }, + "errors": [ + { + "shape": "InternalServiceException" + }, + { + "shape": "FederationSourceException" + }, + { + "shape": "InvalidInputException" + }, + { + "shape": "GlueEncryptionException" + }, + { + "shape": "EntityNotFoundException" + }, + { + "shape": "OperationTimeoutException" + }, + { + "shape": "AccessDeniedException" + } + ] + }, + "GetCatalogs": { + "name": "GetCatalogs", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "GetCatalogsRequest" + }, + "output": { + "shape": "GetCatalogsResponse" + }, + "errors": [ + { + "shape": "InternalServiceException" + }, + { + "shape": "InvalidInputException" + }, + { + "shape": "GlueEncryptionException" + }, + { + "shape": "FederationSourceException" + }, + { + "shape": "EntityNotFoundException" + }, + { + "shape": "OperationTimeoutException" + }, + { + "shape": "AccessDeniedException" + } + ] + }, + "GetCompletion": { + "name": "GetCompletion", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "GetCompletionRequest" + }, + "output": { + "shape": "GetCompletionResponse" + }, + "errors": [ + { + "shape": "AlreadyExistsException" + }, + { + "shape": "InternalServiceException" + }, + { + "shape": "InvalidInputException" + }, + { + "shape": "EntityNotFoundException" + }, + { + "shape": "OperationTimeoutException" + }, + { + "shape": "AccessDeniedException" + }, + { + "shape": "ValidationException" + } + ] + }, + "GetEntityRecords": { + "name": "GetEntityRecords", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "GetEntityRecordsRequest" + }, + "output": { + "shape": "GetEntityRecordsResponse" + }, + "errors": [ + { + "shape": "InvalidInputException" + }, + { + "shape": "GlueEncryptionException" + }, + { + "shape": "FederationSourceException" + }, + { + "shape": "EntityNotFoundException" + }, + { + "shape": "OperationTimeoutException" + }, + { + "shape": "AccessDeniedException" + }, + { + "shape": "ValidationException" + } + ] + }, + "GetJobRun": { + "name": "GetJobRun", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "GetJobRunRequest" + }, + "output": { + "shape": "GetJobRunResponse" + }, + "errors": [ + { + "shape": "InternalServiceException" + }, + { + "shape": "InvalidInputException" + }, + { + "shape": "EntityNotFoundException" + }, + { + "shape": "OperationTimeoutException" + } + ] + }, + "GetJobRuns": { + "name": "GetJobRuns", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "GetJobRunsRequest" + }, + "output": { + "shape": "GetJobRunsResponse" + }, + "errors": [ + { + "shape": "InternalServiceException" + }, + { + "shape": "InvalidInputException" + }, + { + "shape": "EntityNotFoundException" + }, + { + "shape": "OperationTimeoutException" + } + ] + }, + "GetTable": { + "name": "GetTable", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "GetTableRequest" + }, + "output": { + "shape": "GetTableResponse" + }, + "errors": [ + { + "shape": "ResourceNotReadyException" + }, + { + "shape": "FederationSourceRetryableException" + }, + { + "shape": "InternalServiceException" + }, + { + "shape": "InvalidInputException" + }, + { + "shape": "GlueEncryptionException" + }, + { + "shape": "FederationSourceException" + }, + { + "shape": "EntityNotFoundException" + }, + { + "shape": "OperationTimeoutException" + } + ] + }, + "ListConnectionTypes": { + "name": "ListConnectionTypes", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "ListConnectionTypesRequest" + }, + "output": { + "shape": "ListConnectionTypesResponse" + }, + "errors": [ + { + "shape": "InternalServiceException" + }, + { + "shape": "AccessDeniedException" + } + ] + }, + "StartCompletion": { + "name": "StartCompletion", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "StartCompletionRequest" + }, + "output": { + "shape": "StartCompletionResponse" + }, + "errors": [ + { + "shape": "AlreadyExistsException" + }, + { + "shape": "InternalServiceException" + }, + { + "shape": "InvalidInputException" + }, + { + "shape": "EntityNotFoundException" + }, + { + "shape": "OperationTimeoutException" + }, + { + "shape": "AccessDeniedException" + }, + { + "shape": "ValidationException" + } + ] + } + }, + "shapes": { + "AccessDeniedException": { + "type": "structure", + "members": { + "message": { + "shape": "String" + } + }, + "documentation": "

This exception is thrown when the client doesn't have permission for the operation they requested.

", + "exception": true + }, + "AllowedValue": { + "type": "structure", + "required": ["DisplayName", "Description", "Value"], + "members": { + "DisplayName": { + "shape": "AllowedValueDisplayNameString" + }, + "Description": { + "shape": "AllowedValueDescriptionString" + }, + "Value": { + "shape": "AllowedValueValueString" + } + } + }, + "AllowedValueDescriptionString": { + "type": "string", + "max": 1024, + "min": 0 + }, + "AllowedValueDisplayNameString": { + "type": "string", + "max": 128, + "min": 1 + }, + "AllowedValueValueString": { + "type": "string", + "max": 128, + "min": 1 + }, + "AllowedValues": { + "type": "list", + "member": { + "shape": "AllowedValue" + } + }, + "AlreadyExistsException": { + "type": "structure", + "members": { + "message": { + "shape": "String" + } + }, + "documentation": "

This exception occurs when a user submits for an already existing script

", + "exception": true + }, + "ApiVersion": { + "type": "string", + "max": 256, + "min": 1, + "pattern": "[a-zA-Z0-9.-]*" + }, + "ArnString": { + "type": "string", + "max": 2048, + "min": 20 + }, + "AttemptCount": { + "type": "integer", + "box": true + }, + "AttributeCondition": { + "type": "structure", + "members": { + "Expression": { + "shape": "ExpressionString" + }, + "Scope": { + "shape": "ScopeString" + } + } + }, + "AuthConfiguration": { + "type": "structure", + "required": ["AuthenticationType", "SecretArn"], + "members": { + "AuthenticationType": { + "shape": "Property" + }, + "SecretArn": { + "shape": "Property" + }, + "OAuth2Properties": { + "shape": "PropertiesMap" + }, + "BasicAuthenticationProperties": { + "shape": "PropertiesMap" + }, + "CustomAuthenticationProperties": { + "shape": "PropertiesMap" + } + } + }, + "AuthenticationType": { + "type": "string", + "enum": ["BASIC", "OAUTH2", "CUSTOM"] + }, + "AuthenticationTypes": { + "type": "list", + "member": { + "shape": "AuthenticationType" + } + }, + "BlobParametersMap": { + "type": "map", + "key": { + "shape": "KeyString" + }, + "value": { + "shape": "BlobParametersMapValue" + } + }, + "BlobParametersMapValue": { + "type": "blob" + }, + "Bool": { + "type": "boolean", + "box": true + }, + "Boolean": { + "type": "boolean", + "box": true + }, + "BooleanValue": { + "type": "boolean", + "box": true + }, + "Capabilities": { + "type": "structure", + "required": ["SupportedAuthenticationTypes", "SupportedDataOperations", "SupportedComputeEnvironments"], + "members": { + "SupportedAuthenticationTypes": { + "shape": "AuthenticationTypes" + }, + "SupportedDataOperations": { + "shape": "DataOperations" + }, + "SupportedComputeEnvironments": { + "shape": "ComputeEnvironments" + } + } + }, + "Catalog": { + "type": "structure", + "members": { + "CatalogId": { + "shape": "CatalogIdString" + }, + "Name": { + "shape": "CatalogNameString" + }, + "Description": { + "shape": "GlueCommonDescriptionString" + }, + "ResourceArn": { + "shape": "ResourceArnString" + }, + "Parameters": { + "shape": "ParametersMap" + }, + "DataParameters": { + "shape": "BlobParametersMap" + }, + "CatalogType": { + "shape": "CatalogType" + }, + "CreateTime": { + "shape": "Timestamp" + }, + "UpdateTime": { + "shape": "Timestamp" + }, + "TargetCatalog": { + "shape": "TargetCatalog" + }, + "FederatedCatalog": { + "shape": "FederatedCatalog" + }, + "CatalogProperties": { + "shape": "CatalogPropertiesOutput" + }, + "CatalogIdentifier": { + "shape": "CatalogIdentifier" + }, + "ParentCatalogIdentifiers": { + "shape": "CatalogIdentifierList" + }, + "ParentCatalogNames": { + "shape": "CatalogNameList" + }, + "CreateTableDefaultPermissions": { + "shape": "PrincipalPermissionsList" + }, + "CreateDatabaseDefaultPermissions": { + "shape": "PrincipalPermissionsList" + } + } + }, + "CatalogIdString": { + "type": "string", + "max": 255, + "min": 1, + "pattern": ".*[\\u0020-\\uD7FF\\uE000-\\uFFFD\\uD800\\uDC00-\\uDBFF\\uDFFF\\t]*.*" + }, + "CatalogIdentifier": { + "type": "string", + "max": 100, + "min": 0, + "pattern": ".*[\\u0020-\\uD7FF\\uE000-\\uFFFD\\uD800\\uDC00-\\uDBFF\\uDFFF\\t]*.*" + }, + "CatalogIdentifierList": { + "type": "list", + "member": { + "shape": "CatalogIdentifier" + } + }, + "CatalogList": { + "type": "list", + "member": { + "shape": "Catalog" + } + }, + "CatalogNameList": { + "type": "list", + "member": { + "shape": "CatalogNameString" + } + }, + "CatalogNameString": { + "type": "string", + "max": 30, + "min": 1, + "pattern": "(?!(.*[.\\/\\\\]|aws:)).*" + }, + "CatalogPropertiesOutput": { + "type": "structure", + "members": { + "DataLakeAccessProperties": { + "shape": "DataLakeAccessPropertiesOutput" + }, + "IcebergOptimizationProperties": { + "shape": "IcebergOptimizationPropertiesOutput" + } + } + }, + "CatalogType": { + "type": "string", + "enum": [ + "REDSHIFT_CATALOG", + "FEDERATED", + "NATIVE", + "REDSHIFT", + "LINKCONTAINER", + "LINK_FEDERATED", + "LINK_NATIVE", + "LINK_REDSHIFT" + ] + }, + "Column": { + "type": "structure", + "required": ["Name"], + "members": { + "Name": { + "shape": "NameString" + }, + "Type": { + "shape": "TypeString" + }, + "Comment": { + "shape": "CommentString" + }, + "Parameters": { + "shape": "ParametersMap" + } + } + }, + "ColumnList": { + "type": "list", + "member": { + "shape": "Column" + } + }, + "ColumnValueStringList": { + "type": "list", + "member": { + "shape": "ColumnValuesString" + } + }, + "ColumnValuesString": { + "type": "string" + }, + "CommentString": { + "type": "string", + "max": 255, + "min": 0, + "pattern": ".*[\\u0020-\\uD7FF\\uE000-\\uFFFD\\uD800\\uDC00-\\uDBFF\\uDFFF\\t]*.*" + }, + "CompletionIdString": { + "type": "string", + "max": 36, + "min": 36, + "pattern": ".*[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}.*" + }, + "CompletionStatus": { + "type": "string", + "enum": ["SUBMITTED", "SUCCEEDED", "FAILED", "RUNNING", "EXPIRED", "DELETED"] + }, + "CompletionString": { + "type": "string", + "max": 30720, + "min": 1 + }, + "ComputeEnvironment": { + "type": "string", + "enum": ["SPARK", "PYTHON", "ATHENA"] + }, + "ComputeEnvironmentConfiguration": { + "type": "structure", + "required": [ + "Name", + "Description", + "ComputeEnvironment", + "SupportedAuthenticationTypes", + "AdditionalConnectionProperties", + "AdditionalConnectionOptions", + "ConnectionPropertyNameOverrides", + "ConnectionOptionNameOverrides", + "ConnectionPropertyExclusions", + "ConnectionOptionExclusions", + "ConnectionPropertiesRequiredOverrides" + ], + "members": { + "Name": { + "shape": "ComputeEnvironmentName" + }, + "Description": { + "shape": "String" + }, + "ComputeEnvironment": { + "shape": "ComputeEnvironment" + }, + "SupportedAuthenticationTypes": { + "shape": "AuthenticationTypes" + }, + "AdditionalConnectionProperties": { + "shape": "PropertiesMap" + }, + "AdditionalConnectionOptions": { + "shape": "PropertiesMap" + }, + "ConnectionPropertyNameOverrides": { + "shape": "PropertyNameOverrides" + }, + "ConnectionOptionNameOverrides": { + "shape": "PropertyNameOverrides" + }, + "ConnectionPropertyExclusions": { + "shape": "ListOfString" + }, + "ConnectionOptionExclusions": { + "shape": "ListOfString" + }, + "ConnectionPropertiesRequiredOverrides": { + "shape": "ListOfString" + }, + "PhysicalConnectionPropertiesRequired": { + "shape": "Bool" + } + } + }, + "ComputeEnvironmentConfigurationMap": { + "type": "map", + "key": { + "shape": "ComputeEnvironmentName" + }, + "value": { + "shape": "ComputeEnvironmentConfiguration" + } + }, + "ComputeEnvironmentName": { + "type": "string", + "max": 128, + "min": 1 + }, + "ComputeEnvironments": { + "type": "list", + "member": { + "shape": "ComputeEnvironment" + } + }, + "ConditionStatement": { + "type": "map", + "key": { + "shape": "String" + }, + "value": { + "shape": "String" + } + }, + "ConditionStatements": { + "type": "list", + "member": { + "shape": "ConditionStatement" + } + }, + "ConnectionOptions": { + "type": "map", + "key": { + "shape": "OptionKey" + }, + "value": { + "shape": "OptionValue" + } + }, + "ConnectionType": { + "type": "string", + "enum": [ + "JDBC", + "SFTP", + "REDSHIFT", + "ATHENA", + "MONGODB", + "KAFKA", + "NETWORK", + "YARNRESOURCEMANAGER", + "MARKETPLACE", + "HIVE_METASTORE", + "CUSTOM", + "SALESFORCE", + "VIEW_VALIDATION_REDSHIFT", + "VIEW_VALIDATION_ATHENA" + ] + }, + "ConnectionTypeBrief": { + "type": "structure", + "members": { + "ConnectionType": { + "shape": "ConnectionType" + }, + "DisplayName": { + "shape": "DisplayName" + }, + "Vendor": { + "shape": "Vendor" + }, + "Description": { + "shape": "Description" + }, + "Categories": { + "shape": "ListOfString" + }, + "Capabilities": { + "shape": "Capabilities" + }, + "LogoUrl": { + "shape": "UrlString" + }, + "DocumentationUrl": { + "shape": "UrlString" + }, + "ConnectionTypeVariants": { + "shape": "ConnectionTypeVariantList" + } + } + }, + "ConnectionTypeList": { + "type": "list", + "member": { + "shape": "ConnectionTypeBrief" + } + }, + "ConnectionTypeVariant": { + "type": "structure", + "members": { + "ConnectionTypeVariantName": { + "shape": "DisplayName" + }, + "DisplayName": { + "shape": "DisplayName" + }, + "Description": { + "shape": "Description" + }, + "LogoUrl": { + "shape": "UrlString" + }, + "DocumentationUrl": { + "shape": "UrlString" + } + } + }, + "ConnectionTypeVariantList": { + "type": "list", + "member": { + "shape": "ConnectionTypeVariant" + } + }, + "DataAccessModeEnum": { + "type": "string", + "enum": ["LakeFormation", "Hybrid", "Other"] + }, + "DataLakeAccessPropertiesOutput": { + "type": "structure", + "members": { + "DataLakeAccess": { + "shape": "Boolean" + }, + "DataTransferRole": { + "shape": "GlueCommonIAMRoleArn" + }, + "KmsKey": { + "shape": "ResourceArnString" + }, + "ManagedWorkgroupName": { + "shape": "GlueCommonNameString" + }, + "ManagedWorkgroupStatus": { + "shape": "GlueCommonNameString" + }, + "NamespaceArn": { + "shape": "ResourceArnString" + }, + "RedshiftDatabaseName": { + "shape": "GlueCommonNameString" + }, + "StatusMessage": { + "shape": "GlueCommonNameString" + }, + "CatalogType": { + "shape": "GlueCommonNameString" + } + } + }, + "DataLakePrincipal": { + "type": "structure", + "members": { + "DataLakePrincipalIdentifier": { + "shape": "DataLakePrincipalString" + }, + "AttributeCondition": { + "shape": "AttributeCondition" + } + } + }, + "DataLakePrincipalString": { + "type": "string", + "max": 255, + "min": 1 + }, + "DataOperation": { + "type": "string", + "enum": ["READ", "WRITE"] + }, + "DataOperations": { + "type": "list", + "member": { + "shape": "DataOperation" + } + }, + "DataType": { + "type": "string", + "enum": ["STRING", "INTEGER", "BOOLEAN", "STRING_LIST"] + }, + "DatabaseIdString": { + "type": "string", + "max": 100, + "min": 0, + "pattern": ".*[\\u0020-\\uD7FF\\uE000-\\uFFFD\\uD800\\uDC00-\\uDBFF\\uDFFF\\t]*.*" + }, + "DescribeConnectionTypeRequest": { + "type": "structure", + "members": { + "ConnectionType": { + "shape": "NameString" + } + } + }, + "DescribeConnectionTypeResponse": { + "type": "structure", + "members": { + "ConnectionType": { + "shape": "NameString" + }, + "DisplayName": { + "shape": "DisplayName" + }, + "Vendor": { + "shape": "Vendor" + }, + "Description": { + "shape": "Description" + }, + "LogoUrl": { + "shape": "UrlString" + }, + "DocumentationUrl": { + "shape": "UrlString" + }, + "Categories": { + "shape": "ListOfString" + }, + "Capabilities": { + "shape": "Capabilities" + }, + "ConnectionProperties": { + "shape": "PropertiesMap" + }, + "SparkConnectionProperties": { + "shape": "PropertiesMap" + }, + "AthenaConnectionProperties": { + "shape": "PropertiesMap" + }, + "ConnectionOptions": { + "shape": "PropertiesMap" + }, + "AuthenticationConfiguration": { + "shape": "AuthConfiguration" + }, + "ComputeEnvironmentConfigurations": { + "shape": "ComputeEnvironmentConfigurationMap" + }, + "PhysicalConnectionRequirements": { + "shape": "PropertiesMap" + } + } + }, + "Description": { + "type": "string", + "max": 1024, + "min": 0 + }, + "DescriptionErrorString": { + "type": "string", + "max": 400000, + "min": 0 + }, + "DescriptionString": { + "type": "string", + "max": 2048, + "min": 0, + "pattern": ".*[\\u0020-\\uD7FF\\uE000-\\uFFFD\\uD800\\uDC00-\\uDBFF\\uDFFF\\r\\n\\t]*.*" + }, + "DisplayName": { + "type": "string", + "max": 128, + "min": 1 + }, + "EntityFieldName": { + "type": "string" + }, + "EntityName": { + "type": "string" + }, + "EntityNotFoundException": { + "type": "structure", + "members": { + "message": { + "shape": "String" + }, + "fromFederationSource": { + "shape": "NullableBoolean" + } + }, + "documentation": "

This exception is thrown when the requested entity is not found in the server side.

", + "exception": true + }, + "ErrorDetail": { + "type": "structure", + "members": { + "ErrorCode": { + "shape": "NameString" + }, + "ErrorMessage": { + "shape": "DescriptionString" + } + } + }, + "ExecutionClass": { + "type": "string", + "enum": ["FLEX", "STANDARD"] + }, + "ExecutionTime": { + "type": "integer", + "box": true + }, + "ExpressionString": { + "type": "string" + }, + "FederatedCatalog": { + "type": "structure", + "members": { + "Identifier": { + "shape": "GlueCommonFederationIdentifier" + }, + "ConnectionName": { + "shape": "GlueCommonNameString" + } + } + }, + "FederatedTable": { + "type": "structure", + "members": { + "Identifier": { + "shape": "FederationIdentifier" + }, + "DatabaseIdentifier": { + "shape": "FederationIdentifier" + }, + "ProfileName": { + "shape": "NameString" + }, + "ConnectionName": { + "shape": "NameString" + }, + "ConnectionType": { + "shape": "NameString" + } + } + }, + "FederationIdentifier": { + "type": "string", + "max": 512, + "min": 1, + "pattern": ".*[\\u0020-\\uD7FF\\uE000-\\uFFFD\\uD800\\uDC00-\\uDBFF\\uDFFF\\t]*.*" + }, + "FederationSourceException": { + "type": "structure", + "members": { + "message": { + "shape": "String" + } + }, + "exception": true + }, + "FederationSourceRetryableException": { + "type": "structure", + "members": { + "message": { + "shape": "String" + } + }, + "exception": true + }, + "FilterPredicate": { + "type": "string", + "max": 2048, + "min": 1, + "pattern": "[\\u0020-\\uD7FF\\uE000-\\uFFFD\\uD800\\uDC00-\\uDBFF\\uDFFF\\r\\n\\t]*" + }, + "FormatString": { + "type": "string", + "max": 128, + "min": 0, + "pattern": ".*[\\u0020-\\uD7FF\\uE000-\\uFFFD\\uD800\\uDC00-\\uDBFF\\uDFFF\\t]*.*" + }, + "GenericMap": { + "type": "map", + "key": { + "shape": "GenericString" + }, + "value": { + "shape": "GenericString" + } + }, + "GenericString": { + "type": "string" + }, + "GetCatalogRequest": { + "type": "structure", + "required": ["Name"], + "members": { + "Name": { + "shape": "CatalogNameString" + }, + "ParentCatalogId": { + "shape": "CatalogIdString" + }, + "CatalogId": { + "shape": "CatalogIdString" + }, + "CatalogIdentifier": { + "shape": "CatalogIdentifier" + }, + "ContextMap": { + "shape": "RequestContextMap" + }, + "FederateToSource": { + "shape": "Boolean" + } + } + }, + "GetCatalogResponse": { + "type": "structure", + "required": ["Catalog"], + "members": { + "Catalog": { + "shape": "Catalog" + }, + "DataParameters": { + "shape": "BlobParametersMap" + } + } + }, + "GetCatalogsRequest": { + "type": "structure", + "members": { + "ParentCatalogId": { + "shape": "CatalogIdString" + }, + "NextToken": { + "shape": "NextToken" + }, + "MaxResults": { + "shape": "PageSize" + }, + "Recursive": { + "shape": "NullableBoolean" + }, + "ContextMap": { + "shape": "RequestContextMap" + } + } + }, + "GetCatalogsResponse": { + "type": "structure", + "required": ["CatalogList"], + "members": { + "CatalogList": { + "shape": "CatalogList" + }, + "NextToken": { + "shape": "NextToken" + } + } + }, + "GetCompletionRequest": { + "type": "structure", + "required": ["CompletionId"], + "members": { + "CompletionId": { + "shape": "CompletionIdString" + } + } + }, + "GetCompletionResponse": { + "type": "structure", + "required": ["CompletionId", "LastModifiedOn", "Status"], + "members": { + "CompletionId": { + "shape": "CompletionIdString" + }, + "StartedOn": { + "shape": "startedOn" + }, + "LastModifiedOn": { + "shape": "lastModifiedOn" + }, + "ErrorMessage": { + "shape": "HashString" + }, + "CompletedOn": { + "shape": "completedOn" + }, + "Status": { + "shape": "CompletionStatus" + }, + "Completion": { + "shape": "CompletionString" + }, + "SourceURLs": { + "shape": "SourceUrlList" + }, + "Tags": { + "shape": "TagsMap" + } + } + }, + "GetEntityRecordsRequest": { + "type": "structure", + "required": ["EntityName", "Limit"], + "members": { + "EntityName": { + "shape": "EntityName" + }, + "Limit": { + "shape": "Limit" + }, + "ConnectionName": { + "shape": "NameString" + }, + "CatalogId": { + "shape": "CatalogIdString" + }, + "NextToken": { + "shape": "NextToken" + }, + "DataStoreApiVersion": { + "shape": "ApiVersion" + }, + "ConnectionOptions": { + "shape": "ConnectionOptions" + }, + "FilterPredicate": { + "shape": "FilterPredicate" + }, + "OrderBy": { + "shape": "String" + }, + "SelectedFields": { + "shape": "SelectedFields" + }, + "StagingConfiguration": { + "shape": "StagingConfiguration" + } + } + }, + "GetEntityRecordsResponse": { + "type": "structure", + "members": { + "Records": { + "shape": "Records" + }, + "NextToken": { + "shape": "NextToken" + } + } + }, + "GetJobRunRequest": { + "type": "structure", + "required": ["JobName", "RunId"], + "members": { + "JobName": { + "shape": "NameString" + }, + "RunId": { + "shape": "IdString" + }, + "PredecessorsIncluded": { + "shape": "BooleanValue" + } + } + }, + "GetJobRunResponse": { + "type": "structure", + "members": { + "JobRun": { + "shape": "JobRun" + } + } + }, + "GetJobRunsRequest": { + "type": "structure", + "required": ["JobName"], + "members": { + "JobName": { + "shape": "NameString" + }, + "NextToken": { + "shape": "OrchestrationToken" + }, + "MaxResults": { + "shape": "OrchestrationPageSize200" + } + } + }, + "GetJobRunsResponse": { + "type": "structure", + "members": { + "JobRuns": { + "shape": "JobRunList" + }, + "NextToken": { + "shape": "OrchestrationToken" + } + } + }, + "GetTableRequest": { + "type": "structure", + "required": ["DatabaseName", "Name"], + "members": { + "DatabaseName": { + "shape": "NameString" + }, + "Name": { + "shape": "NameString" + }, + "CatalogId": { + "shape": "CatalogIdString" + }, + "TransactionId": { + "shape": "TransactionIdString" + }, + "QueryAsOfTime": { + "shape": "Timestamp" + }, + "IncludeAccessMode": { + "shape": "NullableBoolean" + }, + "IncludeStatusDetails": { + "shape": "NullableBoolean" + }, + "AttributesToGet": { + "shape": "TableAttributesList" + }, + "CatalogIdentifier": { + "shape": "CatalogIdentifier" + }, + "DatabaseIdentifier": { + "shape": "DatabaseIdString" + }, + "TableIdentifier": { + "shape": "TableIdString" + }, + "ContextMap": { + "shape": "RequestContextMap" + } + } + }, + "GetTableResponse": { + "type": "structure", + "members": { + "Table": { + "shape": "Table" + }, + "UseAdvancedFiltering": { + "shape": "NullableBoolean" + } + } + }, + "GlueCommonDescriptionString": { + "type": "string", + "max": 2048, + "min": 1, + "pattern": ".*[\\u0020-\\uD7FF\\uE000-\\uFFFD\\uD800\\uDC00-\\uDBFF\\uDFFF\\t]*.*" + }, + "GlueCommonFederationIdentifier": { + "type": "string", + "max": 512, + "min": 1, + "pattern": ".*[\\u0020-\\uD7FF\\uE000-\\uFFFD\\uD800\\uDC00-\\uDBFF\\uDFFF\\t]*.*" + }, + "GlueCommonIAMRoleArn": { + "type": "string", + "pattern": "arn:aws(-(cn|us-gov|iso(-[bef])?))?:iam::[0-9]{12}:role/.+.*" + }, + "GlueCommonNameString": { + "type": "string", + "max": 155, + "min": 1, + "pattern": ".*[\\u0020-\\uD7FF\\uE000-\\uFFFD\\uD800\\uDC00-\\uDBFF\\uDFFF\\t]*.*" + }, + "GlueEncryptionException": { + "type": "structure", + "members": { + "message": { + "shape": "String" + } + }, + "exception": true + }, + "GlueResourceArn": { + "type": "string", + "pattern": ".*arn:aws(-(cn|us-gov|iso(-[bef])?))?:glue:.*" + }, + "GlueVersionString": { + "type": "string", + "max": 255, + "min": 1, + "pattern": "(\\w+\\.)+\\w+" + }, + "HashString": { + "type": "string", + "max": 255, + "min": 1 + }, + "IcebergOptimizationPropertiesOutput": { + "type": "structure", + "members": { + "RoleArn": { + "shape": "GlueCommonIAMRoleArn" + }, + "Compaction": { + "shape": "ParametersMap" + }, + "Retention": { + "shape": "ParametersMap" + }, + "OrphanFileDeletion": { + "shape": "ParametersMap" + }, + "LastUpdatedTime": { + "shape": "Timestamp" + } + } + }, + "IdString": { + "type": "string", + "max": 255, + "min": 1, + "pattern": ".*[\\u0020-\\uD7FF\\uE000-\\uFFFD\\uD800\\uDC00-\\uDBFF\\uDFFF\\t]*.*" + }, + "Integer": { + "type": "integer", + "box": true + }, + "IntegerFlag": { + "type": "integer", + "box": true, + "max": 1, + "min": 0 + }, + "IntegerValue": { + "type": "integer", + "box": true + }, + "InternalServiceException": { + "type": "structure", + "members": { + "message": { + "shape": "String" + } + }, + "documentation": "

This exception is thrown when a call fails due to internal error.

", + "exception": true, + "fault": true + }, + "InvalidInputException": { + "type": "structure", + "members": { + "message": { + "shape": "String" + }, + "fromFederationSource": { + "shape": "NullableBoolean" + } + }, + "documentation": "

This exception is thrown when the format of the input is incorrect.

", + "exception": true + }, + "JobMode": { + "type": "string", + "enum": ["SCRIPT", "VISUAL", "NOTEBOOK"] + }, + "JobRun": { + "type": "structure", + "members": { + "Id": { + "shape": "IdString" + }, + "Attempt": { + "shape": "AttemptCount" + }, + "PreviousRunId": { + "shape": "IdString" + }, + "TriggerName": { + "shape": "NameString" + }, + "JobName": { + "shape": "NameString" + }, + "JobMode": { + "shape": "JobMode" + }, + "JobRunQueuingEnabled": { + "shape": "NullableBoolean" + }, + "StartedOn": { + "shape": "TimestampValue" + }, + "LastModifiedOn": { + "shape": "TimestampValue" + }, + "CompletedOn": { + "shape": "TimestampValue" + }, + "JobRunState": { + "shape": "JobRunState" + }, + "Arguments": { + "shape": "GenericMap" + }, + "ErrorMessage": { + "shape": "DescriptionErrorString" + }, + "PredecessorRuns": { + "shape": "PredecessorList" + }, + "AllocatedCapacity": { + "shape": "IntegerValue" + }, + "ExecutionTime": { + "shape": "ExecutionTime" + }, + "Timeout": { + "shape": "Timeout" + }, + "MaxCapacity": { + "shape": "NullableDouble" + }, + "WorkerType": { + "shape": "WorkerType" + }, + "NumberOfWorkers": { + "shape": "NullableInteger" + }, + "SecurityConfiguration": { + "shape": "NameString" + }, + "LogGroupName": { + "shape": "LogGroupString" + }, + "NotificationProperty": { + "shape": "NotificationProperty" + }, + "GlueVersion": { + "shape": "GlueVersionString" + }, + "ExecutionClass": { + "shape": "ExecutionClass" + }, + "MinFlexWorkers": { + "shape": "NullableInteger" + }, + "DPUSeconds": { + "shape": "NullableDouble" + }, + "ExecutionArguments": { + "shape": "GenericMap" + }, + "ProfileName": { + "shape": "NameString" + }, + "StateDetail": { + "shape": "OrchestrationMessageString" + }, + "MaintenanceWindow": { + "shape": "MaintenanceWindow" + }, + "UpgradeAnalysisMetadata": { + "shape": "UpgradeAnalysisMetadata" + } + } + }, + "JobRunList": { + "type": "list", + "member": { + "shape": "JobRun" + } + }, + "JobRunState": { + "type": "string", + "enum": [ + "STARTING", + "RUNNING", + "STOPPING", + "STOPPED", + "SUCCEEDED", + "FAILED", + "TIMEOUT", + "ERROR", + "WAITING", + "EXPIRED" + ] + }, + "KeyString": { + "type": "string", + "max": 255, + "min": 1, + "pattern": ".*[\\u0020-\\uD7FF\\uE000-\\uFFFD\\uD800\\uDC00-\\uDBFF\\uDFFF\\t]*.*" + }, + "LakeFormationPermissionEnforcedEnum": { + "type": "string", + "enum": ["AllUsers", "SomeUsers", "NoUser"] + }, + "Limit": { + "type": "long", + "box": true, + "max": 1000, + "min": 1 + }, + "ListConnectionTypesRequest": { + "type": "structure", + "members": { + "MaxResults": { + "shape": "PageSize" + }, + "NextToken": { + "shape": "NextToken" + } + } + }, + "ListConnectionTypesResponse": { + "type": "structure", + "members": { + "ConnectionTypes": { + "shape": "ConnectionTypeList" + }, + "NextToken": { + "shape": "NextToken" + } + } + }, + "ListOfString": { + "type": "list", + "member": { + "shape": "String" + } + }, + "LocationMap": { + "type": "map", + "key": { + "shape": "ColumnValuesString" + }, + "value": { + "shape": "ColumnValuesString" + } + }, + "LocationString": { + "type": "string", + "max": 2056, + "min": 0, + "pattern": ".*[\\u0020-\\uD7FF\\uE000-\\uFFFD\\uD800\\uDC00-\\uDBFF\\uDFFF\\r\\n\\t]*.*" + }, + "LocationStringList": { + "type": "list", + "member": { + "shape": "LocationString" + } + }, + "LogGroupString": { + "type": "string", + "max": 400000, + "min": 0 + }, + "MaintenanceWindow": { + "type": "string", + "pattern": "(Sun|Mon|Tue|Wed|Thu|Fri|Sat):([01]?[0-9]|2[0-3])" + }, + "Maximum": { + "type": "integer", + "box": true + }, + "Minimum": { + "type": "integer", + "box": true + }, + "NameString": { + "type": "string", + "max": 255, + "min": 1, + "pattern": ".*[\\u0020-\\uD7FF\\uE000-\\uFFFD\\uD800\\uDC00-\\uDBFF\\uDFFF\\t]*.*" + }, + "NameStringList": { + "type": "list", + "member": { + "shape": "NameString" + } + }, + "NextToken": { + "type": "string" + }, + "NonNegativeInteger": { + "type": "integer", + "box": true, + "min": 0 + }, + "NotificationProperty": { + "type": "structure", + "members": { + "NotifyDelayAfter": { + "shape": "NotifyDelayAfter" + } + } + }, + "NotifyDelayAfter": { + "type": "integer", + "box": true, + "min": 1 + }, + "NullableBoolean": { + "type": "boolean", + "box": true + }, + "NullableDouble": { + "type": "double", + "box": true + }, + "NullableInteger": { + "type": "integer", + "box": true + }, + "OperationTimeoutException": { + "type": "structure", + "members": { + "message": { + "shape": "String" + } + }, + "documentation": "

This exception occurs when the server throws a timeout

", + "exception": true + }, + "OptionKey": { + "type": "string", + "max": 256, + "min": 1, + "pattern": "[\\w]*" + }, + "OptionValue": { + "type": "string", + "max": 256, + "min": 1, + "pattern": "[\\S]*" + }, + "OrchestrationMessageString": { + "type": "string", + "max": 400000, + "min": 0 + }, + "OrchestrationPageSize200": { + "type": "integer", + "box": true, + "max": 200, + "min": 1 + }, + "OrchestrationToken": { + "type": "string", + "max": 400000, + "min": 0 + }, + "Order": { + "type": "structure", + "required": ["Column", "SortOrder"], + "members": { + "Column": { + "shape": "NameString" + }, + "SortOrder": { + "shape": "IntegerFlag" + } + } + }, + "OrderList": { + "type": "list", + "member": { + "shape": "Order" + } + }, + "OutputLocation": { + "type": "string" + }, + "PageSize": { + "type": "integer", + "box": true, + "max": 1000, + "min": 1 + }, + "ParametersMap": { + "type": "map", + "key": { + "shape": "KeyString" + }, + "value": { + "shape": "ParametersMapValue" + }, + "max": 50, + "min": 0 + }, + "ParametersMapValue": { + "type": "string", + "max": 512000, + "min": 0 + }, + "Permission": { + "type": "string", + "enum": [ + "ALL", + "SELECT", + "ALTER", + "DROP", + "DELETE", + "INSERT", + "DESCRIBE", + "CREATE_DATABASE", + "CREATE_TABLE", + "DATA_LOCATION_ACCESS", + "READ", + "WRITE", + "CREATE_LF_TAG", + "ASSOCIATE", + "UPDATE", + "GRANT_WITH_LF_TAG_EXPRESSION", + "CREATE_LF_TAG_EXPRESSION" + ] + }, + "PermissionList": { + "type": "list", + "member": { + "shape": "Permission" + } + }, + "Phase": { + "type": "string", + "enum": ["AUTHENTICATION", "CONNECTION_CREATION"] + }, + "Predecessor": { + "type": "structure", + "members": { + "JobName": { + "shape": "NameString" + }, + "RunId": { + "shape": "IdString" + } + } + }, + "PredecessorList": { + "type": "list", + "member": { + "shape": "Predecessor" + } + }, + "PrimitiveInteger": { + "type": "integer", + "box": true + }, + "PrincipalPermissions": { + "type": "structure", + "members": { + "Principal": { + "shape": "DataLakePrincipal" + }, + "Permissions": { + "shape": "PermissionList" + } + } + }, + "PrincipalPermissionsList": { + "type": "list", + "member": { + "shape": "PrincipalPermissions" + } + }, + "PromptString": { + "type": "string", + "max": 30720, + "min": 1 + }, + "PropertiesMap": { + "type": "map", + "key": { + "shape": "PropertyName" + }, + "value": { + "shape": "Property" + } + }, + "Property": { + "type": "structure", + "members": { + "Name": { + "shape": "PropertyName" + }, + "DisplayName": { + "shape": "PropertyName" + }, + "Description": { + "shape": "PropertyDescriptionString" + }, + "DataType": { + "shape": "DataType" + }, + "Required": { + "shape": "Bool" + }, + "ConditionallyRequired": { + "shape": "ConditionStatements" + }, + "DefaultValue": { + "shape": "String" + }, + "Phase": { + "shape": "Phase" + }, + "PropertyTypes": { + "shape": "PropertyTypes" + }, + "AllowedValues": { + "shape": "AllowedValues" + }, + "Validations": { + "shape": "Validations" + }, + "DataOperationScopes": { + "shape": "DataOperations" + }, + "Order": { + "shape": "PrimitiveInteger" + }, + "DocumentationUrl": { + "shape": "String" + }, + "Reference": { + "shape": "String" + }, + "Format": { + "shape": "String" + } + } + }, + "PropertyDescriptionString": { + "type": "string", + "max": 1024, + "min": 0 + }, + "PropertyName": { + "type": "string", + "max": 128, + "min": 1 + }, + "PropertyNameOverrides": { + "type": "map", + "key": { + "shape": "PropertyName" + }, + "value": { + "shape": "PropertyName" + } + }, + "PropertyType": { + "type": "string", + "enum": ["USER_INPUT", "SECRET", "READ_ONLY", "UNUSED"] + }, + "PropertyTypes": { + "type": "list", + "member": { + "shape": "PropertyType" + } + }, + "Record": { + "type": "structure", + "members": {}, + "document": true, + "sensitive": true + }, + "Records": { + "type": "list", + "member": { + "shape": "Record" + } + }, + "RequestContextKey": { + "type": "string", + "max": 1024, + "min": 1 + }, + "RequestContextMap": { + "type": "map", + "key": { + "shape": "RequestContextKey" + }, + "value": { + "shape": "RequestContextValue" + }, + "max": 50, + "min": 0 + }, + "RequestContextValue": { + "type": "string", + "max": 10240, + "min": 0 + }, + "ResourceAction": { + "type": "string", + "enum": ["CREATE", "UPDATE"] + }, + "ResourceArnString": { + "type": "string" + }, + "ResourceNotReadyException": { + "type": "structure", + "members": { + "message": { + "shape": "String" + } + }, + "exception": true + }, + "ResourceState": { + "type": "string", + "enum": ["QUEUED", "IN_PROGRESS", "SUCCESS", "STOPPED", "FAILED"] + }, + "SchemaId": { + "type": "structure", + "members": { + "SchemaArn": { + "shape": "GlueResourceArn" + }, + "SchemaName": { + "shape": "SchemaRegistryNameString" + }, + "RegistryName": { + "shape": "SchemaRegistryNameString" + } + } + }, + "SchemaReference": { + "type": "structure", + "members": { + "SchemaId": { + "shape": "SchemaId" + }, + "SchemaVersionId": { + "shape": "SchemaVersionIdString" + }, + "SchemaVersionNumber": { + "shape": "VersionLongNumber" + } + } + }, + "SchemaRegistryNameString": { + "type": "string", + "max": 255, + "min": 1, + "pattern": ".*[a-zA-Z0-9-_$#.]+.*" + }, + "SchemaVersionIdString": { + "type": "string", + "max": 36, + "min": 36, + "pattern": ".*[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}.*" + }, + "ScopeString": { + "type": "string", + "max": 25, + "min": 25 + }, + "ScriptLocationString": { + "type": "string", + "max": 400000, + "min": 0 + }, + "SelectedFields": { + "type": "list", + "member": { + "shape": "EntityFieldName" + } + }, + "SerDeInfo": { + "type": "structure", + "members": { + "Name": { + "shape": "NameString" + }, + "SerializationLibrary": { + "shape": "NameString" + }, + "Parameters": { + "shape": "ParametersMap" + } + } + }, + "SkewedInfo": { + "type": "structure", + "members": { + "SkewedColumnNames": { + "shape": "NameStringList" + }, + "SkewedColumnValues": { + "shape": "ColumnValueStringList" + }, + "SkewedColumnValueLocationMaps": { + "shape": "LocationMap" + } + } + }, + "SourceUrlList": { + "type": "list", + "member": { + "shape": "HashString" + }, + "max": 3, + "min": 1 + }, + "StagingConfiguration": { + "type": "structure", + "members": { + "OutputLocation": { + "shape": "OutputLocation" + } + } + }, + "StartCompletionContext": { + "type": "list", + "member": { + "shape": "StartCompletionContextItem" + } + }, + "StartCompletionContextItem": { + "type": "map", + "key": { + "shape": "HashString" + }, + "value": { + "shape": "HashString" + } + }, + "StartCompletionRequest": { + "type": "structure", + "required": ["Prompt"], + "members": { + "Prompt": { + "shape": "PromptString" + }, + "Tags": { + "shape": "TagsMap" + }, + "Context": { + "shape": "StartCompletionContext" + } + } + }, + "StartCompletionResponse": { + "type": "structure", + "required": ["CompletionId", "ConversationId"], + "members": { + "CompletionId": { + "shape": "CompletionIdString" + }, + "ConversationId": { + "shape": "CompletionIdString" + } + } + }, + "StatusDetails": { + "type": "structure", + "members": { + "RequestedChange": { + "shape": "Table" + }, + "ViewValidations": { + "shape": "ViewValidationList" + } + } + }, + "StorageDescriptor": { + "type": "structure", + "members": { + "Columns": { + "shape": "ColumnList" + }, + "Location": { + "shape": "LocationString" + }, + "AdditionalLocations": { + "shape": "LocationStringList" + }, + "InputFormat": { + "shape": "FormatString" + }, + "OutputFormat": { + "shape": "FormatString" + }, + "Compressed": { + "shape": "Boolean" + }, + "NumberOfBuckets": { + "shape": "Integer" + }, + "SerDeInfo": { + "shape": "SerDeInfo" + }, + "BucketColumns": { + "shape": "NameStringList" + }, + "SortColumns": { + "shape": "OrderList" + }, + "Parameters": { + "shape": "ParametersMap" + }, + "SkewedInfo": { + "shape": "SkewedInfo" + }, + "StoredAsSubDirectories": { + "shape": "Boolean" + }, + "SchemaReference": { + "shape": "SchemaReference" + } + } + }, + "String": { + "type": "string" + }, + "Table": { + "type": "structure", + "required": ["Name"], + "members": { + "Name": { + "shape": "NameString" + }, + "DatabaseName": { + "shape": "NameString" + }, + "Description": { + "shape": "DescriptionString" + }, + "Owner": { + "shape": "NameString" + }, + "CreateTime": { + "shape": "Timestamp" + }, + "UpdateTime": { + "shape": "Timestamp" + }, + "LastAccessTime": { + "shape": "Timestamp" + }, + "LastAnalyzedTime": { + "shape": "Timestamp" + }, + "Retention": { + "shape": "NonNegativeInteger" + }, + "StorageDescriptor": { + "shape": "StorageDescriptor" + }, + "PartitionKeys": { + "shape": "ColumnList" + }, + "ViewOriginalText": { + "shape": "ViewTextString" + }, + "ViewExpandedText": { + "shape": "ViewTextString" + }, + "TableType": { + "shape": "TableTypeString" + }, + "Parameters": { + "shape": "ParametersMap" + }, + "DataParameters": { + "shape": "BlobParametersMap" + }, + "CreatedBy": { + "shape": "NameString" + }, + "IsRegisteredWithLakeFormation": { + "shape": "Boolean" + }, + "LakeFormationPermissionEnforced": { + "shape": "LakeFormationPermissionEnforcedEnum" + }, + "DataAccessMode": { + "shape": "DataAccessModeEnum" + }, + "TargetTable": { + "shape": "TableIdentifier" + }, + "FederatedTable": { + "shape": "FederatedTable" + }, + "CatalogId": { + "shape": "CatalogIdString" + }, + "IsRowFilteringEnabled": { + "shape": "Boolean" + }, + "VersionId": { + "shape": "VersionString" + }, + "CatalogIdentifier": { + "shape": "CatalogIdentifier" + }, + "TableId": { + "shape": "TableIdString" + }, + "DatabaseId": { + "shape": "DatabaseIdString" + }, + "ViewDefinition": { + "shape": "ViewDefinition" + }, + "DataProvider": { + "shape": "NameString" + }, + "IsMultiDialectView": { + "shape": "Boolean" + }, + "Status": { + "shape": "TableStatus" + } + } + }, + "TableAttributes": { + "type": "string", + "enum": ["NAME", "VERSION_ID", "DATA_ACCESS_MODE", "DEFAULT", "ALL", "TABLE_TYPE", "DESCRIPTION"] + }, + "TableAttributesList": { + "type": "list", + "member": { + "shape": "TableAttributes" + } + }, + "TableIdString": { + "type": "string", + "max": 100, + "min": 0, + "pattern": ".*[\\u0020-\\uD7FF\\uE000-\\uFFFD\\uD800\\uDC00-\\uDBFF\\uDFFF\\t]*.*" + }, + "TableIdentifier": { + "type": "structure", + "members": { + "CatalogId": { + "shape": "CatalogIdString" + }, + "DatabaseName": { + "shape": "NameString" + }, + "Name": { + "shape": "NameString" + }, + "Region": { + "shape": "NameString" + }, + "DatabaseId": { + "shape": "DatabaseIdString" + } + } + }, + "TableStatus": { + "type": "structure", + "members": { + "RequestedBy": { + "shape": "NameString" + }, + "UpdatedBy": { + "shape": "NameString" + }, + "RequestTime": { + "shape": "Timestamp" + }, + "UpdateTime": { + "shape": "Timestamp" + }, + "Action": { + "shape": "ResourceAction" + }, + "State": { + "shape": "ResourceState" + }, + "Error": { + "shape": "ErrorDetail" + }, + "Details": { + "shape": "StatusDetails" + } + } + }, + "TableTypeString": { + "type": "string", + "max": 255, + "min": 0 + }, + "TagKey": { + "type": "string", + "max": 128, + "min": 1 + }, + "TagValue": { + "type": "string", + "max": 256, + "min": 0 + }, + "TagsMap": { + "type": "map", + "key": { + "shape": "TagKey" + }, + "value": { + "shape": "TagValue" + }, + "max": 50, + "min": 0 + }, + "TargetCatalog": { + "type": "structure", + "members": { + "CatalogArn": { + "shape": "ResourceArnString" + }, + "CatalogIdentifier": { + "shape": "CatalogIdentifier" + }, + "AutoDiscovery": { + "shape": "Boolean" + } + } + }, + "Timeout": { + "type": "integer", + "box": true + }, + "Timestamp": { + "type": "timestamp" + }, + "TimestampValue": { + "type": "timestamp" + }, + "TransactionIdString": { + "type": "string", + "max": 255, + "min": 1, + "pattern": ".*[\\p{L}\\p{N}\\p{P}]*.*" + }, + "TypeString": { + "type": "string", + "max": 20000, + "min": 0, + "pattern": ".*[\\u0020-\\uD7FF\\uE000-\\uFFFD\\uD800\\uDC00-\\uDBFF\\uDFFF\\t]*.*" + }, + "UpgradeAnalysisMetadata": { + "type": "structure", + "members": { + "ValidationJobRunId": { + "shape": "NameString" + }, + "GlueVersion": { + "shape": "NameString" + }, + "ScriptLocation": { + "shape": "ScriptLocationString" + }, + "AnalysisId": { + "shape": "IdString" + } + } + }, + "UrlString": { + "type": "string" + }, + "Validation": { + "type": "structure", + "members": { + "ValidationType": { + "shape": "ValidationType" + }, + "Patterns": { + "shape": "ListOfString" + }, + "Description": { + "shape": "ValidationDescriptionString" + }, + "MaxLength": { + "shape": "Maximum" + }, + "Maximum": { + "shape": "Maximum" + }, + "Minimum": { + "shape": "Minimum" + } + } + }, + "ValidationDescriptionString": { + "type": "string", + "max": 1024, + "min": 0 + }, + "ValidationDryRunOpts": { + "type": "structure", + "members": { + "SerializedMockEngineResult": { + "shape": "String" + }, + "ErrorMessage": { + "shape": "String" + }, + "MinimumReceiveCount": { + "shape": "Integer" + } + } + }, + "ValidationException": { + "type": "structure", + "members": { + "message": { + "shape": "String" + } + }, + "documentation": "

This exception occurs when the dag cannot be successfully validated

", + "exception": true + }, + "ValidationType": { + "type": "string", + "enum": ["REGEX", "RANGE"] + }, + "Validations": { + "type": "list", + "member": { + "shape": "Validation" + } + }, + "Vendor": { + "type": "string", + "max": 128, + "min": 1 + }, + "VersionLongNumber": { + "type": "long", + "box": true, + "max": 100000, + "min": 1 + }, + "VersionString": { + "type": "string", + "max": 255, + "min": 1, + "pattern": ".*[\\u0020-\\uD7FF\\uE000-\\uFFFD\\uD800\\uDC00-\\uDBFF\\uDFFF\\t]*.*" + }, + "ViewDefinition": { + "type": "structure", + "members": { + "IsProtected": { + "shape": "Boolean" + }, + "Definer": { + "shape": "ArnString" + }, + "SubObjects": { + "shape": "ViewSubObjectsList" + }, + "Representations": { + "shape": "ViewRepresentationList" + } + } + }, + "ViewDialect": { + "type": "string", + "enum": ["REDSHIFT", "ATHENA", "SPARK"] + }, + "ViewDialectVersionString": { + "type": "string", + "max": 255, + "min": 1, + "pattern": ".*[a-zA-Z0-9_.-]+.*" + }, + "ViewRepresentation": { + "type": "structure", + "members": { + "Dialect": { + "shape": "ViewDialect" + }, + "DialectVersion": { + "shape": "ViewDialectVersionString" + }, + "ViewOriginalText": { + "shape": "ViewTextString" + }, + "ViewExpandedText": { + "shape": "ViewTextString" + }, + "ValidationConnection": { + "shape": "NameString" + }, + "IsStale": { + "shape": "Boolean" + }, + "ValidationDryRunOpts": { + "shape": "ValidationDryRunOpts" + } + } + }, + "ViewRepresentationList": { + "type": "list", + "member": { + "shape": "ViewRepresentation" + }, + "max": 1000, + "min": 1 + }, + "ViewSubObjectsList": { + "type": "list", + "member": { + "shape": "ArnString" + }, + "max": 10, + "min": 0 + }, + "ViewTextString": { + "type": "string", + "max": 409600, + "min": 0 + }, + "ViewValidation": { + "type": "structure", + "members": { + "Dialect": { + "shape": "ViewDialect" + }, + "DialectVersion": { + "shape": "ViewDialectVersionString" + }, + "ViewValidationText": { + "shape": "ViewTextString" + }, + "UpdateTime": { + "shape": "Timestamp" + }, + "State": { + "shape": "ResourceState" + }, + "Error": { + "shape": "ErrorDetail" + } + } + }, + "ViewValidationList": { + "type": "list", + "member": { + "shape": "ViewValidation" + } + }, + "WorkerType": { + "type": "string", + "enum": ["Standard", "G_1X", "G_2X", "G_4X", "G_8X", "G_025X", "Z_2X"] + }, + "completedOn": { + "type": "long", + "box": true + }, + "lastModifiedOn": { + "type": "long", + "box": true + }, + "startedOn": { + "type": "long", + "box": true + } + } +} 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..d86c3904a07 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/shared/client/s3Client.ts @@ -0,0 +1,147 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { S3 } 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() + + 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 || 'root'}`) + + 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 + } + } + + /** + * 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..5513f139d2b --- /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() + + 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/smusUtils.ts b/packages/core/src/sagemakerunifiedstudio/shared/smusUtils.ts new file mode 100644 index 00000000000..35858f0dc5a --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/shared/smusUtils.ts @@ -0,0 +1,416 @@ +/*! + * 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' + +/** + * 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', +} 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 + +/** + * 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() + + /** + * 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(`SMUS Auth: 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('SMUS Auth: 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('SMUS Auth: 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 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() + + try { + logger.debug('SMUS: 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') + } +} diff --git a/packages/core/src/sagemakerunifiedstudio/shared/telemetry.ts b/packages/core/src/sagemakerunifiedstudio/shared/telemetry.ts new file mode 100644 index 00000000000..ceeb4828b83 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/shared/telemetry.ts @@ -0,0 +1,122 @@ +/*! + * 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, DataZoneClient } from './client/datazoneClient' + +/** + * Records space telemetry + */ +export async function recordSpaceTelemetry( + span: Span | Span, + node: SagemakerUnifiedStudioSpaceNode +) { + const logger = getLogger() + + 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 DataZoneClient.getInstance(authProvider) + const toolingEnv = await dzClient.getToolingEnvironment(projectId) + projectRegion = toolingEnv.awsAccountRegion + } + + span.record({ + smusSpaceKey: node.resource.DomainSpaceKey, + smusDomainRegion: node.resource.regionCode, + smusDomainId: parent?.getAuthProvider()?.activeConnection?.domainId, + 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() + + span.record({ + 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() + + try { + const isInSmusSpace = getContext('aws.smus.inSmusSpaceEnvironment') + const accountId = await connectionCredentialsProvider.getDomainAccountId() + span.record({ + 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..61ce0430ecd --- /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() + + 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() + 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/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..4bc0f3ccbe6 100644 --- a/packages/core/src/shared/awsClientBuilderV3.ts +++ b/packages/core/src/shared/awsClientBuilderV3.ts @@ -31,6 +31,7 @@ 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' @@ -42,6 +43,7 @@ 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 ( @@ -81,6 +83,8 @@ export interface AwsClientOptions { retryStrategy: RetryStrategy | RetryStrategyV2 logger: Logger token: TokenIdentity | TokenIdentityProvider + forcePathStyle: boolean + hostPrefixEnabled: boolean } interface AwsServiceOptions { @@ -125,6 +129,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(':') } @@ -173,7 +178,22 @@ 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(loggingMiddleware, { step: 'finalizeRequest' }) 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 456a5c1e5cd..beb117a9bf6 100644 --- a/packages/core/src/shared/clients/clientWrapper.ts +++ b/packages/core/src/shared/clients/clientWrapper.ts @@ -23,7 +23,10 @@ export abstract class ClientWrapper implements vscode.Dispo ) {} protected getClient(ignoreCache: boolean = false) { - const args = { serviceClient: this.clientType, region: this.regionCode } + const args = { + serviceClient: this.clientType, + region: this.regionCode, + } return ignoreCache ? globals.sdkClientBuilderV3.createAwsService(args) : globals.sdkClientBuilderV3.getAwsService(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/lambdaClient.ts b/packages/core/src/shared/clients/lambdaClient.ts index 331564521ee..fb73ce9c2d2 100644 --- a/packages/core/src/shared/clients/lambdaClient.ts +++ b/packages/core/src/shared/clients/lambdaClient.ts @@ -3,56 +3,89 @@ * 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, + 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' + export type LambdaClient = ClassToInterfaceType export class DefaultLambdaClient { private readonly defaultTimeoutInMs: number - public constructor(public readonly regionCode: string) { + public constructor( + public readonly regionCode: string, + public readonly userAgent: string | undefined = undefined + ) { this.defaultTimeoutInMs = 5 * 60 * 1000 // 5 minutes (SDK default is 2 minutes) } - public async deleteFunction(name: string): Promise { + 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): Promise { + public async invoke(name: string, payload?: BlobPayloadInputTypes, version?: string): Promise { const sdkClient = await this.createSdkClient() - const response = await sdkClient - .invoke({ + const response = await sdkClient.send( + new InvokeCommand({ FunctionName: name, LogType: 'Tail', 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 @@ -62,12 +95,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, @@ -80,38 +113,70 @@ export class DefaultLambdaClient { } } - public async getFunctionUrlConfigs(name: string): 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.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, + Code: 'Pruned', + }) + return response + } catch (e) { + getLogger().error('Failed to get function: %s', e) + throw e + } + } + + public async *listLayerVersions(name: string): AsyncIterableIterator { + const client = await this.createSdkClient() + + const request: ListLayerVersionsRequest = { LayerName: name } + do { + const response: ListLayerVersionsResponse = await client.send(new ListLayerVersionsCommand(request)) + + if (response.LayerVersions) { + yield* response.LayerVersions + } + + request.Marker = response.NextMarker + } while (request.Marker) + } + + 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) { @@ -120,11 +185,167 @@ export class DefaultLambdaClient { } } - private async createSdkClient(): Promise { - return await globals.sdkClientBuilder.createAwsService( - Lambda, - { httpOptions: { timeout: this.defaultTimeoutInMs } }, - this.regionCode + public async updateFunctionConfiguration( + params: UpdateFunctionConfigurationRequest, + options: { + maxRetries?: number + initialDelayMs?: number + backoffMultiplier?: number + waitForUpdate?: boolean + } = {} + ): Promise { + const client = await this.createSdkClient() + const maxRetries = options.maxRetries ?? 5 + const initialDelayMs = options.initialDelayMs ?? 1000 + const backoffMultiplier = options.backoffMultiplier ?? 2 + // return until lambda update is completed + const waitForUpdate = options.waitForUpdate ?? false + + let retryCount = 0 + let lastError: any + + // there could be race condition, if function is being updated, wait and retry + while (retryCount <= maxRetries) { + try { + const response = await client.send(new UpdateFunctionConfigurationCommand(params)) + getLogger().debug('updateFunctionConfiguration returned response: %O', response) + if (waitForUpdate) { + // don't return if wait for result + break + } + return response + } catch (e) { + lastError = e + + // Check if this is an "update in progress" error + if (this.isUpdateInProgressError(e) && retryCount < maxRetries) { + const delayMs = initialDelayMs * Math.pow(backoffMultiplier, retryCount) + getLogger().info( + `Update in progress for Lambda function ${params.FunctionName}. ` + + `Retrying in ${delayMs}ms (attempt ${retryCount + 1}/${maxRetries})` + ) + + await new Promise((resolve) => setTimeout(resolve, delayMs)) + retryCount++ + } else { + getLogger().error('Failed to run updateFunctionConfiguration: %s', e) + throw e + } + } + } + + // check if lambda update is completed, use client.getFunctionConfiguration to poll until + // LastUpdateStatus is Successful or Failed + if (waitForUpdate) { + let lastUpdateStatus = 'InProgress' + while (lastUpdateStatus === 'InProgress') { + await new Promise((resolve) => setTimeout(resolve, 1000)) + const response = await client.send( + new GetFunctionConfigurationCommand({ FunctionName: params.FunctionName }) + ) + lastUpdateStatus = response.LastUpdateStatus ?? 'Failed' + if (lastUpdateStatus === 'Successful') { + return response + } else if (lastUpdateStatus === 'Failed') { + getLogger().error('Failed to update function configuration: %O', response) + throw new Error(`Failed to update function configuration: ${response.LastUpdateStatusReason}`) + } + } + } + + getLogger().error(`Failed to update function configuration after ${maxRetries} retries: %s`, lastError) + throw lastError + } + + public async publishVersion( + name: string, + options: { waitForUpdate?: boolean } = {} + ): Promise { + const client = await this.createSdkClient() + // return until lambda update is completed + const waitForUpdate = options.waitForUpdate ?? false + const response = await client.send( + new PublishVersionCommand({ + FunctionName: name, + }) ) + + if (waitForUpdate) { + let state = 'Pending' + while (state === 'Pending') { + await new Promise((resolve) => setTimeout(resolve, 1000)) + const statusResponse = await client.send( + new GetFunctionConfigurationCommand({ FunctionName: name, Qualifier: response.Version }) + ) + state = statusResponse.State ?? 'Failed' + if (state === 'Active' || state === 'InActive') { + // version creation finished + return statusResponse + } else if (state === 'Failed') { + getLogger().error('Failed to create Version: %O', statusResponse) + throw new Error(`Failed to create Version: ${statusResponse.LastUpdateStatusReason}`) + } + } + } + + return response } + + private isUpdateInProgressError(error: any): boolean { + return ( + error?.message && + error.message.includes( + 'The operation cannot be performed at this time. An update is in progress for resource:' + ) + ) + } + + public async waitForActive( + functionName: string, + waiter?: { maxWaitTime?: number; minDelay?: number; maxDelay?: number } + ): Promise { + const sdkClient = await this.createSdkClient() + + await waitUntilFunctionActiveV2( + { + client: sdkClient, + maxWaitTime: waiter?.maxWaitTime ?? 600, + minDelay: waiter?.minDelay ?? 1, + maxDelay: waiter?.maxDelay ?? 120, + }, + { FunctionName: functionName } + ) + } + + private async createSdkClient(): Promise { + return globals.sdkClientBuilderV3.createAwsService({ + serviceClient: LambdaSdkClient, + userAgent: !this.userAgent, + clientOptions: { + userAgent: this.userAgent ? [[this.userAgent]] : undefined, + region: this.regionCode, + requestHandler: new NodeHttpHandler({ + requestTimeout: this.defaultTimeoutInMs, + }), + }, + }) + } +} + +export async function getFunctionWithCredentials(region: string, name: string): Promise { + const connection = await getIAMConnection({ + prompt: true, + messageText: 'Opening a Lambda Function requires you to be authenticated.', + }) + + if (!connection) { + throw new CancellationError('user') + } + + const credentials = + connection.type === 'iam' ? await connection.getCredentials() : fromSSO({ profile: connection.id }) + const client = new LambdaSdkClient({ region, credentials }) + + const command = new GetFunctionCommand({ FunctionName: name }) + return client.send(command) } 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 new file mode 100644 index 00000000000..fda420effef --- /dev/null +++ b/packages/core/src/shared/clients/sagemaker.ts @@ -0,0 +1,380 @@ +/*! + * 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, + DeleteAppCommand, + DeleteAppCommandInput, + DeleteAppCommandOutput, + DescribeAppCommand, + DescribeAppCommandInput, + DescribeAppCommandOutput, + DescribeDomainCommand, + DescribeDomainCommandInput, + DescribeDomainCommandOutput, + DescribeDomainResponse, + DescribeSpaceCommand, + DescribeSpaceCommandInput, + DescribeSpaceCommandOutput, + ListAppsCommandInput, + ListSpacesCommandInput, + ResourceSpec, + SageMakerClient, + SpaceDetails, + UpdateSpaceCommand, + UpdateSpaceCommandInput, + UpdateSpaceCommandOutput, + paginateListApps, + paginateListSpaces, +} from '@amzn/sagemaker-client' +import { isEmpty } from 'lodash' +import { sleep } from '../utilities/timeoutUtils' +import { ClientWrapper } from './clientWrapper' +import { AsyncCollection } from '../utilities/asyncCollection' +import { + InstanceTypeError, + InstanceTypeMinimum, + InstanceTypeInsufficientMemory, + InstanceTypeInsufficientMemoryMessage, + InstanceTypeNotSelectedMessage, +} 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 { AwsCredentialIdentity } from '@aws-sdk/types' +import globals from '../extensionGlobals' + +const appTypeSettingsMap: Record = { + [AppType.JupyterLab as string]: 'JupyterLabAppSettings', + [AppType.CodeEditor as string]: 'CodeEditorAppSettings', +} as const + +export interface SagemakerSpaceApp extends SpaceDetails { + App?: AppDetails + DomainSpaceKey: string +} + +export class SagemakerClient extends ClientWrapper { + 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 args = { + serviceClient: SageMakerClient, + region: this.regionCode, + clientOptions: { + endpoint: `https://sagemaker.${this.regionCode}.amazonaws.com`, + 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 { + // @ts-ignore: Suppressing type mismatch on paginator return type + return this.makePaginatedRequest(paginateListSpaces, request, (page) => page.Spaces) + } + + public listApps(request: ListAppsCommandInput = {}): AsyncCollection { + // @ts-ignore: Suppressing type mismatch on paginator return type + return this.makePaginatedRequest(paginateListApps, request, (page) => page.Apps) + } + + public describeApp(request: DescribeAppCommandInput): Promise { + return this.makeRequest(DescribeAppCommand, request) + } + + public describeDomain(request: DescribeDomainCommandInput): Promise { + return this.makeRequest(DescribeDomainCommand, request) + } + + public describeSpace(request: DescribeSpaceCommandInput): Promise { + return this.makeRequest(DescribeSpaceCommand, request) + } + + public updateSpace(request: UpdateSpaceCommandInput): Promise { + return this.makeRequest(UpdateSpaceCommand, request) + } + + public createApp(request: CreateAppCommandInput): Promise { + return this.makeRequest(CreateAppCommand, request) + } + + public deleteApp(request: DeleteAppCommandInput): Promise { + return this.makeRequest(DeleteAppCommand, request) + } + + public async startSpace(spaceName: string, domainId: string) { + let spaceDetails: DescribeSpaceCommandOutput + + // Get existing space details + try { + spaceDetails = await this.describeSpace({ + DomainId: domainId, + SpaceName: spaceName, + }) + } catch (err) { + throw this.handleStartSpaceError(err) + } + + // Get app type + const appType = spaceDetails.SpaceSettings?.AppType + if (!appType || !(appType in appTypeSettingsMap)) { + throw new ToolkitError(`Unsupported AppType "${appType}" for space "${spaceName}"`) + } + + // Get app resource spec + const requestedResourceSpec = + appType === AppType.JupyterLab + ? spaceDetails.SpaceSettings?.JupyterLabAppSettings?.DefaultResourceSpec + : spaceDetails.SpaceSettings?.CodeEditorAppSettings?.DefaultResourceSpec + + let instanceType = requestedResourceSpec?.InstanceType + + // 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 (response === no) { + throw new ToolkitError('InstanceType has insufficient memory.', { code: InstanceTypeError }) + } + + 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 (response === cancel) { + throw new ToolkitError('InstanceType not defined.', { code: InstanceTypeError }) + } + + instanceType = InstanceTypeMinimum + } + + // First, update the space if needed + const needsRemoteAccess = + !spaceDetails.SpaceSettings?.RemoteAccess || spaceDetails.SpaceSettings?.RemoteAccess === 'DISABLED' + const instanceTypeChanged = requestedResourceSpec?.InstanceType !== instanceType + + if (needsRemoteAccess || instanceTypeChanged) { + const updateSpaceRequest: UpdateSpaceCommandInput = { + DomainId: domainId, + SpaceName: spaceName, + SpaceSettings: { + ...(needsRemoteAccess && { RemoteAccess: 'ENABLED' }), + ...(instanceTypeChanged && { + [appTypeSettingsMap[appType]]: { + DefaultResourceSpec: { + InstanceType: instanceType, + }, + }, + }), + }, + } + + try { + 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) + } + } + + const resourceSpec: ResourceSpec = { + // Default values + SageMakerImageArn: 'arn:aws:sagemaker:us-west-2:542918446943:image/sagemaker-distribution-cpu', + SageMakerImageVersionAlias: '3.2.0', + + // The existing resource spec + ...requestedResourceSpec, + + // The instance type user has chosen + InstanceType: instanceType, + } + + const cleanedResourceSpec = + resourceSpec && 'EnvironmentArn' in resourceSpec + ? { ...resourceSpec, EnvironmentArn: undefined, EnvironmentVersionArn: undefined } + : resourceSpec + + // Second, create the App + const createAppRequest: CreateAppCommandInput = { + DomainId: domainId, + SpaceName: spaceName, + AppType: appType, + AppName: 'default', + ResourceSpec: cleanedResourceSpec, + } + + try { + getLogger().debug('SagemakerClient: Creating app: domainId=%s, spaceName=%s', domainId, spaceName) + await this.createApp(createAppRequest) + } catch (err) { + throw this.handleStartSpaceError(err) + } + } + + 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[] = domainId + ? [domainId] + : [...new Set([...spaceApps].map(([_, spaceApp]) => spaceApp.DomainId || ''))] + + // Get details for each domain + const domains: [string, DescribeDomainResponse][] = await Promise.all( + domainIds.map(async (domainId, index) => { + await sleep(index * 100) + const response = await this.describeDomain({ DomainId: domainId }) + return [domainId, response] + }) + ) + + const domainsMap = new Map(domains) + + const filteredSpaceApps = new Map( + [...spaceApps] + // Filter out SageMaker Unified Studio domains only if filterSmusDomains is true + .filter( + ([_, spaceApp]) => + !filterSmusDomains || + isEmpty(domainsMap.get(spaceApp.DomainId || '')?.DomainSettings?.UnifiedStudioSettings) + ) + ) + + return [filteredSpaceApps, domainsMap] + } catch (err) { + const error = err as Error + getLogger().error('Failed to fetch space apps: %s', err) + if (error.name === 'AccessDeniedException') { + void vscode.window.showErrorMessage( + 'AccessDeniedException: You do not have permission to view spaces. Please contact your administrator', + { modal: false, detail: 'AWS Toolkit' } + ) + } + throw err + } + } + + private async waitForSpaceInService( + spaceName: string, + domainId: string, + maxRetries = 30, + intervalMs = 5000 + ): Promise { + for (let attempt = 0; attempt < maxRetries; attempt++) { + const result = await this.describeSpace({ SpaceName: spaceName, DomainId: domainId }) + + if (result.Status === 'InService') { + return + } + + await sleep(intervalMs) + } + + throw new ToolkitError( + `Timed out waiting for space "${spaceName}" in domain "${domainId}" to reach "InService" status.` + ) + } + + public async waitForAppInService( + domainId: string, + spaceName: string, + appType: string, + maxRetries = 30, + intervalMs = 5000 + ): Promise { + for (let attempt = 0; attempt < maxRetries; attempt++) { + const { Status } = await this.describeApp({ + DomainId: domainId, + SpaceName: spaceName, + AppType: appType as any, + AppName: 'default', + }) + + if (Status === 'InService') { + return + } + + if (['Failed', 'DeleteFailed'].includes(Status ?? '')) { + throw new ToolkitError(`App failed to start. Status: ${Status}`) + } + + await sleep(intervalMs) + } + + throw new ToolkitError(`Timed out waiting for app "${spaceName}" to reach "InService" status.`) + } + + private handleStartSpaceError(err: unknown) { + const error = err as Error + if (error.name === 'AccessDeniedException') { + throw new ToolkitError('You do not have permission to start spaces. Please contact your administrator', { + cause: error, + }) + } else { + throw err + } + } +} 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/stepFunctions.ts b/packages/core/src/shared/clients/stepFunctions.ts new file mode 100644 index 00000000000..22483307654 --- /dev/null +++ b/packages/core/src/shared/clients/stepFunctions.ts @@ -0,0 +1,68 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + CreateStateMachineCommand, + CreateStateMachineCommandInput, + CreateStateMachineCommandOutput, + DescribeStateMachineCommand, + DescribeStateMachineCommandInput, + DescribeStateMachineCommandOutput, + ListStateMachinesCommand, + ListStateMachinesCommandInput, + ListStateMachinesCommandOutput, + SFNClient, + StartExecutionCommand, + StartExecutionCommandInput, + StartExecutionCommandOutput, + StateMachineListItem, + TestStateCommand, + TestStateCommandInput, + TestStateCommandOutput, + UpdateStateMachineCommand, + UpdateStateMachineCommandInput, + UpdateStateMachineCommandOutput, +} from '@aws-sdk/client-sfn' +import { ClientWrapper } from './clientWrapper' + +export class StepFunctionsClient extends ClientWrapper { + public constructor(regionCode: string) { + super(regionCode, SFNClient) + } + + public async *listStateMachines( + request: ListStateMachinesCommandInput = {} + ): AsyncIterableIterator { + do { + const response: ListStateMachinesCommandOutput = await this.makeRequest(ListStateMachinesCommand, request) + if (response.stateMachines) { + yield* response.stateMachines + } + request.nextToken = response.nextToken + } while (request.nextToken) + } + + public async getStateMachineDetails( + request: DescribeStateMachineCommandInput + ): Promise { + return this.makeRequest(DescribeStateMachineCommand, request) + } + + public async executeStateMachine(request: StartExecutionCommandInput): Promise { + return this.makeRequest(StartExecutionCommand, request) + } + + public async createStateMachine(request: CreateStateMachineCommandInput): Promise { + return this.makeRequest(CreateStateMachineCommand, request) + } + + public async updateStateMachine(request: UpdateStateMachineCommandInput): Promise { + return this.makeRequest(UpdateStateMachineCommand, request) + } + + public async testState(request: TestStateCommandInput): Promise { + return this.makeRequest(TestStateCommand, request) + } +} diff --git a/packages/core/src/shared/clients/stepFunctionsClient.ts b/packages/core/src/shared/clients/stepFunctionsClient.ts deleted file mode 100644 index 66d45bcd58a..00000000000 --- a/packages/core/src/shared/clients/stepFunctionsClient.ts +++ /dev/null @@ -1,79 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { StepFunctions } from 'aws-sdk' -import globals from '../extensionGlobals' -import { ClassToInterfaceType } from '../utilities/tsUtils' - -export type StepFunctionsClient = ClassToInterfaceType -export class DefaultStepFunctionsClient { - public constructor(public readonly regionCode: string) {} - - public async *listStateMachines(): AsyncIterableIterator { - const client = await this.createSdkClient() - - const request: StepFunctions.ListStateMachinesInput = {} - do { - const response: StepFunctions.ListStateMachinesOutput = await client.listStateMachines(request).promise() - - if (response.stateMachines) { - yield* response.stateMachines - } - - request.nextToken = response.nextToken - } while (request.nextToken) - } - - public async getStateMachineDetails(arn: string): Promise { - const client = await this.createSdkClient() - - const request: StepFunctions.DescribeStateMachineInput = { - stateMachineArn: arn, - } - - const response: StepFunctions.DescribeStateMachineOutput = await client.describeStateMachine(request).promise() - - return response - } - - public async executeStateMachine(arn: string, input?: string): Promise { - const client = await this.createSdkClient() - - const request: StepFunctions.StartExecutionInput = { - stateMachineArn: arn, - input: input, - } - - const response: StepFunctions.StartExecutionOutput = await client.startExecution(request).promise() - - return response - } - - public async createStateMachine( - params: StepFunctions.CreateStateMachineInput - ): Promise { - const client = await this.createSdkClient() - - return client.createStateMachine(params).promise() - } - - public async updateStateMachine( - params: StepFunctions.UpdateStateMachineInput - ): Promise { - const client = await this.createSdkClient() - - return client.updateStateMachine(params).promise() - } - - public async testState(params: StepFunctions.TestStateInput): Promise { - const client = await this.createSdkClient() - - return await client.testState(params).promise() - } - - private async createSdkClient(): Promise { - return await globals.sdkClientBuilder.createAwsService(StepFunctions, undefined, this.regionCode) - } -} diff --git a/packages/core/src/shared/clients/stsClient.ts b/packages/core/src/shared/clients/stsClient.ts index a090a846bf8..f3a225882a5 100644 --- a/packages/core/src/shared/clients/stsClient.ts +++ b/packages/core/src/shared/clients/stsClient.ts @@ -3,38 +3,60 @@ * 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' +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() + 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 5d08bb836dc..690a5aee73c 100644 --- a/packages/core/src/shared/cloudformation/cloudformation.ts +++ b/packages/core/src/shared/cloudformation/cloudformation.ts @@ -16,6 +16,10 @@ import { isUntitledScheme, normalizeVSCodeUri } from '../utilities/vsCodeUtils' export const SERVERLESS_API_TYPE = 'AWS::Serverless::Api' // eslint-disable-line @typescript-eslint/naming-convention export const SERVERLESS_FUNCTION_TYPE = 'AWS::Serverless::Function' // eslint-disable-line @typescript-eslint/naming-convention export const LAMBDA_FUNCTION_TYPE = 'AWS::Lambda::Function' // eslint-disable-line @typescript-eslint/naming-convention +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 serverlessTableType = 'AWS::Serverless::SimpleTable' export const s3BucketType = 'AWS::S3::Bucket' export const appRunnerType = 'AWS::AppRunner::Service' diff --git a/packages/core/src/shared/constants.ts b/packages/core/src/shared/constants.ts index 48e64342e57..95d2aaac309 100644 --- a/packages/core/src/shared/constants.ts +++ b/packages/core/src/shared/constants.ts @@ -4,6 +4,7 @@ */ import * as vscode from 'vscode' +import { manageAccessGuideURL } from '../amazonq/webview/ui/texts/constants' export const profileSettingKey = 'profile' export const productName: string = 'aws-toolkit-vscode' @@ -196,3 +197,11 @@ export const amazonQVscodeMarketplace = export const crashMonitoringDirName = 'crashMonitoring' export const amazonQTabSuffix = '(Generated by Amazon Q)' + +/** + * Common strings used throughout the application + */ + +export const uploadCodeError = `I'm sorry, I couldn't upload your workspace artifacts to Amazon S3 to help you with this task. You might need to allow access to the S3 bucket. For more information, see the [Amazon Q documentation](${manageAccessGuideURL}) or contact your network or organization administrator.` + +export const featureName = 'Amazon Q Developer Agent for software development' diff --git a/packages/core/src/shared/datetime.ts b/packages/core/src/shared/datetime.ts index 6123421666a..8043f94d343 100644 --- a/packages/core/src/shared/datetime.ts +++ b/packages/core/src/shared/datetime.ts @@ -154,3 +154,25 @@ export function formatDateTimestamp(forceUTC: boolean, d: Date = new Date()): st // trim 'Z' (last char of iso string) and add offset string return `${iso.substring(0, iso.length - 1)}${offsetString}` } + +/** + * Checks if a given timestamp is within 30 days of the current day + * @param timeStamp + * @returns true if timeStamp is within 30 days, false otherwise + */ +export function isWithin30Days(timeStamp: string): boolean { + if (!timeStamp) { + return false // No timestamp given + } + + const startDate = new Date(timeStamp) + const currentDate = new Date() + + // Calculate the difference in milliseconds + const timeDifference = currentDate.getTime() - startDate.getTime() + + // Convert milliseconds to days (1000ms * 60s * 60min * 24hr) + const daysDifference = timeDifference / (1000 * 60 * 60 * 24) + + return daysDifference <= 30 +} diff --git a/packages/core/src/shared/db/chatDb/util.ts b/packages/core/src/shared/db/chatDb/util.ts index fc681b2b5a5..316cbe5660c 100644 --- a/packages/core/src/shared/db/chatDb/util.ts +++ b/packages/core/src/shared/db/chatDb/util.ts @@ -267,16 +267,10 @@ function getTabTypeIcon(tabType: TabType): MynahIconsType { switch (tabType) { case 'cwc': return 'chat' - case 'doc': - return 'file' case 'review': return 'bug' case 'gumby': return 'transform' - case 'testgen': - return 'check-list' - case 'featuredev': - return 'code-block' default: return 'chat' } 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 7cec122acaf..24e3f1ba93a 100644 --- a/packages/core/src/shared/errors.ts +++ b/packages/core/src/shared/errors.ts @@ -15,7 +15,7 @@ import type * as os from 'os' import { CodeWhispererStreamingServiceException } from '@amzn/codewhisperer-streaming' import { driveLetterRegex } from './utilities/pathUtils' import { getLogger } from './logger/logger' -import { crashMonitoringDirName } from './constants' +import { crashMonitoringDirName, uploadCodeError } from './constants' import { RequestCancelledError } from './request' let _username = 'unknown-user' @@ -600,6 +600,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' } @@ -849,6 +853,21 @@ export class ServiceError extends ToolkitError { } } +export class UploadURLExpired extends ClientError { + constructor() { + super( + "I’m sorry, I wasn't able to generate code. A connection timed out or became unavailable. Please try again or check the following:\n\n- Exclude non-essential files in your workspace’s .gitignore.\n\n- Check that your network connection is stable.", + { code: 'UploadURLExpired' } + ) + } +} + +export class UploadCodeError extends ServiceError { + constructor(statusCode: string) { + super(uploadCodeError, { code: `UploadCode-${statusCode}` }) + } +} + export class ContentLengthError extends ClientError { constructor(message: string, info: ErrorInformation = { code: 'ContentLengthError' }) { super(message, info) diff --git a/packages/core/src/shared/extensionUtilities.ts b/packages/core/src/shared/extensionUtilities.ts index 9675f060951..b8b5780c612 100644 --- a/packages/core/src/shared/extensionUtilities.ts +++ b/packages/core/src/shared/extensionUtilities.ts @@ -11,7 +11,7 @@ import { getLogger } from './logger/logger' import { VSCODE_EXTENSION_ID, extensionAlphaVersion } from './extensions' import { Ec2MetadataClient } from './clients/ec2MetadataClient' import { DefaultEc2MetadataClient } from './clients/ec2MetadataClient' -import { extensionVersion, getCodeCatalystDevEnvId } from './vscode/env' +import { extensionVersion, getCodeCatalystDevEnvId, hasSageMakerEnvVars } from './vscode/env' import globals from './extensionGlobals' import { once } from './utilities/functionUtils' import { @@ -150,7 +150,11 @@ function createCloud9Properties(company: string): IdeProperties { } } -function isSageMakerUnifiedStudio(): boolean { +/** + * export method - for testing purposes only + * @internal + */ +export function isSageMakerUnifiedStudio(): boolean { if (serviceName === notInitialized) { serviceName = process.env.SERVICE_NAME ?? '' isSMUS = serviceName === sageMakerUnifiedStudio @@ -158,6 +162,15 @@ function isSageMakerUnifiedStudio(): boolean { return isSMUS } +/** + * Reset cached SageMaker state - for testing purposes only + * @internal + */ +export function resetSageMakerState(): void { + serviceName = notInitialized + isSMUS = false +} + /** * Decides if the current system is (the specified flavor of) Cloud9. */ @@ -175,19 +188,39 @@ 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()) { + getLogger().debug('SageMaker environment detected via environment variables') + hasSMEnvVars = true + } + switch (appName) { case 'SMAI': - return vscode.env.appName === sageMakerAppname + return vscode.env.appName === sageMakerAppname && hasSMEnvVars case 'SMUS': - return vscode.env.appName === sageMakerAppname && isSageMakerUnifiedStudio() + 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 } } export function isCn(): boolean { - return getComputeRegion()?.startsWith('cn') ?? false + try { + const region = getComputeRegion() + if (!region || region === 'notInitialized') { + getLogger().debug('isCn called before compute region initialized, defaulting to false') + return false + } + return region.startsWith('cn') + } catch (err) { + getLogger().error(`Error in isCn method: ${err}`) + return false + } } /** 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 d7acb9657be..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 @@ -55,6 +57,9 @@ export const featureDefinitions = new Map([ export class FeatureConfigProvider { private featureConfigs = new Map() + private fetchPromise: Promise | undefined = undefined + private lastFetchTime = 0 + private readonly minFetchInterval = 5000 // 5 seconds minimum between fetches static #instance: FeatureConfigProvider @@ -103,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 = { @@ -123,6 +138,28 @@ export class FeatureConfigProvider { return } + // Debounce multiple concurrent calls + const now = performance.now() + if (this.fetchPromise && now - this.lastFetchTime < this.minFetchInterval) { + getLogger().debug('amazonq: Debouncing feature config fetch') + return this.fetchPromise + } + + if (this.fetchPromise) { + return this.fetchPromise + } + + this.lastFetchTime = now + this.fetchPromise = this._fetchFeatureConfigsInternal() + + try { + await this.fetchPromise + } finally { + this.fetchPromise = undefined + } + } + + private async _fetchFeatureConfigsInternal(): Promise { getLogger().debug('amazonq: Fetching feature configs') try { const response = await this.listFeatureEvaluations() diff --git a/packages/core/src/shared/filesystemUtilities.ts b/packages/core/src/shared/filesystemUtilities.ts index 54ca5b4b0e1..6414fd11b66 100644 --- a/packages/core/src/shared/filesystemUtilities.ts +++ b/packages/core/src/shared/filesystemUtilities.ts @@ -20,8 +20,6 @@ export const tempDirPath = path.join( 'aws-toolkit-vscode' ) -export const testGenerationLogsDir = path.join(tempDirPath, 'testGenerationLogs') - export async function getDirSize(dirPath: string, startTime: number, duration: number): Promise { if (performance.now() - startTime > duration) { getLogger().warn('getDirSize: exceeds time limit') 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 13db46b430a..edde0611e0b 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' @@ -79,6 +79,12 @@ export type globalKey = | 'aws.toolkit.lambda.walkthroughSelected' | 'aws.toolkit.lambda.walkthroughCompleted' | 'aws.toolkit.appComposer.templateToOpenOnStart' + | 'aws.lambda.remoteDebugContext' + | 'aws.lambda.remoteDebugSnapshot' + // List of Domain-Users to show/hide Sagemaker SpaceApps in AWS Explorer. + | 'aws.sagemaker.selectedDomainUsers' + // Name of the connection if it's not to the AWS cloud. Current supported value only 'localstack' + | 'aws.toolkit.externalConnection' /** * Extension-local (not visible to other vscode extensions) shared state which persists after IDE diff --git a/packages/core/src/shared/index.ts b/packages/core/src/shared/index.ts index 799ffb1b35c..8b62fd3c5dc 100644 --- a/packages/core/src/shared/index.ts +++ b/packages/core/src/shared/index.ts @@ -27,7 +27,7 @@ export { Prompter } from './ui/prompter' export { VirtualFileSystem } from './virtualFilesystem' export { VirtualMemoryFile } from './virtualMemoryFile' export { AmazonqCreateUpload, Metric } from './telemetry/telemetry' -export { getClientId, getOperatingSystem, getOptOutPreference } from './telemetry/util' +export { getClientId, getClientName, getOperatingSystem, getOptOutPreference } from './telemetry/util' export { extensionVersion } from './vscode/env' export { cast } from './utilities/typeConstructors' export * as workspaceUtils from './utilities/workspaceUtils' @@ -50,7 +50,12 @@ export * as env from './vscode/env' export * from './vscode/commands2' export * from './utilities/pathUtils' export * from './utilities/zipStream' +export * as editorUtilities from './utilities/editorUtilities' +export * as functionUtilities from './utilities/functionUtils' +export * as diffUtilities from './utilities/diffUtils' +export * as vscodeUtilities from './utilities/vsCodeUtils' export * from './errors' +export { isTextEditor } from './utilities/editorUtilities' export * as messages from './utilities/messages' export * as errors from './errors' export * as funcUtil from './utilities/functionUtils' @@ -75,3 +80,4 @@ export * as BaseLspInstaller from './lsp/baseLspInstaller' export * as collectionUtil from './utilities/collectionUtils' export * from './datetime' export * from './performance/marks' +export * as mementoUtils from './utilities/mementos' diff --git a/packages/core/src/shared/logger/logger.ts b/packages/core/src/shared/logger/logger.ts index eb2602c30b9..95c4c7af769 100644 --- a/packages/core/src/shared/logger/logger.ts +++ b/packages/core/src/shared/logger/logger.ts @@ -22,6 +22,7 @@ export type LogTopic = | 'resourceCache' | 'telemetry' | 'proxyUtil' + | 'sagemaker' class ErrorLog { constructor( diff --git a/packages/core/src/shared/lsp/baseLspInstaller.ts b/packages/core/src/shared/lsp/baseLspInstaller.ts index 0aeca1dfda4..7acf58ad788 100644 --- a/packages/core/src/shared/lsp/baseLspInstaller.ts +++ b/packages/core/src/shared/lsp/baseLspInstaller.ts @@ -5,7 +5,6 @@ import * as nodePath from 'path' import vscode from 'vscode' -import { LspConfig } from '../../amazonq/lsp/config' import { LanguageServerResolver } from './lspResolver' import { ManifestResolver } from './manifestResolver' import { LspResolution, ResourcePaths } from './types' @@ -14,6 +13,14 @@ import { Range } from 'semver' import { getLogger } from '../logger/logger' import type { Logger, LogTopic } from '../logger/logger' +export interface LspConfig { + manifestUrl: string + supportedVersions: string + id: string + suppressPromptPrefix: string + path?: string +} + export abstract class BaseLspInstaller { private logger: Logger diff --git a/packages/core/src/shared/lsp/utils/platform.ts b/packages/core/src/shared/lsp/utils/platform.ts index 39284e8a0ac..190a29f7ab1 100644 --- a/packages/core/src/shared/lsp/utils/platform.ts +++ b/packages/core/src/shared/lsp/utils/platform.ts @@ -3,11 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ +import * as vscode from 'vscode' import { ToolkitError } from '../../errors' import { Logger } from '../../logger/logger' import { ChildProcess } from '../../utilities/processUtils' import { waitUntil } from '../../utilities/timeoutUtils' -import { isDebugInstance } from '../../vscode/env' +import { isDebugInstance, isRemoteWorkspace } from '../../vscode/env' +import { isSageMaker } from '../../extensionUtilities' +import { getLogger } from '../../logger/logger' + +interface SagemakerCookie { + authMode?: 'Sso' | 'Iam' +} export function getNodeExecutableName(): string { return process.platform === 'win32' ? 'node.exe' : 'node' @@ -96,11 +103,53 @@ export function createServerOptions({ }) { return async () => { const bin = executable[0] - const args = [...executable.slice(1), serverModule, ...execArgv] + const args = [...executable.slice(1), '--max-old-space-size=8196', serverModule, ...execArgv] if (isDebugInstance()) { args.unshift('--inspect=6080') } - const lspProcess = new ChildProcess(bin, args, { warnThresholds }) + + // Set USE_IAM_AUTH environment variable for SageMaker environments based on cookie detection + // This tells the language server to use IAM authentication mode instead of SSO mode + const env = { ...process.env } + if (isSageMaker()) { + try { + // The command `sagemaker.parseCookies` is registered in VS Code SageMaker environment + const result = (await vscode.commands.executeCommand('sagemaker.parseCookies')) as SagemakerCookie + if (result.authMode !== 'Sso') { + env.USE_IAM_AUTH = 'true' + getLogger().info( + `[SageMaker Debug] Setting USE_IAM_AUTH=true for language server process (authMode: ${result.authMode})` + ) + } else { + getLogger().info(`[SageMaker Debug] Using SSO auth mode, not setting USE_IAM_AUTH`) + } + } catch (err) { + if (isRemoteWorkspace() && env.SERVICE_NAME !== 'SageMakerUnifiedStudio') { + getLogger().warn( + `[SageMaker Debug] Failed to parse SageMaker cookies in remote space, not SMUS env, not defaulting to IAM auth: ${err}` + ) + } else { + getLogger().warn( + `[SageMaker Debug] Failed to parse SageMaker cookies, defaulting to IAM auth: ${err}` + ) + env.USE_IAM_AUTH = 'true' + } + } + + // Log important environment variables for debugging + getLogger().info(`[SageMaker Debug] Environment variables for language server:`) + getLogger().info(`[SageMaker Debug] USE_IAM_AUTH: ${env.USE_IAM_AUTH}`) + getLogger().info( + `[SageMaker Debug] AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: ${env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}` + ) + getLogger().info(`[SageMaker Debug] AWS_DEFAULT_REGION: ${env.AWS_DEFAULT_REGION}`) + getLogger().info(`[SageMaker Debug] AWS_REGION: ${env.AWS_REGION}`) + } + + const lspProcess = new ChildProcess(bin, args, { + warnThresholds, + spawnOptions: { env }, + }) // this is a long running process, awaiting it will never resolve void lspProcess.run() diff --git a/packages/core/src/shared/lsp/utils/setupStage.ts b/packages/core/src/shared/lsp/utils/setupStage.ts index cd9dcfa319a..8f43ba16f3f 100644 --- a/packages/core/src/shared/lsp/utils/setupStage.ts +++ b/packages/core/src/shared/lsp/utils/setupStage.ts @@ -5,6 +5,7 @@ import { LanguageServerSetup, LanguageServerSetupStage, telemetry } from '../../telemetry/telemetry' import { tryFunctions } from '../../utilities/tsUtils' +import { AuthUtil } from '../../../codewhisperer/util/authUtil' /** * Runs the designated stage within a telemetry span and optionally uses the getMetadata extractor to record metadata from the result of the stage. @@ -20,6 +21,7 @@ export async function lspSetupStage( ) { return await telemetry.languageServer_setup.run(async (span) => { span.record({ languageServerSetupStage: stageName }) + span.record({ credentialStartUrl: AuthUtil.instance.startUrl ?? 'Undefined' }) const result = await runStage() if (getMetadata) { span.record(getMetadata(result)) diff --git a/packages/core/src/shared/remoteSession.ts b/packages/core/src/shared/remoteSession.ts index 282b629df51..b45bdb3ca9c 100644 --- a/packages/core/src/shared/remoteSession.ts +++ b/packages/core/src/shared/remoteSession.ts @@ -25,6 +25,11 @@ import { EvaluationResult } from '@aws-sdk/client-iam' const policyAttachDelay = 5000 +export enum RemoteSessionError { + ExtensionVersionTooLow = 'ExtensionVersionTooLow', + MissingExtension = 'MissingExtension', +} + export interface MissingTool { readonly name: 'code' | 'ssm' | 'ssh' readonly reason?: string @@ -114,13 +119,13 @@ export async function ensureRemoteSshInstalled(): Promise { if (isExtensionInstalled(VSCODE_EXTENSION_ID.remotessh)) { throw new ToolkitError('Remote SSH extension version is too low', { cancelled: true, - code: 'ExtensionVersionTooLow', + code: RemoteSessionError.ExtensionVersionTooLow, details: { expected: vscodeExtensionMinVersion.remotessh }, }) } else { throw new ToolkitError('Remote SSH extension not installed', { cancelled: true, - code: 'MissingExtension', + code: RemoteSessionError.MissingExtension, }) } } 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/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/pythonSamDebug.ts b/packages/core/src/shared/sam/debugger/pythonSamDebug.ts index 670c2ddd71a..902dc85003d 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 { @@ -166,7 +166,7 @@ function getPythonExeAndBootstrap(runtime: Runtime) { 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': + case 'python3.13' as Runtime: return { python: '/var/lang/bin/python3.13', 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/settings-amazonq.gen.ts b/packages/core/src/shared/settings-amazonq.gen.ts index 836b68444f2..2ca8481b55e 100644 --- a/packages/core/src/shared/settings-amazonq.gen.ts +++ b/packages/core/src/shared/settings-amazonq.gen.ts @@ -37,7 +37,8 @@ export const amazonqSettings = { "amazonQ.workspaceIndexCacheDirPath": {}, "amazonQ.workspaceIndexIgnoreFilePatterns": {}, "amazonQ.ignoredSecurityIssues": {}, - "amazonQ.proxy.certificateAuthority": {} + "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 59a637a4870..55bc77f9828 100644 --- a/packages/core/src/shared/settings-toolkit.gen.ts +++ b/packages/core/src/shared/settings-toolkit.gen.ts @@ -44,13 +44,16 @@ export const toolkitSettings = { "jsonResourceModification": {}, "amazonqLSP": {}, "amazonqLSPInline": {}, - "amazonqChatLSP": {} + "amazonqChatLSP": {}, + "amazonqLSPInlineChat": {}, + "amazonqLSPNEP": {} }, "aws.resources.enabledResources": {}, "aws.lambda.recentlyUploaded": {}, "aws.accessAnalyzer.policyChecks.checkNoNewAccessFilePath": {}, "aws.accessAnalyzer.policyChecks.checkAccessNotGrantedFilePath": {}, - "aws.accessAnalyzer.policyChecks.cloudFormationParameterFilePath": {} + "aws.accessAnalyzer.policyChecks.cloudFormationParameterFilePath": {}, + "aws.sagemaker.studio.spaces.enableIdentityFiltering": {} } export default toolkitSettings diff --git a/packages/core/src/shared/sshConfig.ts b/packages/core/src/shared/sshConfig.ts index 2c60b423ab3..92b32666b06 100644 --- a/packages/core/src/shared/sshConfig.ts +++ b/packages/core/src/shared/sshConfig.ts @@ -40,6 +40,9 @@ export class SshConfig { } protected async getProxyCommand(command: string): Promise> { + // Use %n for SageMaker to preserve original hostname case (avoids SSH canonicalization lowercasing and DNS lookup) + const hostnameToken = this.scriptPrefix === 'sagemaker_connect' ? '%n' : '%h' + if (this.isWin()) { // Some older versions of OpenSSH (7.8 and below) have a bug where attempting to use powershell.exe directly will fail without an absolute path const proc = new ChildProcess('powershell.exe', ['-Command', '(get-command powershell.exe).Path']) @@ -47,9 +50,9 @@ export class SshConfig { if (r.exitCode !== 0) { return Result.err(new ToolkitError('Failed to get absolute path for powershell', { cause: r.error })) } - return Result.ok(`"${r.stdout}" -ExecutionPolicy RemoteSigned -File "${command}" %h`) + return Result.ok(`"${r.stdout}" -ExecutionPolicy RemoteSigned -File "${command}" ${hostnameToken}`) } else { - return Result.ok(`'${command}' '%h'`) + return Result.ok(`'${command}' '${hostnameToken}'`) } } @@ -192,8 +195,21 @@ Host ${this.configHostName} ` } + private getSageMakerSSHConfig(proxyCommand: string): string { + return ` +# Created by AWS Toolkit for VSCode. https://github.com/aws/aws-toolkit-vscode +Host ${this.configHostName} + ForwardAgent yes + AddKeysToAgent yes + StrictHostKeyChecking accept-new + ProxyCommand ${proxyCommand} + ` + } + protected createSSHConfigSection(proxyCommand: string): string { - if (this.keyPath) { + if (this.scriptPrefix === 'sagemaker_connect') { + return `${this.getSageMakerSSHConfig(proxyCommand)}` + } else if (this.keyPath) { return `${this.getBaseSSHConfig(proxyCommand)}IdentityFile '${this.keyPath}'\n User '%r'\n` } return this.getBaseSSHConfig(proxyCommand) diff --git a/packages/core/src/shared/telemetry/exemptMetrics.ts b/packages/core/src/shared/telemetry/exemptMetrics.ts index a3fc8d5ad78..4e0deacc058 100644 --- a/packages/core/src/shared/telemetry/exemptMetrics.ts +++ b/packages/core/src/shared/telemetry/exemptMetrics.ts @@ -29,6 +29,8 @@ const validationExemptMetrics: Set = new Set([ 'codewhisperer_codePercentage', 'codewhisperer_userModification', 'codewhisperer_userTriggerDecision', + 'codewhisperer_perceivedLatency', // flare doesn't currently set result property + 'codewhisperer_serviceInvocation', // flare doesn't currently set result property 'dynamicresource_selectResources', 'dynamicresource_copyIdentifier', 'dynamicresource_mutateResource', diff --git a/packages/core/src/shared/telemetry/service-2.json b/packages/core/src/shared/telemetry/service-2.json index 9711b3473cc..a0ca9f7b14e 100644 --- a/packages/core/src/shared/telemetry/service-2.json +++ b/packages/core/src/shared/telemetry/service-2.json @@ -205,7 +205,8 @@ "AWSProduct", "AWSProductVersion", "ClientID", - "MetricData" + "MetricData", + "CredentialStartUrl" ], "members":{ "AWSProduct":{"shape":"AWSProduct"}, @@ -217,7 +218,8 @@ "ComputeEnv": {"shape":"ComputeEnv"}, "ParentProduct":{"shape":"Value"}, "ParentProductVersion":{"shape":"Value"}, - "MetricData":{"shape":"MetricData"} + "MetricData":{"shape":"MetricData"}, + "CredentialStartUrl": {"shape":"Value"} } }, "Sentiment":{ diff --git a/packages/core/src/shared/telemetry/telemetryClient.ts b/packages/core/src/shared/telemetry/telemetryClient.ts index 139b4b48814..97ec2508cfc 100644 --- a/packages/core/src/shared/telemetry/telemetryClient.ts +++ b/packages/core/src/shared/telemetry/telemetryClient.ts @@ -17,6 +17,7 @@ import globals from '../extensionGlobals' import { DevSettings } from '../settings' import { ClassToInterfaceType } from '../utilities/tsUtils' import { getComputeEnvType, getSessionId } from './util' +import { AuthUtil } from '../../codewhisperer/util/authUtil' export const accountMetadataKey = 'awsAccount' export const regionKey = 'awsRegion' @@ -112,6 +113,7 @@ export class DefaultTelemetryClient implements TelemetryClient { ParentProduct: vscode.env.appName, ParentProductVersion: vscode.version, MetricData: batch, + CredentialStartUrl: AuthUtil.instance.startUrl ?? 'Undefined', }) .promise() this.logger.info(`telemetry: sent batch (size=${batch.length})`) diff --git a/packages/core/src/shared/telemetry/util.ts b/packages/core/src/shared/telemetry/util.ts index 310c36b82d6..e6c7254e878 100644 --- a/packages/core/src/shared/telemetry/util.ts +++ b/packages/core/src/shared/telemetry/util.ts @@ -481,3 +481,20 @@ export function withTelemetryContext(opts: TelemetryContextArgs) { }) } } + +/** + * Used to identify the q client info and send the respective origin parameter from LSP to invoke Maestro service at CW API level + * + * Returns default value of vscode appName or AmazonQ-For-SMUS-CE in case of a sagemaker unified studio environment + * Returns default value of vscode appName + * OR AmazonQ-For-SMUS-CE in case of SMUS + * OR AmazonQ-For-SMAI-CE in case of SMAI + */ +export function getClientName(): string { + if (isSageMaker('SMUS')) { + return 'AmazonQ-For-SMUS-CE' + } else if (isSageMaker('SMAI')) { + return 'AmazonQ-For-SMAI-CE' + } + return env.appName +} diff --git a/packages/core/src/shared/telemetry/vscodeTelemetry.json b/packages/core/src/shared/telemetry/vscodeTelemetry.json index b28aeec4847..fcf6140eb13 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,9 +252,112 @@ "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" } ], "metrics": [ + { + "name": "sagemaker_openRemoteConnection", + "description": "Perform a connection to a SageMaker Space", + "metadata": [ + { + "type": "result" + } + ] + }, + { + "name": "sagemaker_stopSpace", + "description": "Stop a SageMaker Space", + "metadata": [ + { + "type": "result" + } + ] + }, + { + "name": "sagemaker_filterSpaces", + "description": "Filter SageMaker Spaces", + "metadata": [ + { + "type": "result" + } + ] + }, + { + "name": "sagemaker_deeplinkConnect", + "description": "Connect to SageMake Space via a deeplink", + "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.", @@ -1110,6 +1227,413 @@ { "name": "docdb_addRegion", "description": "User clicked on add region command" + }, + { + "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", + "metadata": [ + { + "type": "sessionDuration", + "required": false + } + ] + }, + { + "name": "lambda_remoteDebugStart", + "description": "user start remote debugging", + "metadata": [ + { + "type": "runtimeString", + "required": false + }, + { + "type": "source", + "required": false + }, + { + "type": "action", + "required": false + } + ] + }, + { + "name": "lambda_remoteDebugPrecheck", + "description": "user click remote debug checkbox", + "metadata": [ + { + "type": "runtimeString", + "required": false + }, + { + "type": "source", + "required": false + }, + { + "type": "action", + "required": false + } + ] + }, + { + "name": "lambda_invokeRemote", + "description": "Called when invoking lambdas remotely", + "metadata": [ + { + "type": "result" + }, + { + "type": "runtime", + "required": false + }, + { + "type": "source", + "required": false + }, + { + "type": "runtimeString", + "required": false + }, + { + "type": "action", + "required": false + } + ] + }, + { + "name": "languageServer_setup", + "description": "Sets up a language server", + "unit": "Milliseconds", + "passive": true, + "metadata": [ + { + "type": "id", + "required": true + }, + { + "type": "languageServerSetupStage", + "required": true + }, + { + "type": "languageServerLocation", + "required": false + }, + { + "type": "languageServerVersion", + "required": false + }, + { + "type": "manifestLocation", + "required": false + }, + { + "type": "manifestSchemaVersion", + "required": false + }, + { + "type": "credentialStartUrl", + "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 + } + ] + }, + { + "name": "smus_signOut", + "description": "Emitted whenever a user signouts SMUS", + "metadata": [ + { + "type": "smusDomainId", + "required": false + }, + { + "type": "smusDomainAccountId", + "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 + } + ] + }, + { + "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 + } + ], + "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 + } + ] + }, + { + "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 + } + ] + }, + { + "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 + } + ] + }, + { + "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 + } + ] + }, + { + "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 + } + ] } ] } 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/diffUtils.ts b/packages/core/src/shared/utilities/diffUtils.ts index f6dcd9d3fe4..994f91a5434 100644 --- a/packages/core/src/shared/utilities/diffUtils.ts +++ b/packages/core/src/shared/utilities/diffUtils.ts @@ -11,6 +11,7 @@ import { amazonQDiffScheme } from '../constants' import { ContentProvider } from '../../amazonq/commons/controllers/contentController' import { disposeOnEditorClose } from './editorUtilities' import { getLogger } from '../logger/logger' +import jaroWinkler from 'jaro-winkler' /** * Get the patched code from a file and a patch. @@ -24,7 +25,7 @@ import { getLogger } from '../logger/logger' */ export async function getPatchedCode(filePath: string, patch: string, snippetMode = false) { const document = await vscode.workspace.openTextDocument(filePath) - const fileContent = document.getText() + const fileContent = document.getText().replaceAll('\r\n', '\n') // Usage with the existing getPatchedCode function: let updatedPatch = patch @@ -149,3 +150,40 @@ export function getDiffCharsAndLines( addedLines, } } + +/** + * Extracts modified lines by comparing added and removed lines. + * @param addedLines The array of added lines. + * @param removedLines The array of removed lines. + * @returns A Map where keys are removed lines and values are the corresponding modified (added) lines. + */ +export function getModifiedLinesFromCode(addedLines: string[], removedLines: string[]): Map { + const modifiedMap = new Map() + let addedIndex = 0 + + // For each removed line, find the most similar added line + for (const removedLine of removedLines) { + let bestMatchIndex = -1 + + for (let i = addedIndex; i < addedLines.length; i++) { + const addedLine = addedLines[i] + const score = jaroWinkler(removedLine, addedLine) + if (score > 0.5) { + bestMatchIndex = i + break + } + } + + if (bestMatchIndex !== -1) { + // Map the removed line to the most similar added line + modifiedMap.set(removedLine, addedLines[bestMatchIndex]) + // Move addedIndex to the next line after the match + addedIndex = bestMatchIndex + 1 + } else { + // No match found for this removed line, move on + continue + } + } + + return modifiedMap +} diff --git a/packages/core/src/shared/utilities/functionUtils.ts b/packages/core/src/shared/utilities/functionUtils.ts index cbf89340ade..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. * @@ -93,9 +119,10 @@ export function memoize(fn: (...args: U) => T): (...args: U) */ export function debounce( cb: (...args: Input) => Output | Promise, - delay: number = 0 + delay: number = 0, + useLastCall: boolean = false ): (...args: Input) => Promise { - return cancellableDebounce(cb, delay).promise + return cancellableDebounce(cb, delay, useLastCall).promise } /** @@ -104,10 +131,12 @@ export function debounce( */ export function cancellableDebounce( cb: (...args: Input) => Output | Promise, - delay: number = 0 + delay: number = 0, + useLastCall: boolean = false ): { promise: (...args: Input) => Promise; cancel: () => void } { let timeout: Timeout | undefined let promise: Promise | undefined + let lastestArgs: Input | undefined const cancel = (): void => { if (timeout) { @@ -119,6 +148,7 @@ export function cancellableDebounce( return { promise: (...args: Input) => { + lastestArgs = args timeout?.refresh() return (promise ??= new Promise((resolve, reject) => { @@ -126,7 +156,8 @@ export function cancellableDebounce( timeout.onCompletion(async () => { timeout = promise = undefined try { - resolve(await cb(...args)) + const argsToUse = useLastCall ? lastestArgs! : args + resolve(await cb(...argsToUse)) } catch (err) { reject(err) } diff --git a/packages/core/src/shared/utilities/index.ts b/packages/core/src/shared/utilities/index.ts index 520390b5204..e86f941456d 100644 --- a/packages/core/src/shared/utilities/index.ts +++ b/packages/core/src/shared/utilities/index.ts @@ -6,3 +6,7 @@ export { isExtensionInstalled, isExtensionActive } from './vsCodeUtils' export { VSCODE_EXTENSION_ID } from '../extensions' 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/messages.ts b/packages/core/src/shared/utilities/messages.ts index 26fd745c8d6..b3cc178cabb 100644 --- a/packages/core/src/shared/utilities/messages.ts +++ b/packages/core/src/shared/utilities/messages.ts @@ -5,6 +5,7 @@ import * as vscode from 'vscode' import * as nls from 'vscode-nls' +import * as path from 'path' import * as localizedText from '../localizedText' import { getLogger } from '../../shared/logger/logger' import { ProgressEntry } from '../../shared/vscode/window' @@ -14,6 +15,8 @@ import { Timeout } from './timeoutUtils' import { addCodiconToString } from './textUtilities' import { getIcon, codicon } from '../icons' import globals from '../extensionGlobals' +import { ToolkitError } from '../../shared/errors' +import { fs } from '../../shared/fs/fs' import { openUrl } from './vsCodeUtils' import { AmazonQPromptSettings, ToolkitPromptSettings } from '../../shared/settings' import { telemetry, ToolkitShowNotification } from '../telemetry/telemetry' @@ -140,6 +143,41 @@ export async function showViewLogsMessage( }) } +/** + * Checks if a path exists and prompts user for overwrite confirmation if it does. + * @param path The file or directory path to check + * @param itemName The name of the item for display in the message + * @returns Promise - true if should proceed (path doesn't exist or user confirmed overwrite) + */ +export async function handleOverwriteConflict(location: vscode.Uri): Promise { + if (!(await fs.exists(location))) { + return true + } + + const choice = showConfirmationMessage({ + prompt: localize( + 'AWS.toolkit.confirmOverwrite', + '{0} already exists in the selected directory, overwrite?', + location.fsPath + ), + confirm: localize('AWS.generic.overwrite', 'Yes'), + cancel: localize('AWS.generic.cancel', 'No'), + type: 'warning', + }) + + if (!choice) { + throw new ToolkitError(`Folder already exists: ${path.basename(location.fsPath)}`) + } + + try { + await fs.delete(location, { recursive: true, force: true }) + } catch (error) { + throw ToolkitError.chain(error, `Failed to delete existing folder: ${path.basename(location.fsPath)}`) + } + + return true +} + /** * Shows a modal confirmation (warning) message with buttons to confirm or cancel. * diff --git a/packages/core/src/shared/utilities/pathFind.ts b/packages/core/src/shared/utilities/pathFind.ts index 04622733a66..a0eea9e38ae 100644 --- a/packages/core/src/shared/utilities/pathFind.ts +++ b/packages/core/src/shared/utilities/pathFind.ts @@ -18,6 +18,7 @@ let vscPath: string let sshPath: string let gitPath: string let bashPath: string +let javaPath: string const pathMap = new Map() /** @@ -145,6 +146,44 @@ export async function findSshPath(useCache: boolean = true): Promise { + if (javaPath !== undefined) { + return javaPath + } + + const paths = [ + 'java', // Try $PATH first + '/usr/bin/java', + '/usr/local/bin/java', + '/opt/java/bin/java', + // Common Oracle JDK locations + '/usr/lib/jvm/default-java/bin/java', + '/usr/lib/jvm/java-11-openjdk/bin/java', + '/usr/lib/jvm/java-8-openjdk/bin/java', + // Windows locations + 'C:/Program Files/Java/jre1.8.0_301/bin/java.exe', + 'C:/Program Files/Java/jdk1.8.0_301/bin/java.exe', + 'C:/Program Files/OpenJDK/openjdk-11.0.2/bin/java.exe', + 'C:/Program Files (x86)/Java/jre1.8.0_301/bin/java.exe', + 'C:/Program Files (x86)/Java/jdk1.8.0_301/bin/java.exe', + // macOS locations + '/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands/java', + '/usr/libexec/java_home', + ] + for (const p of paths) { + if (!p || ('java' !== p && !(await fs.exists(p)))) { + continue + } + if (await tryRun(p, ['-version'])) { + javaPath = p + return p + } + } +} + /** * Gets the configured `git` path, or falls back to "ssh" (not absolute), * or tries common locations, or returns undefined. diff --git a/packages/core/src/shared/utilities/proxyUtil.ts b/packages/core/src/shared/utilities/proxyUtil.ts index 5c37c5e3e46..e617bcd85c3 100644 --- a/packages/core/src/shared/utilities/proxyUtil.ts +++ b/packages/core/src/shared/utilities/proxyUtil.ts @@ -8,7 +8,10 @@ import { getLogger } from '../logger/logger' interface ProxyConfig { proxyUrl: string | undefined + noProxy: string | undefined + proxyStrictSSL: boolean | true certificateAuthority: string | undefined + isProxyAndCertAutoDiscoveryEnabled: boolean } /** @@ -23,11 +26,11 @@ export class ProxyUtil { * See documentation here for setting the environement variables which are inherited by Flare LS process: * https://github.com/aws/language-server-runtimes/blob/main/runtimes/docs/proxy.md */ - public static configureProxyForLanguageServer(): void { + public static async configureProxyForLanguageServer(): Promise { try { const proxyConfig = this.getProxyConfiguration() - this.setProxyEnvironmentVariables(proxyConfig) + await this.setProxyEnvironmentVariables(proxyConfig) } catch (err) { this.logger.error(`Failed to configure proxy: ${err}`) } @@ -41,21 +44,35 @@ export class ProxyUtil { const proxyUrl = httpConfig.get('proxy') this.logger.debug(`Proxy URL Setting in VSCode Settings: ${proxyUrl}`) + const noProxy = httpConfig.get('noProxy') + if (noProxy) { + this.logger.info(`Using noProxy from VS Code settings: ${noProxy}`) + } + + const proxyStrictSSL = httpConfig.get('proxyStrictSSL', true) + 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, } } /** * Sets environment variables based on proxy configuration */ - private static setProxyEnvironmentVariables(config: ProxyConfig): void { + private static async setProxyEnvironmentVariables(config: ProxyConfig): Promise { + // 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 if (proxyUrl) { @@ -64,7 +81,22 @@ export class ProxyUtil { this.logger.debug(`Set proxy environment variables: ${proxyUrl}`) } - // Set certificate bundle environment variables if configured + // set NO_PROXY vals + const noProxy = config.noProxy + if (noProxy) { + process.env.NO_PROXY = noProxy + this.logger.debug(`Set NO_PROXY environment variable: ${noProxy}`) + } + + const strictSSL = config.proxyStrictSSL + // Handle SSL certificate verification + if (!strictSSL) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' + this.logger.info('SSL verification disabled via VS Code settings') + return // No need to set CA certs when SSL verification is disabled + } + + // Set certificate bundle environment variables if user configured if (config.certificateAuthority) { process.env.NODE_EXTRA_CA_CERTS = config.certificateAuthority process.env.AWS_CA_BUNDLE = config.certificateAuthority diff --git a/packages/core/src/shared/utilities/resourceCache.ts b/packages/core/src/shared/utilities/resourceCache.ts index c0beee61cd6..a399dea66ca 100644 --- a/packages/core/src/shared/utilities/resourceCache.ts +++ b/packages/core/src/shared/utilities/resourceCache.ts @@ -60,6 +60,21 @@ export abstract class CachedResource { abstract resourceProvider(): Promise async getResource(): Promise { + // Check cache without locking first + const quickCheck = this.readCacheOrDefault() + if (quickCheck.resource.result && !quickCheck.resource.locked) { + const duration = now() - quickCheck.resource.timestamp + if (duration < this.expirationInMilli) { + logger.debug( + `cache hit (fast path), duration(%sms) is less than expiration(%sms), returning cached value: %s`, + duration, + this.expirationInMilli, + this.key + ) + return quickCheck.resource.result + } + } + const cachedValue = await this.tryLoadResourceAndLock() const resource = cachedValue?.resource diff --git a/packages/core/src/shared/utilities/workspaceUtils.ts b/packages/core/src/shared/utilities/workspaceUtils.ts index 12cce75b3ff..122f2a185f4 100644 --- a/packages/core/src/shared/utilities/workspaceUtils.ts +++ b/packages/core/src/shared/utilities/workspaceUtils.ts @@ -19,7 +19,6 @@ import * as parser from '@gerhobbelt/gitignore-parser' import fs from '../fs/fs' import { ChildProcess } from './processUtils' import { isWin } from '../vscode/env' -import { maxRepoSizeBytes } from '../../amazonqFeatureDev/constants' type GitIgnoreRelativeAcceptor = { folderPath: string @@ -378,6 +377,8 @@ export async function collectFiles( const includeContent = options?.includeContent ?? true const maxFileSizeBytes = options?.maxFileSizeBytes ?? 1024 * 1024 * 10 + // Max allowed size for file collection + const maxRepoSizeBytes = 200 * 1024 * 1024 const excludeByGitIgnore = options?.excludeByGitIgnore ?? true const failOnLimit = options?.failOnLimit ?? true const inputExcludePatterns = options?.excludePatterns ?? defaultExcludePatterns diff --git a/packages/core/src/shared/vscode/env.ts b/packages/core/src/shared/vscode/env.ts index 02d46ae6695..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). @@ -125,14 +125,133 @@ export function isRemoteWorkspace(): boolean { } /** - * There is Amazon Linux 2. + * Parses an os-release file according to the freedesktop.org standard. * - * Use {@link isCloudDesktop()} to know if we are specifically using internal Amazon Linux 2. + * @param content The content of the os-release file + * @returns A record of key-value pairs from the os-release file * - * Example: `5.10.220-188.869.amzn2int.x86_64` or `5.10.236-227.928.amzn2.x86_64` (Cloud Dev Machine) + * @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 + */ +export function hasSageMakerEnvVars(): boolean { + // Check both old and new environment variable names + // SageMaker is renaming their environment variables in their Docker images + return ( + // Original environment variables + process.env.SAGEMAKER_APP_TYPE !== undefined || + process.env.SAGEMAKER_INTERNAL_IMAGE_URI !== undefined || + process.env.STUDIO_LOGGING_DIR?.includes('/var/log/studio') === true || + // New environment variables (update these with the actual new names) + process.env.SM_APP_TYPE !== undefined || + process.env.SM_INTERNAL_IMAGE_URI !== undefined || + process.env.SERVICE_NAME === 'SageMakerUnifiedStudio' + ) +} + +/** + * Checks if the current environment is running on Amazon Linux 2. + * + * 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) + * + * 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() { - return (os.release().includes('.amzn2int.') || os.release().includes('.amzn2.')) && process.platform === 'linux' + // 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 container is AL2 + if (hasSageMakerEnvVars()) { + 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 files, we cannot determine AL2 status + getLogger().error(`Checking os-release files failed: ${e}`) + } + + // 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 } /** @@ -174,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' @@ -264,6 +383,15 @@ export async function getMachineId(): Promise { // TODO: use `vscode.env.machineId` instead? return 'browser' } + // Eclipse Che-based envs (backing compute rotates, not classified as a web instance) + // TODO: use `vscode.env.machineId` instead? + if (process.env.CHE_WORKSPACE_ID) { + return process.env.CHE_WORKSPACE_ID + } + // RedHat Dev Workspaces (run some VSC web variant) + if (process.env.DEVWORKSPACE_ID) { + return process.env.DEVWORKSPACE_ID + } const proc = new ChildProcess('hostname', [], { collect: true, logging: 'no' }) // TODO: check exit code. return (await proc.run()).stdout.trim() ?? 'unknown-host' diff --git a/packages/core/src/shared/vscode/setContext.ts b/packages/core/src/shared/vscode/setContext.ts index b630a708d15..3d45d93e14a 100644 --- a/packages/core/src/shared/vscode/setContext.ts +++ b/packages/core/src/shared/vscode/setContext.ts @@ -29,6 +29,9 @@ export type contextKey = | 'aws.toolkit.amazonqInstall.dismissed' | 'aws.stepFunctions.isWorkflowStudioFocused' | 'aws.toolkit.notifications.show' + | 'aws.amazonq.editSuggestionActive' + | 'aws.smus.connected' + | 'aws.smus.inSmusSpaceEnvironment' // Deprecated/legacy names. New keys should start with "aws.". | 'codewhisperer.activeLine' | 'gumby.isPlanAvailable' @@ -39,6 +42,7 @@ export type contextKey = | 'gumby.wasQCodeTransformationUsed' | 'amazonq.inline.codelensShortcutEnabled' | 'aws.toolkit.lambda.walkthroughSelected' + | 'aws.amazonq.amazonqChatLSP.isFocus' const contextMap: Partial> = {} diff --git a/packages/core/src/shared/vscode/uriHandler.ts b/packages/core/src/shared/vscode/uriHandler.ts index 24be2b26321..c8beda72fc4 100644 --- a/packages/core/src/shared/vscode/uriHandler.ts +++ b/packages/core/src/shared/vscode/uriHandler.ts @@ -46,7 +46,8 @@ export class UriHandler implements vscode.UriHandler { const { handler, parser } = this.handlers.get(uri.path)! let parsedQuery: Parameters[0] - const url = new URL(uri.toString(true)) + // 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')) 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/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/activation.ts b/packages/core/src/stepFunctions/activation.ts index 4898fc36b54..ab37cb7a09a 100644 --- a/packages/core/src/stepFunctions/activation.ts +++ b/packages/core/src/stepFunctions/activation.ts @@ -96,7 +96,7 @@ async function registerStepFunctionCommands( }), Commands.register('aws.stepfunctions.publishStateMachine', async (node?: any) => { const region: string | undefined = node?.regionCode - await publishStateMachine(awsContext, outputChannel, region) + await publishStateMachine({ awsContext: awsContext, outputChannel: outputChannel, region: region }) }) ) } diff --git a/packages/core/src/stepFunctions/commands/downloadStateMachineDefinition.ts b/packages/core/src/stepFunctions/commands/downloadStateMachineDefinition.ts index f26fc8a793b..e89dad1ffcd 100644 --- a/packages/core/src/stepFunctions/commands/downloadStateMachineDefinition.ts +++ b/packages/core/src/stepFunctions/commands/downloadStateMachineDefinition.ts @@ -7,10 +7,10 @@ import * as nls from 'vscode-nls' import * as os from 'os' const localize = nls.loadMessageBundle() -import { StepFunctions } from 'aws-sdk' +import * as StepFunctions from '@aws-sdk/client-sfn' import * as path from 'path' import * as vscode from 'vscode' -import { DefaultStepFunctionsClient, StepFunctionsClient } from '../../shared/clients/stepFunctionsClient' +import { StepFunctionsClient } from '../../shared/clients/stepFunctions' import { getLogger, Logger } from '../../shared/logger/logger' import { Result } from '../../shared/telemetry/telemetry' @@ -28,10 +28,11 @@ export async function downloadStateMachineDefinition(params: { let downloadResult: Result = 'Succeeded' const stateMachineName = params.stateMachineNode.details.name try { - const client: StepFunctionsClient = new DefaultStepFunctionsClient(params.stateMachineNode.regionCode) - const stateMachineDetails: StepFunctions.DescribeStateMachineOutput = await client.getStateMachineDetails( - params.stateMachineNode.details.stateMachineArn - ) + const client: StepFunctionsClient = new StepFunctionsClient(params.stateMachineNode.regionCode) + const stateMachineDetails: StepFunctions.DescribeStateMachineCommandOutput = + await client.getStateMachineDetails({ + stateMachineArn: params.stateMachineNode.details.stateMachineArn, + }) if (params.isPreviewAndRender) { const doc = await vscode.workspace.openTextDocument({ @@ -53,7 +54,7 @@ export async function downloadStateMachineDefinition(params: { if (fileInfo) { const filePath = fileInfo.fsPath - await fs.writeFile(filePath, stateMachineDetails.definition, 'utf8') + await fs.writeFile(filePath, stateMachineDetails.definition || '', 'utf8') const openPath = vscode.Uri.file(filePath) const doc = await vscode.workspace.openTextDocument(openPath) await vscode.window.showTextDocument(doc) diff --git a/packages/core/src/stepFunctions/commands/publishStateMachine.ts b/packages/core/src/stepFunctions/commands/publishStateMachine.ts index e07b21b86f2..799d5b2a724 100644 --- a/packages/core/src/stepFunctions/commands/publishStateMachine.ts +++ b/packages/core/src/stepFunctions/commands/publishStateMachine.ts @@ -7,7 +7,7 @@ import { load } from 'js-yaml' import * as vscode from 'vscode' import * as nls from 'vscode-nls' import { AwsContext } from '../../shared/awsContext' -import { DefaultStepFunctionsClient, StepFunctionsClient } from '../../shared/clients/stepFunctionsClient' +import { StepFunctionsClient } from '../../shared/clients/stepFunctions' import { getLogger, Logger } from '../../shared/logger/logger' import { showViewLogsMessage } from '../../shared/utilities/messages' import { VALID_SFN_PUBLISH_FORMATS, YAML_FORMATS } from '../constants/aslFormats' @@ -15,14 +15,21 @@ import { refreshStepFunctionsTree } from '../explorer/stepFunctionsNodes' import { PublishStateMachineWizard, PublishStateMachineWizardState } from '../wizards/publishStateMachineWizard' const localize = nls.loadMessageBundle() -export async function publishStateMachine( - awsContext: AwsContext, - outputChannel: vscode.OutputChannel, +interface publishStateMachineParams { + awsContext: AwsContext + outputChannel: vscode.OutputChannel region?: string -) { + text?: vscode.TextDocument +} +export async function publishStateMachine(params: publishStateMachineParams) { const logger: Logger = getLogger() + let textDocument: vscode.TextDocument | undefined - const textDocument = vscode.window.activeTextEditor?.document + if (params.text) { + textDocument = params.text + } else { + textDocument = vscode.window.activeTextEditor?.document + } if (!textDocument) { logger.error('Could not get active text editor for state machine definition') @@ -53,17 +60,17 @@ export async function publishStateMachine( } try { - const response = await new PublishStateMachineWizard(region).run() + const response = await new PublishStateMachineWizard(params.region).run() if (!response) { return } - const client = new DefaultStepFunctionsClient(response.region) + const client = new StepFunctionsClient(response.region) if (response?.createResponse) { - await createStateMachine(response.createResponse, text, outputChannel, response.region, client) + await createStateMachine(response.createResponse, text, params.outputChannel, response.region, client) refreshStepFunctionsTree(response.region) } else if (response?.updateResponse) { - await updateStateMachine(response.updateResponse, text, outputChannel, response.region, client) + await updateStateMachine(response.updateResponse, text, params.outputChannel, response.region, client) } } catch (err) { logger.error(err as Error) @@ -102,7 +109,7 @@ async function createStateMachine( wizardResponse.name ) ) - outputChannel.appendLine(result.stateMachineArn) + outputChannel.appendLine(result.stateMachineArn || '') logger.info(`Created "${result.stateMachineArn}"`) } catch (err) { const msg = localize( diff --git a/packages/core/src/stepFunctions/explorer/stepFunctionsNodes.ts b/packages/core/src/stepFunctions/explorer/stepFunctionsNodes.ts index 2fcf62a9eb6..8b693d3bb9e 100644 --- a/packages/core/src/stepFunctions/explorer/stepFunctionsNodes.ts +++ b/packages/core/src/stepFunctions/explorer/stepFunctionsNodes.ts @@ -7,9 +7,9 @@ import * as os from 'os' import * as nls from 'vscode-nls' const localize = nls.loadMessageBundle() -import { StepFunctions } from 'aws-sdk' +import * as StepFunctions from '@aws-sdk/client-sfn' import * as vscode from 'vscode' -import { DefaultStepFunctionsClient } from '../../shared/clients/stepFunctionsClient' +import { StepFunctionsClient } from '../../shared/clients/stepFunctions' import { AWSResourceNode } from '../../shared/treeview/nodes/awsResourceNode' import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase' @@ -40,7 +40,7 @@ export class StepFunctionsNode extends AWSTreeNodeBase { public constructor( public override readonly regionCode: string, - private readonly client = new DefaultStepFunctionsClient(regionCode) + private readonly client = new StepFunctionsClient(regionCode) ) { super('Step Functions', vscode.TreeItemCollapsibleState.Collapsed) this.stateMachineNodes = new Map() @@ -101,7 +101,7 @@ export class StateMachineNode extends AWSTreeNodeBase implements AWSResourceNode } public get arn(): string { - return this.details.stateMachineArn + return this.details.stateMachineArn || '' } public get name(): string { diff --git a/packages/core/src/stepFunctions/utils.ts b/packages/core/src/stepFunctions/utils.ts index fedea23acc5..f578d6cda86 100644 --- a/packages/core/src/stepFunctions/utils.ts +++ b/packages/core/src/stepFunctions/utils.ts @@ -5,10 +5,10 @@ import * as nls from 'vscode-nls' const localize = nls.loadMessageBundle() -import { StepFunctions } from 'aws-sdk' +import * as StepFunctions from '@aws-sdk/client-sfn' import * as yaml from 'js-yaml' import * as vscode from 'vscode' -import { StepFunctionsClient } from '../shared/clients/stepFunctionsClient' +import { StepFunctionsClient } from '../shared/clients/stepFunctions' import { DiagnosticSeverity, DocumentLanguageSettings, diff --git a/packages/core/src/stepFunctions/vue/executeStateMachine/executeStateMachine.ts b/packages/core/src/stepFunctions/vue/executeStateMachine/executeStateMachine.ts index 985f2494e52..b4e47bc65f6 100644 --- a/packages/core/src/stepFunctions/vue/executeStateMachine/executeStateMachine.ts +++ b/packages/core/src/stepFunctions/vue/executeStateMachine/executeStateMachine.ts @@ -6,7 +6,7 @@ import * as nls from 'vscode-nls' const localize = nls.loadMessageBundle() -import { DefaultStepFunctionsClient } from '../../../shared/clients/stepFunctionsClient' +import { StepFunctionsClient } from '../../../shared/clients/stepFunctions' import { getLogger } from '../../../shared/logger/logger' import { Result } from '../../../shared/telemetry/telemetry' @@ -56,11 +56,14 @@ export class ExecuteStateMachineWebview extends VueWebview { this.channel.appendLine('') try { - const client = new DefaultStepFunctionsClient(this.stateMachine.region) - const startExecResponse = await client.executeStateMachine(this.stateMachine.arn, input) + const client = new StepFunctionsClient(this.stateMachine.region) + const startExecResponse = await client.executeStateMachine({ + stateMachineArn: this.stateMachine.arn, + input, + }) this.logger.info('started execution for Step Functions State Machine') this.channel.appendLine(localize('AWS.stepFunctions.executeStateMachine.info.started', 'Execution started')) - this.channel.appendLine(startExecResponse.executionArn) + this.channel.appendLine(startExecResponse.executionArn || '') } catch (e) { executeResult = 'Failed' const error = e as Error @@ -82,8 +85,8 @@ const Panel = VueWebview.compilePanel(ExecuteStateMachineWebview) export async function executeStateMachine(context: ExtContext, node: StateMachineNode): Promise { const wv = new Panel(context.extensionContext, context.outputChannel, { - arn: node.details.stateMachineArn, - name: node.details.name, + arn: node.details.stateMachineArn || '', + name: node.details.name || '', region: node.regionCode, }) diff --git a/packages/core/src/stepFunctions/wizards/publishStateMachineWizard.ts b/packages/core/src/stepFunctions/wizards/publishStateMachineWizard.ts index 866d7f5bb03..ed141ac1894 100644 --- a/packages/core/src/stepFunctions/wizards/publishStateMachineWizard.ts +++ b/packages/core/src/stepFunctions/wizards/publishStateMachineWizard.ts @@ -22,7 +22,7 @@ import { Wizard, WIZARD_BACK } from '../../shared/wizards/wizard' import { isStepFunctionsRole } from '../utils' import { createRolePrompter } from '../../shared/ui/common/roles' import { IamClient } from '../../shared/clients/iam' -import { DefaultStepFunctionsClient } from '../../shared/clients/stepFunctionsClient' +import { StepFunctionsClient } from '../../shared/clients/stepFunctions' export enum PublishStateMachineAction { QuickCreate, @@ -109,14 +109,14 @@ function createStepFunctionsRolePrompter(region: string) { } async function* listStateMachines(region: string) { - const client = new DefaultStepFunctionsClient(region) + const client = new StepFunctionsClient(region) for await (const machine of client.listStateMachines()) { yield [ { - label: machine.name, - data: machine.stateMachineArn, - description: machine.stateMachineArn, + label: machine.name || '', + data: machine.stateMachineArn || '', + description: machine.stateMachineArn || '', }, ] } diff --git a/packages/core/src/stepFunctions/workflowStudio/handleMessage.ts b/packages/core/src/stepFunctions/workflowStudio/handleMessage.ts index 13477db19e2..2b548ab957f 100644 --- a/packages/core/src/stepFunctions/workflowStudio/handleMessage.ts +++ b/packages/core/src/stepFunctions/workflowStudio/handleMessage.ts @@ -202,15 +202,22 @@ async function saveFileMessageHandler(request: SaveFileRequestMessage, context: } /** - * Handler for saving a file and starting the state machine deployment flow, while also switching to default editor. + * Handler for saving a file and starting the state machine deployment flow while staying in WFS view. * Triggered when the user triggers 'Save and Deploy' action in WFS * @param request The request message containing the file contents. * @param context The webview context containing the necessary information for saving the file. */ async function saveFileAndDeployMessageHandler(request: SaveFileRequestMessage, context: WebviewContext) { await saveFileMessageHandler(request, context) - await closeCustomEditorMessageHandler(context) - await publishStateMachine(globals.awsContext, globals.outputChannel) + await publishStateMachine({ + awsContext: globals.awsContext, + outputChannel: globals.outputChannel, + text: context.textDocument, + }) + + telemetry.ui_click.emit({ + elementId: 'stepfunctions_saveAndDeploy', + }) } /** diff --git a/packages/core/src/stepFunctions/workflowStudio/types.ts b/packages/core/src/stepFunctions/workflowStudio/types.ts index 989ef4517d6..22a93a8404d 100644 --- a/packages/core/src/stepFunctions/workflowStudio/types.ts +++ b/packages/core/src/stepFunctions/workflowStudio/types.ts @@ -2,7 +2,8 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ -import { IAM, StepFunctions } from 'aws-sdk' +import { ListRolesCommandInput } from '@aws-sdk/client-iam' +import { TestStateInput } from '@aws-sdk/client-sfn' import * as vscode from 'vscode' export enum WorkflowMode { @@ -93,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/stepFunctions/workflowStudio/workflowStudioApiHandler.ts b/packages/core/src/stepFunctions/workflowStudio/workflowStudioApiHandler.ts index 8b7244cd844..6c0fb850b9d 100644 --- a/packages/core/src/stepFunctions/workflowStudio/workflowStudioApiHandler.ts +++ b/packages/core/src/stepFunctions/workflowStudio/workflowStudioApiHandler.ts @@ -3,9 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { StepFunctions } from 'aws-sdk' +import * as StepFunctions from '@aws-sdk/client-sfn' import { IamClient, IamRole } from '../../shared/clients/iam' -import { DefaultStepFunctionsClient } from '../../shared/clients/stepFunctionsClient' +import { StepFunctionsClient } from '../../shared/clients/stepFunctions' import { ApiAction, ApiCallRequestMessage, Command, MessageType, WebviewContext } from './types' import { telemetry } from '../../shared/telemetry/telemetry' import { ListRolesRequest } from '@aws-sdk/client-iam' @@ -15,7 +15,7 @@ export class WorkflowStudioApiHandler { region: string, private readonly context: WebviewContext, private readonly clients = { - sfn: new DefaultStepFunctionsClient(region), + sfn: new StepFunctionsClient(region), iam: new IamClient(region), } ) {} diff --git a/packages/core/src/stepFunctions/workflowStudio/workflowStudioEditor.ts b/packages/core/src/stepFunctions/workflowStudio/workflowStudioEditor.ts index da1a9f2e9bf..ba719856516 100644 --- a/packages/core/src/stepFunctions/workflowStudio/workflowStudioEditor.ts +++ b/packages/core/src/stepFunctions/workflowStudio/workflowStudioEditor.ts @@ -147,16 +147,18 @@ export class WorkflowStudioEditor { // The text document acts as our model, thus we send and event to the webview on file save to trigger update contextObject.disposables.push( - vscode.workspace.onDidSaveTextDocument(async () => { - await telemetry.stepfunctions_saveFile.run(async (span) => { - span.record({ - id: contextObject.fileId, - saveType: 'MANUAL_SAVE', - source: 'VSCODE', - isInvalidJson: isInvalidJsonFile(contextObject.textDocument), + vscode.workspace.onDidSaveTextDocument(async (savedDocument) => { + if (savedDocument.uri.toString() === this.documentUri.toString()) { + await telemetry.stepfunctions_saveFile.run(async (span) => { + span.record({ + id: contextObject.fileId, + saveType: 'MANUAL_SAVE', + source: 'VSCODE', + isInvalidJson: isInvalidJsonFile(contextObject.textDocument), + }) + await broadcastFileChange(contextObject, 'MANUAL_SAVE') }) - await broadcastFileChange(contextObject, 'MANUAL_SAVE') - }) + } }) ) diff --git a/packages/core/src/test/amazonq/common/diff.test.ts b/packages/core/src/test/amazonq/common/diff.test.ts index 0fc81403a59..a8f3bea8747 100644 --- a/packages/core/src/test/amazonq/common/diff.test.ts +++ b/packages/core/src/test/amazonq/common/diff.test.ts @@ -13,7 +13,6 @@ import * as path from 'path' import * as vscode from 'vscode' import sinon from 'sinon' import { FileSystem } from '../../../shared/fs/fs' -import { featureDevScheme } from '../../../amazonqFeatureDev' import { createAmazonQUri, getFileDiffUris, @@ -28,6 +27,7 @@ describe('diff', () => { const filePath = path.join('/', 'foo', 'fi') const rightPath = path.join('foo', 'fee') const tabId = '0' + const featureDevScheme = 'aws-featureDev' let sandbox: sinon.SinonSandbox let executeCommandSpy: sinon.SinonSpy diff --git a/packages/core/src/test/amazonq/session/sessionState.test.ts b/packages/core/src/test/amazonq/session/sessionState.test.ts deleted file mode 100644 index dcff3398cea..00000000000 --- a/packages/core/src/test/amazonq/session/sessionState.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import sinon from 'sinon' -import { CodeGenBase } from '../../../amazonq/session/sessionState' -import { RunCommandLogFileName } from '../../../amazonq/session/sessionState' -import assert from 'assert' -import * as workspaceUtils from '../../../shared/utilities/workspaceUtils' -import { TelemetryHelper } from '../../../amazonq/util/telemetryHelper' -import { assertLogsContain } from '../../globalSetup.test' - -describe('CodeGenBase generateCode log file handling', () => { - class TestCodeGen extends CodeGenBase { - public generatedFiles: any[] = [] - constructor(config: any, tabID: string) { - super(config, tabID) - } - protected handleProgress(_messenger: any): void { - // No-op for test. - } - protected getScheme(): string { - return 'file' - } - protected getTimeoutErrorCode(): string { - return 'test_timeout' - } - protected handleGenerationComplete(_messenger: any, newFileInfo: any[]): void { - this.generatedFiles = newFileInfo - } - protected handleError(_messenger: any, _codegenResult: any): Error { - throw new Error('handleError called') - } - } - - let fakeProxyClient: any - let testConfig: any - let fsMock: any - let messengerMock: any - let testAction: any - - beforeEach(async () => { - const ret = { - testworkspacefolder: { - uri: vscode.Uri.file('/path/to/testworkspacefolder'), - name: 'testworkspacefolder', - index: 0, - }, - } - sinon.stub(workspaceUtils, 'getWorkspaceFoldersByPrefixes').returns(ret) - - fakeProxyClient = { - getCodeGeneration: sinon.stub().resolves({ - codeGenerationStatus: { status: 'Complete' }, - codeGenerationRemainingIterationCount: 0, - codeGenerationTotalIterationCount: 1, - }), - exportResultArchive: sinon.stub(), - } - - testConfig = { - conversationId: 'conv_test', - uploadId: 'upload_test', - workspaceRoots: ['/path/to/testworkspacefolder'], - proxyClient: fakeProxyClient, - } - - fsMock = { - writeFile: sinon.stub().resolves(), - registerProvider: sinon.stub().resolves(), - } - - messengerMock = { sendAnswer: sinon.spy() } - - testAction = { - fs: fsMock, - messenger: messengerMock, - tokenSource: { - token: { - isCancellationRequested: false, - onCancellationRequested: () => {}, - }, - }, - } - }) - - afterEach(() => { - sinon.restore() - }) - - const runGenerateCode = async (codeGenerationId: string) => { - const testCodeGen = new TestCodeGen(testConfig, 'tab1') - return await testCodeGen.generateCode({ - messenger: messengerMock, - fs: fsMock, - codeGenerationId, - telemetry: new TelemetryHelper(), - workspaceFolders: [testConfig.workspaceRoots[0]], - action: testAction, - }) - } - - const createExpectedNewFile = (fileObj: { zipFilePath: string; fileContent: string }) => ({ - zipFilePath: fileObj.zipFilePath, - fileContent: fileObj.fileContent, - changeApplied: false, - rejected: false, - relativePath: fileObj.zipFilePath, - virtualMemoryUri: vscode.Uri.file(`/upload_test/${fileObj.zipFilePath}`), - workspaceFolder: { - index: 0, - name: 'testworkspacefolder', - uri: vscode.Uri.file('/path/to/testworkspacefolder'), - }, - }) - - it('adds the log content to logger if present and excludes it from new files', async () => { - const logFileInfo = { - zipFilePath: RunCommandLogFileName, - fileContent: 'Log content', - } - const otherFile = { zipFilePath: 'other.ts', fileContent: 'other content' } - fakeProxyClient.exportResultArchive.resolves({ - newFileContents: [logFileInfo, otherFile], - deletedFiles: [], - references: [], - }) - const result = await runGenerateCode('codegen1') - - assertLogsContain(`sessionState: Run Command logs, Log content`, false, 'info') - - const expectedNewFile = createExpectedNewFile(otherFile) - assert.deepStrictEqual(result.newFiles[0].fileContent, expectedNewFile.fileContent) - }) - - it('skips log file handling if log file is not present', async () => { - const file1 = { zipFilePath: 'file1.ts', fileContent: 'content1' } - fakeProxyClient.exportResultArchive.resolves({ - newFileContents: [file1], - deletedFiles: [], - references: [], - }) - - const result = await runGenerateCode('codegen2') - - assert.throws(() => assertLogsContain(`sessionState: Run Command logs, Log content`, false, 'info')) - - const expectedNewFile = createExpectedNewFile(file1) - assert.deepStrictEqual(result.newFiles[0].fileContent, expectedNewFile.fileContent) - }) -}) diff --git a/packages/core/src/test/amazonq/session/testSetup.ts b/packages/core/src/test/amazonq/session/testSetup.ts deleted file mode 100644 index 76f2c90f94f..00000000000 --- a/packages/core/src/test/amazonq/session/testSetup.ts +++ /dev/null @@ -1,74 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import sinon from 'sinon' -import { createBasicTestConfig, createMockSessionStateConfig, TestSessionMocks } from '../utils' -import { SessionStateConfig } from '../../../amazonq' - -export function createSessionTestSetup() { - const conversationId = 'conversation-id' - const uploadId = 'upload-id' - const tabId = 'tab-id' - const currentCodeGenerationId = '' - - return { - conversationId, - uploadId, - tabId, - currentCodeGenerationId, - } -} - -export async function createTestConfig( - testMocks: TestSessionMocks, - conversationId: string, - uploadId: string, - currentCodeGenerationId: string -) { - testMocks.getCodeGeneration = sinon.stub() - testMocks.exportResultArchive = sinon.stub() - testMocks.createUploadUrl = sinon.stub() - const basicConfig = await createBasicTestConfig(conversationId, uploadId, currentCodeGenerationId) - const testConfig = createMockSessionStateConfig(basicConfig, testMocks) - return testConfig -} - -export interface TestContext { - conversationId: string - uploadId: string - tabId: string - currentCodeGenerationId: string - testConfig: SessionStateConfig - testMocks: Record -} - -export function createTestContext(): TestContext { - const { conversationId, uploadId, tabId, currentCodeGenerationId } = createSessionTestSetup() - - return { - conversationId, - uploadId, - tabId, - currentCodeGenerationId, - testConfig: {} as SessionStateConfig, - testMocks: {}, - } -} - -export function setupTestHooks(context: TestContext) { - beforeEach(async () => { - context.testMocks = {} - context.testConfig = await createTestConfig( - context.testMocks, - context.conversationId, - context.uploadId, - context.currentCodeGenerationId - ) - }) - - afterEach(() => { - sinon.restore() - }) -} diff --git a/packages/core/src/test/amazonq/utils.ts b/packages/core/src/test/amazonq/utils.ts deleted file mode 100644 index ec2e7020e4e..00000000000 --- a/packages/core/src/test/amazonq/utils.ts +++ /dev/null @@ -1,182 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import * as sinon from 'sinon' -import { MessagePublisher } from '../../amazonq/messages/messagePublisher' -import { ChatControllerEventEmitters, FeatureDevController } from '../../amazonqFeatureDev/controllers/chat/controller' -import { FeatureDevChatSessionStorage } from '../../amazonqFeatureDev/storages/chatSession' -import { createTestWorkspaceFolder } from '../testUtil' -import { Session } from '../../amazonqFeatureDev/session/session' -import { SessionState, SessionStateAction, SessionStateConfig } from '../../amazonq/commons/types' -import { FeatureDevClient } from '../../amazonqFeatureDev/client/featureDev' -import { VirtualMemoryFile } from '../../shared/virtualMemoryFile' -import path from 'path' -import { featureDevChat } from '../../amazonqFeatureDev/constants' -import { Messenger } from '../../amazonq/commons/connector/baseMessenger' -import { AppToWebViewMessageDispatcher } from '../../amazonq/commons/connector/connectorMessages' -import { createSessionConfig } from '../../amazonq/commons/session/sessionConfigFactory' -import { VirtualFileSystem } from '../../shared' -import { TelemetryHelper } from '../../amazonq/util/telemetryHelper' -import { FeatureClient } from '../../amazonq/client/client' - -export function createMessenger(): Messenger { - return new Messenger( - new AppToWebViewMessageDispatcher(new MessagePublisher(sinon.createStubInstance(vscode.EventEmitter))), - featureDevChat - ) -} - -export function createMockChatEmitters(): ChatControllerEventEmitters { - return { - processHumanChatMessage: new vscode.EventEmitter(), - followUpClicked: new vscode.EventEmitter(), - openDiff: new vscode.EventEmitter(), - processChatItemVotedMessage: new vscode.EventEmitter(), - processChatItemFeedbackMessage: new vscode.EventEmitter(), - stopResponse: new vscode.EventEmitter(), - tabOpened: new vscode.EventEmitter(), - tabClosed: new vscode.EventEmitter(), - authClicked: new vscode.EventEmitter(), - processResponseBodyLinkClick: new vscode.EventEmitter(), - insertCodeAtPositionClicked: new vscode.EventEmitter(), - fileClicked: new vscode.EventEmitter(), - storeCodeResultMessageId: new vscode.EventEmitter(), - } -} - -export interface ControllerSetup { - emitters: ChatControllerEventEmitters - workspaceFolder: vscode.WorkspaceFolder - messenger: Messenger - sessionStorage: FeatureDevChatSessionStorage -} - -export async function createSession({ - messenger, - sessionState, - scheme, - conversationID = '0', - tabID = '0', - uploadID = '0', -}: { - messenger: Messenger - scheme: string - sessionState?: Omit - conversationID?: string - tabID?: string - uploadID?: string -}) { - const sessionConfig = await createSessionConfig(scheme) - - const client = sinon.createStubInstance(FeatureDevClient) - client.createConversation.resolves(conversationID) - const session = new Session(sessionConfig, messenger, tabID, sessionState, client) - - sinon.stub(session, 'conversationId').get(() => conversationID) - sinon.stub(session, 'uploadId').get(() => uploadID) - - return session -} - -export async function sessionRegisterProvider(session: Session, uri: vscode.Uri, fileContents: Uint8Array) { - session.config.fs.registerProvider(uri, new VirtualMemoryFile(fileContents)) -} - -export function generateVirtualMemoryUri(uploadID: string, filePath: string, scheme: string) { - const generationFilePath = path.join(uploadID, filePath) - const uri = vscode.Uri.from({ scheme, path: generationFilePath }) - return uri -} - -export async function sessionWriteFile(session: Session, uri: vscode.Uri, encodedContent: Uint8Array) { - await session.config.fs.writeFile(uri, encodedContent, { - create: true, - overwrite: true, - }) -} - -export async function createController(): Promise { - const messenger = createMessenger() - - // Create a new workspace root - const testWorkspaceFolder = await createTestWorkspaceFolder() - sinon.stub(vscode.workspace, 'workspaceFolders').value([testWorkspaceFolder]) - - const sessionStorage = new FeatureDevChatSessionStorage(messenger) - - const mockChatControllerEventEmitters = createMockChatEmitters() - - new FeatureDevController( - mockChatControllerEventEmitters, - messenger, - sessionStorage, - sinon.createStubInstance(vscode.EventEmitter).event - ) - - return { - emitters: mockChatControllerEventEmitters, - workspaceFolder: testWorkspaceFolder, - messenger, - sessionStorage, - } -} - -export function createMockSessionStateAction(msg?: string): SessionStateAction { - return { - task: 'test-task', - msg: msg ?? 'test-msg', - fs: new VirtualFileSystem(), - messenger: new Messenger( - new AppToWebViewMessageDispatcher(new MessagePublisher(new vscode.EventEmitter())), - featureDevChat - ), - telemetry: new TelemetryHelper(), - uploadHistory: {}, - } -} - -export interface TestSessionMocks { - getCodeGeneration?: sinon.SinonStub - exportResultArchive?: sinon.SinonStub - createUploadUrl?: sinon.SinonStub -} - -export interface SessionTestConfig { - conversationId: string - uploadId: string - workspaceFolder: vscode.WorkspaceFolder - currentCodeGenerationId?: string -} - -export function createMockSessionStateConfig(config: SessionTestConfig, mocks: TestSessionMocks): SessionStateConfig { - return { - workspaceRoots: ['fake-source'], - workspaceFolders: [config.workspaceFolder], - conversationId: config.conversationId, - proxyClient: { - createConversation: () => sinon.stub(), - createUploadUrl: () => mocks.createUploadUrl!(), - startCodeGeneration: () => sinon.stub(), - getCodeGeneration: () => mocks.getCodeGeneration!(), - exportResultArchive: () => mocks.exportResultArchive!(), - } as unknown as FeatureClient, - uploadId: config.uploadId, - currentCodeGenerationId: config.currentCodeGenerationId, - } -} - -export async function createBasicTestConfig( - conversationId: string = 'conversation-id', - uploadId: string = 'upload-id', - currentCodeGenerationId: string = '' -): Promise { - return { - conversationId, - uploadId, - workspaceFolder: await createTestWorkspaceFolder('fake-root'), - currentCodeGenerationId, - } -} diff --git a/packages/core/src/test/amazonqDoc/controller.test.ts b/packages/core/src/test/amazonqDoc/controller.test.ts deleted file mode 100644 index d69edc47fd7..00000000000 --- a/packages/core/src/test/amazonqDoc/controller.test.ts +++ /dev/null @@ -1,577 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import * as vscode from 'vscode' -import sinon from 'sinon' -import { - assertTelemetry, - ControllerSetup, - createController, - createExpectedEvent, - createExpectedMetricData, - createSession, - EventMetrics, - FollowUpSequences, - generateVirtualMemoryUri, - updateFilePaths, -} from './utils' -import { CurrentWsFolders, MetricDataOperationName, MetricDataResult, NewFileInfo } from '../../amazonqDoc/types' -import { DocCodeGenState, docScheme, Session } from '../../amazonqDoc' -import { AuthUtil } from '../../codewhisperer' -import { - ApiClientError, - ApiServiceError, - CodeIterationLimitError, - FeatureDevClient, - getMetricResult, - MonthlyConversationLimitError, - PrepareRepoFailedError, - TabIdNotFoundError, - UploadCodeError, - UploadURLExpired, - UserMessageNotFoundError, - ZipFileError, -} from '../../amazonqFeatureDev' -import { i18n, ToolkitError, waitUntil } from '../../shared' -import { FollowUpTypes } from '../../amazonq/commons/types' -import { FileSystem } from '../../shared/fs/fs' -import { ReadmeBuilder } from './mockContent' -import * as path from 'path' -import { - ContentLengthError, - NoChangeRequiredException, - PromptRefusalException, - PromptTooVagueError, - PromptUnrelatedError, - ReadmeTooLargeError, - ReadmeUpdateTooLargeError, - WorkspaceEmptyError, -} from '../../amazonqDoc/errors' -import { LlmError } from '../../amazonq/errors' -describe('Controller - Doc Generation', () => { - const firstTabID = '123' - const firstConversationID = '123' - const firstUploadID = '123' - - const secondTabID = '456' - const secondConversationID = '456' - const secondUploadID = '456' - - let controllerSetup: ControllerSetup - let session: Session - let sendDocTelemetrySpy: sinon.SinonStub - let sendDocTelemetrySpyForSecondTab: sinon.SinonStub - let mockGetCodeGeneration: sinon.SinonStub - let getSessionStub: sinon.SinonStub - let modifiedReadme: string - const generatedReadme = ReadmeBuilder.createBaseReadme() - let sandbox: sinon.SinonSandbox - - const getFilePaths = (controllerSetup: ControllerSetup, uploadID: string): NewFileInfo[] => [ - { - zipFilePath: path.normalize('README.md'), - relativePath: path.normalize('README.md'), - fileContent: generatedReadme, - rejected: false, - virtualMemoryUri: generateVirtualMemoryUri(uploadID, path.normalize('README.md'), docScheme), - workspaceFolder: controllerSetup.workspaceFolder, - changeApplied: false, - }, - ] - - async function createCodeGenState( - sandbox: sinon.SinonSandbox, - tabID: string, - conversationID: string, - uploadID: string - ) { - mockGetCodeGeneration = sandbox.stub().resolves({ codeGenerationStatus: { status: 'Complete' } }) - - const workspaceFolders = [controllerSetup.workspaceFolder] as CurrentWsFolders - const testConfig = { - conversationId: conversationID, - proxyClient: { - createConversation: () => sandbox.stub(), - createUploadUrl: () => sandbox.stub(), - generatePlan: () => sandbox.stub(), - startCodeGeneration: () => sandbox.stub(), - getCodeGeneration: () => mockGetCodeGeneration(), - exportResultArchive: () => sandbox.stub(), - } as unknown as FeatureDevClient, - workspaceRoots: [''], - uploadId: uploadID, - workspaceFolders, - } - - const codeGenState = new DocCodeGenState( - testConfig, - getFilePaths(controllerSetup, uploadID), - [], - [], - tabID, - 0, - {} - ) - return createSession({ - messenger: controllerSetup.messenger, - sessionState: codeGenState, - conversationID, - tabID, - uploadID, - scheme: docScheme, - sandbox, - }) - } - async function fireFollowUps(followUpTypes: FollowUpTypes[], stub: sinon.SinonStub, tabID: string) { - for (const type of followUpTypes) { - controllerSetup.emitters.followUpClicked.fire({ - tabID, - followUp: { type }, - }) - await waitForStub(stub) - } - } - - async function waitForStub(stub: sinon.SinonStub) { - await waitUntil(() => Promise.resolve(stub.callCount > 0), {}) - } - - async function performAction( - action: 'generate' | 'update' | 'makeChanges' | 'accept' | 'edit', - getSessionStub: sinon.SinonStub, - message?: string, - tabID = firstTabID, - conversationID = firstConversationID - ) { - const sequences = { - generate: FollowUpSequences.generateReadme, - update: FollowUpSequences.updateReadme, - edit: FollowUpSequences.editReadme, - makeChanges: FollowUpSequences.makeChanges, - accept: FollowUpSequences.acceptContent, - } - - await fireFollowUps(sequences[action], getSessionStub, tabID) - - if ((action === 'makeChanges' || action === 'edit') && message) { - controllerSetup.emitters.processHumanChatMessage.fire({ - tabID, - conversationID, - message, - }) - await waitForStub(getSessionStub) - } - } - - async function setupTest(sandbox: sinon.SinonSandbox, isMultiTabs?: boolean, error?: ToolkitError) { - controllerSetup = await createController(sandbox) - session = await createCodeGenState(sandbox, firstTabID, firstConversationID, firstUploadID) - sendDocTelemetrySpy = sandbox.stub(session, 'sendDocTelemetryEvent').resolves() - sandbox.stub(session, 'preloader').resolves() - error ? sandbox.stub(session, 'send').throws(error) : sandbox.stub(session, 'send').resolves() - Object.defineProperty(session, '_conversationId', { - value: firstConversationID, - writable: true, - configurable: true, - }) - - sandbox.stub(AuthUtil.instance, 'getChatAuthState').resolves({ - codewhispererCore: 'connected', - codewhispererChat: 'connected', - amazonQ: 'connected', - }) - sandbox.stub(FileSystem.prototype, 'exists').resolves(false) - if (isMultiTabs) { - const secondSession = await createCodeGenState(sandbox, secondTabID, secondConversationID, secondUploadID) - sendDocTelemetrySpyForSecondTab = sandbox.stub(secondSession, 'sendDocTelemetryEvent').resolves() - sandbox.stub(secondSession, 'preloader').resolves() - sandbox.stub(secondSession, 'send').resolves() - Object.defineProperty(secondSession, '_conversationId', { - value: secondConversationID, - writable: true, - configurable: true, - }) - getSessionStub = sandbox - .stub(controllerSetup.sessionStorage, 'getSession') - .callsFake(async (tabId: string): Promise => { - if (tabId === firstTabID) { - return session - } - if (tabId === secondTabID) { - return secondSession - } - throw new Error(`Unknown tab ID: ${tabId}`) - }) - } else { - getSessionStub = sandbox.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - } - modifiedReadme = ReadmeBuilder.createReadmeWithRepoStructure() - sandbox - .stub(vscode.workspace, 'openTextDocument') - .callsFake(async (options?: string | vscode.Uri | { language?: string; content?: string }) => { - let documentPath = '' - if (typeof options === 'string') { - documentPath = options - } else if (options && 'path' in options) { - documentPath = options.path - } - - const isTempFile = documentPath === 'empty' - return { - getText: () => (isTempFile ? generatedReadme : modifiedReadme), - } as any - }) - } - - const retryTest = async ( - testMethod: () => Promise, - isMultiTabs?: boolean, - error?: ToolkitError, - maxRetries: number = 3, - delayMs: number = 1000 - ): Promise => { - let lastError: Error | undefined - - for (let attempt = 1; attempt <= maxRetries + 1; attempt++) { - sandbox = sinon.createSandbox() - sandbox.useFakeTimers({ - now: new Date('2025-03-20T12:00:00.000Z'), - toFake: ['Date'], - }) - try { - await setupTest(sandbox, isMultiTabs, error) - await testMethod() - sandbox.restore() - return - } catch (error) { - lastError = error as Error - sandbox.restore() - - if (attempt > maxRetries) { - console.error(`Test failed after ${maxRetries} retries:`, lastError) - throw lastError - } - - console.log(`Test attempt ${attempt} failed, retrying...`) - await new Promise((resolve) => setTimeout(resolve, delayMs)) - } - } - } - - after(() => { - if (sandbox) { - sandbox.restore() - } - }) - - it('should emit generation telemetry for initial README generation', async () => { - await retryTest(async () => { - await performAction('generate', getSessionStub) - - const expectedEvent = createExpectedEvent({ - type: 'generation', - ...EventMetrics.INITIAL_README, - interactionType: 'GENERATE_README', - conversationId: firstConversationID, - }) - - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent, - type: 'generation', - sandbox, - }) - }) - }) - it('should emit another generation telemetry for make changes operation after initial README generation', async () => { - await retryTest(async () => { - await performAction('generate', getSessionStub) - const firstExpectedEvent = createExpectedEvent({ - type: 'generation', - ...EventMetrics.INITIAL_README, - interactionType: 'GENERATE_README', - conversationId: firstConversationID, - }) - - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent: firstExpectedEvent, - type: 'generation', - sandbox, - }) - - await updateFilePaths(session, modifiedReadme, firstUploadID, docScheme, controllerSetup.workspaceFolder) - await performAction('makeChanges', getSessionStub, 'add repository structure section') - - const secondExpectedEvent = createExpectedEvent({ - type: 'generation', - ...EventMetrics.REPO_STRUCTURE, - interactionType: 'GENERATE_README', - conversationId: firstConversationID, - }) - - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent: secondExpectedEvent, - type: 'generation', - sandbox, - }) - }) - }) - - it('should emit acceptance telemetry for README generation', async () => { - await retryTest(async () => { - await performAction('generate', getSessionStub) - await new Promise((resolve) => setTimeout(resolve, 100)) - const expectedEvent = createExpectedEvent({ - type: 'acceptance', - ...EventMetrics.INITIAL_README, - interactionType: 'GENERATE_README', - conversationId: firstConversationID, - }) - - await performAction('accept', getSessionStub) - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent, - type: 'acceptance', - sandbox, - }) - }) - }) - it('should emit generation telemetry for README update', async () => { - await retryTest(async () => { - await performAction('update', getSessionStub) - - const expectedEvent = createExpectedEvent({ - type: 'generation', - ...EventMetrics.REPO_STRUCTURE, - interactionType: 'UPDATE_README', - conversationId: firstConversationID, - }) - - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent, - type: 'generation', - sandbox, - }) - }) - }) - it('should emit another generation telemetry for make changes operation after README update', async () => { - await retryTest(async () => { - await performAction('update', getSessionStub) - await new Promise((resolve) => setTimeout(resolve, 100)) - - modifiedReadme = ReadmeBuilder.createReadmeWithDataFlow() - await updateFilePaths(session, modifiedReadme, firstUploadID, docScheme, controllerSetup.workspaceFolder) - - await performAction('makeChanges', getSessionStub, 'add data flow section') - - const expectedEvent = createExpectedEvent({ - type: 'generation', - ...EventMetrics.DATA_FLOW, - interactionType: 'UPDATE_README', - conversationId: firstConversationID, - }) - - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent, - type: 'generation', - sandbox, - }) - }) - }) - - it('should emit acceptance telemetry for README update', async () => { - await retryTest(async () => { - await performAction('update', getSessionStub) - await new Promise((resolve) => setTimeout(resolve, 100)) - - const expectedEvent = createExpectedEvent({ - type: 'acceptance', - ...EventMetrics.REPO_STRUCTURE, - interactionType: 'UPDATE_README', - conversationId: firstConversationID, - }) - - await performAction('accept', getSessionStub) - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent, - type: 'acceptance', - sandbox, - }) - }) - }) - - it('should emit generation telemetry for README edit', async () => { - await retryTest(async () => { - await performAction('edit', getSessionStub, 'add repository structure section') - - const expectedEvent = createExpectedEvent({ - type: 'generation', - ...EventMetrics.REPO_STRUCTURE, - interactionType: 'EDIT_README', - conversationId: firstConversationID, - }) - - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent, - type: 'generation', - sandbox, - }) - }) - }) - it('should emit acceptance telemetry for README edit', async () => { - await retryTest(async () => { - await performAction('edit', getSessionStub, 'add repository structure section') - await new Promise((resolve) => setTimeout(resolve, 100)) - - const expectedEvent = createExpectedEvent({ - type: 'acceptance', - ...EventMetrics.REPO_STRUCTURE, - interactionType: 'EDIT_README', - conversationId: firstConversationID, - }) - - await performAction('accept', getSessionStub) - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent, - type: 'acceptance', - sandbox, - }) - }) - }) - it('should emit separate telemetry events when executing /doc in different tabs', async () => { - await retryTest(async () => { - const firstSession = await getSessionStub(firstTabID) - const secondSession = await getSessionStub(secondTabID) - await performAction('generate', firstSession) - await performAction('update', secondSession, undefined, secondTabID, secondConversationID) - - const expectedEvent = createExpectedEvent({ - type: 'generation', - ...EventMetrics.INITIAL_README, - interactionType: 'GENERATE_README', - conversationId: firstConversationID, - }) - - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent, - type: 'generation', - sandbox, - }) - - const expectedEventForSecondTab = createExpectedEvent({ - type: 'generation', - ...EventMetrics.REPO_STRUCTURE, - interactionType: 'UPDATE_README', - conversationId: secondConversationID, - }) - - await assertTelemetry({ - spy: sendDocTelemetrySpyForSecondTab, - expectedEvent: expectedEventForSecondTab, - type: 'generation', - sandbox, - }) - }, true) - }) - - describe('Doc Generation Error Handling', () => { - const errors = [ - { - name: 'MonthlyConversationLimitError', - error: new MonthlyConversationLimitError('Service Quota Exceeded'), - }, - { - name: 'DocGenerationGuardrailsException', - error: new ApiClientError( - i18n('AWS.amazonq.doc.error.docGen.default'), - 'GetTaskAssistCodeGeneration', - 'GuardrailsException', - 400 - ), - }, - { - name: 'DocGenerationEmptyPatchException', - error: new LlmError(i18n('AWS.amazonq.doc.error.docGen.default'), { - code: 'EmptyPatchException', - }), - }, - { - name: 'DocGenerationThrottlingException', - error: new ApiClientError( - i18n('AWS.amazonq.featureDev.error.throttling'), - 'GetTaskAssistCodeGeneration', - 'ThrottlingException', - 429 - ), - }, - { name: 'UploadCodeError', error: new UploadCodeError('403: Forbiden') }, - { name: 'UserMessageNotFoundError', error: new UserMessageNotFoundError() }, - { name: 'TabIdNotFoundError', error: new TabIdNotFoundError() }, - { name: 'PrepareRepoFailedError', error: new PrepareRepoFailedError() }, - { name: 'PromptRefusalException', error: new PromptRefusalException(0) }, - { name: 'ZipFileError', error: new ZipFileError() }, - { name: 'CodeIterationLimitError', error: new CodeIterationLimitError() }, - { name: 'UploadURLExpired', error: new UploadURLExpired() }, - { name: 'NoChangeRequiredException', error: new NoChangeRequiredException() }, - { name: 'ReadmeTooLargeError', error: new ReadmeTooLargeError() }, - { name: 'ReadmeUpdateTooLargeError', error: new ReadmeUpdateTooLargeError(0) }, - { name: 'ContentLengthError', error: new ContentLengthError() }, - { name: 'WorkspaceEmptyError', error: new WorkspaceEmptyError() }, - { name: 'PromptUnrelatedError', error: new PromptUnrelatedError(0) }, - { name: 'PromptTooVagueError', error: new PromptTooVagueError(0) }, - { name: 'PromptRefusalException', error: new PromptRefusalException(0) }, - { - name: 'default', - error: new ApiServiceError( - i18n('AWS.amazonq.doc.error.docGen.default'), - 'GetTaskAssistCodeGeneration', - 'UnknownException', - 500 - ), - }, - ] - for (const { name, error } of errors) { - it(`should emit failure operation telemetry when ${name} occurs`, async () => { - await retryTest( - async () => { - await performAction('generate', getSessionStub) - - const expectedSuccessMetric = createExpectedMetricData( - MetricDataOperationName.StartDocGeneration, - MetricDataResult.Success - ) - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent: expectedSuccessMetric, - type: 'metric', - sandbox, - }) - - const expectedFailureMetric = createExpectedMetricData( - MetricDataOperationName.EndDocGeneration, - getMetricResult(error) - ) - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent: expectedFailureMetric, - type: 'metric', - sandbox, - }) - }, - undefined, - error - ) - }) - } - }) -}) diff --git a/packages/core/src/test/amazonqDoc/mockContent.ts b/packages/core/src/test/amazonqDoc/mockContent.ts deleted file mode 100644 index 1f3e68f6a58..00000000000 --- a/packages/core/src/test/amazonqDoc/mockContent.ts +++ /dev/null @@ -1,86 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -export const ReadmeSections = { - HEADER: `# My Awesome Project - -This is a demo project showcasing various features and capabilities.`, - - GETTING_STARTED: `## Getting Started -1. Clone the repository -2. Run npm install -3. Start the application`, - - FEATURES: `## Features -- Fast processing -- Easy to use -- Well documented`, - - LICENSE: '## License\nMIT License', - - REPO_STRUCTURE: `## Repository Structure -/src - /components - /utils -/tests - /unit -/docs`, - - DATA_FLOW: `## Data Flow -1. Input processing - - Data validation - - Format conversion -2. Core processing - - Business logic - - Data transformation -3. Output generation - - Result formatting - - Response delivery`, -} as const - -export class ReadmeBuilder { - private sections: string[] = [] - - addSection(section: string): this { - this.sections.push(section.replace(/\r\n/g, '\n')) - return this - } - - build(): string { - return this.sections.join('\n\n').replace(/\r\n/g, '\n') - } - - static createBaseReadme(): string { - return new ReadmeBuilder() - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.HEADER)) - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.GETTING_STARTED)) - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.FEATURES)) - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.LICENSE)) - .build() - } - - static createReadmeWithRepoStructure(): string { - return new ReadmeBuilder() - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.HEADER)) - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.REPO_STRUCTURE)) - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.GETTING_STARTED)) - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.FEATURES)) - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.LICENSE)) - .build() - } - - static createReadmeWithDataFlow(): string { - return new ReadmeBuilder() - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.HEADER)) - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.GETTING_STARTED)) - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.FEATURES)) - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.DATA_FLOW)) - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.LICENSE)) - .build() - } - - private static normalizeSection(section: string): string { - return section.replace(/\r\n/g, '\n') - } -} diff --git a/packages/core/src/test/amazonqDoc/session/sessionState.test.ts b/packages/core/src/test/amazonqDoc/session/sessionState.test.ts deleted file mode 100644 index 8f96894cc22..00000000000 --- a/packages/core/src/test/amazonqDoc/session/sessionState.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -/*! - * 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 from 'sinon' -import { DocPrepareCodeGenState } from '../../../amazonqDoc' -import { createMockSessionStateAction } from '../../amazonq/utils' - -import { createTestContext, setupTestHooks } from '../../amazonq/session/testSetup' - -describe('sessionStateDoc', () => { - const context = createTestContext() - setupTestHooks(context) - - describe('DocPrepareCodeGenState', () => { - it('error when failing to prepare repo information', async () => { - sinon.stub(vscode.workspace, 'findFiles').throws() - context.testMocks.createUploadUrl!.resolves({ uploadId: '', uploadUrl: '' }) - const testAction = createMockSessionStateAction() - - await assert.rejects(() => { - return new DocPrepareCodeGenState(context.testConfig, [], [], [], context.tabId, 0).interact(testAction) - }) - }) - }) -}) diff --git a/packages/core/src/test/amazonqDoc/utils.ts b/packages/core/src/test/amazonqDoc/utils.ts deleted file mode 100644 index 51c7305902c..00000000000 --- a/packages/core/src/test/amazonqDoc/utils.ts +++ /dev/null @@ -1,269 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import * as sinon from 'sinon' -import { MessagePublisher } from '../../amazonq/messages/messagePublisher' -import { ChatControllerEventEmitters, DocController } from '../../amazonqDoc/controllers/chat/controller' -import { DocChatSessionStorage } from '../../amazonqDoc/storages/chatSession' -import { createTestWorkspaceFolder } from '../testUtil' -import { Session } from '../../amazonqDoc/session/session' -import { NewFileInfo, SessionState } from '../../amazonqDoc/types' -import { FeatureDevClient } from '../../amazonqFeatureDev/client/featureDev' -import { VirtualMemoryFile } from '../../shared/virtualMemoryFile' -import path from 'path' -import { docChat } from '../../amazonqDoc/constants' -import { DocMessenger } from '../../amazonqDoc/messenger' -import { AppToWebViewMessageDispatcher } from '../../amazonq/commons/connector/connectorMessages' -import { createSessionConfig } from '../../amazonq/commons/session/sessionConfigFactory' -import { - DocV2GenerationEvent, - DocV2AcceptanceEvent, - MetricData, -} from '../../amazonqFeatureDev/client/featuredevproxyclient' -import { FollowUpTypes } from '../../amazonq/commons/types' - -export function createMessenger(sandbox: sinon.SinonSandbox): DocMessenger { - return new DocMessenger( - new AppToWebViewMessageDispatcher(new MessagePublisher(sandbox.createStubInstance(vscode.EventEmitter))), - docChat - ) -} - -export function createMockChatEmitters(): ChatControllerEventEmitters { - return { - processHumanChatMessage: new vscode.EventEmitter(), - followUpClicked: new vscode.EventEmitter(), - openDiff: new vscode.EventEmitter(), - processChatItemVotedMessage: new vscode.EventEmitter(), - processChatItemFeedbackMessage: new vscode.EventEmitter(), - stopResponse: new vscode.EventEmitter(), - tabOpened: new vscode.EventEmitter(), - tabClosed: new vscode.EventEmitter(), - authClicked: new vscode.EventEmitter(), - processResponseBodyLinkClick: new vscode.EventEmitter(), - insertCodeAtPositionClicked: new vscode.EventEmitter(), - fileClicked: new vscode.EventEmitter(), - formActionClicked: new vscode.EventEmitter(), - } -} - -export interface ControllerSetup { - emitters: ChatControllerEventEmitters - workspaceFolder: vscode.WorkspaceFolder - messenger: DocMessenger - sessionStorage: DocChatSessionStorage -} - -export async function createSession({ - messenger, - sessionState, - scheme, - conversationID = '0', - tabID = '0', - uploadID = '0', - sandbox, -}: { - messenger: DocMessenger - scheme: string - sessionState?: Omit - conversationID?: string - tabID?: string - uploadID?: string - sandbox: sinon.SinonSandbox -}) { - const sessionConfig = await createSessionConfig(scheme) - - const client = sandbox.createStubInstance(FeatureDevClient) - client.createConversation.resolves(conversationID) - const session = new Session(sessionConfig, messenger, tabID, sessionState, client) - - sandbox.stub(session, 'conversationId').get(() => conversationID) - sandbox.stub(session, 'uploadId').get(() => uploadID) - - return session -} -export async function sessionRegisterProvider(session: Session, uri: vscode.Uri, fileContents: Uint8Array) { - session.config.fs.registerProvider(uri, new VirtualMemoryFile(fileContents)) -} - -export function generateVirtualMemoryUri(uploadID: string, filePath: string, scheme: string) { - const generationFilePath = path.join(uploadID, filePath) - const uri = vscode.Uri.from({ scheme, path: generationFilePath }) - return uri -} - -export async function sessionWriteFile(session: Session, uri: vscode.Uri, encodedContent: Uint8Array) { - await session.config.fs.writeFile(uri, encodedContent, { - create: true, - overwrite: true, - }) -} - -export async function createController(sandbox: sinon.SinonSandbox): Promise { - const messenger = createMessenger(sandbox) - - // Create a new workspace root - const testWorkspaceFolder = await createTestWorkspaceFolder() - sandbox.stub(vscode.workspace, 'workspaceFolders').value([testWorkspaceFolder]) - - const sessionStorage = new DocChatSessionStorage(messenger) - - const mockChatControllerEventEmitters = createMockChatEmitters() - - new DocController( - mockChatControllerEventEmitters, - messenger, - sessionStorage, - sandbox.createStubInstance(vscode.EventEmitter).event - ) - - return { - emitters: mockChatControllerEventEmitters, - workspaceFolder: testWorkspaceFolder, - messenger, - sessionStorage, - } -} - -export type EventParams = { - type: 'generation' | 'acceptance' - chars: number - lines: number - files: number - interactionType: 'GENERATE_README' | 'UPDATE_README' | 'EDIT_README' - callIndex?: number - conversationId: string -} -/** - * Metrics for measuring README content changes in documentation generation tests. - */ -export const EventMetrics = { - /** - * Initial README content measurements - * Generated using ReadmeBuilder.createBaseReadme() - */ - INITIAL_README: { - chars: 265, - lines: 16, - files: 1, - }, - /** - * Repository Structure section measurements - * Differential metrics when adding repository structure documentation compare to the initial readme - */ - REPO_STRUCTURE: { - chars: 60, - lines: 8, - files: 1, - }, - /** - * Data Flow section measurements - * Differential metrics when adding data flow documentation compare to the initial readme - */ - DATA_FLOW: { - chars: 180, - lines: 11, - files: 1, - }, -} as const - -export function createExpectedEvent(params: EventParams) { - const baseEvent = { - conversationId: params.conversationId, - numberOfNavigations: 1, - folderLevel: 'ENTIRE_WORKSPACE', - interactionType: params.interactionType, - } - - if (params.type === 'generation') { - return { - ...baseEvent, - numberOfGeneratedChars: params.chars, - numberOfGeneratedLines: params.lines, - numberOfGeneratedFiles: params.files, - } as DocV2GenerationEvent - } else { - return { - ...baseEvent, - numberOfAddedChars: params.chars, - numberOfAddedLines: params.lines, - numberOfAddedFiles: params.files, - userDecision: 'ACCEPT', - } as DocV2AcceptanceEvent - } -} - -export function createExpectedMetricData(operationName: string, result: string) { - return { - metricName: 'Operation', - metricValue: 1, - timestamp: new Date(), - product: 'DocGeneration', - dimensions: [ - { - name: 'operationName', - value: operationName, - }, - { - name: 'result', - value: result, - }, - ], - } -} - -export async function assertTelemetry(params: { - spy: sinon.SinonStub - expectedEvent: DocV2GenerationEvent | DocV2AcceptanceEvent | MetricData - type: 'generation' | 'acceptance' | 'metric' - sandbox: sinon.SinonSandbox -}) { - await new Promise((resolve) => setTimeout(resolve, 100)) - params.sandbox.assert.calledWith(params.spy, params.sandbox.match(params.expectedEvent), params.type) -} - -export async function updateFilePaths( - session: Session, - content: string, - uploadId: string, - docScheme: string, - workspaceFolder: any -) { - const updatedFilePaths: NewFileInfo[] = [ - { - zipFilePath: path.normalize('README.md'), - relativePath: path.normalize('README.md'), - fileContent: content, - rejected: false, - virtualMemoryUri: generateVirtualMemoryUri(uploadId, path.normalize('README.md'), docScheme), - workspaceFolder: workspaceFolder, - changeApplied: false, - }, - ] - - Object.defineProperty(session.state, 'filePaths', { - get: () => updatedFilePaths, - configurable: true, - }) -} - -export const FollowUpSequences = { - generateReadme: [FollowUpTypes.NewTask, FollowUpTypes.CreateDocumentation, FollowUpTypes.ProceedFolderSelection], - updateReadme: [ - FollowUpTypes.NewTask, - FollowUpTypes.UpdateDocumentation, - FollowUpTypes.SynchronizeDocumentation, - FollowUpTypes.ProceedFolderSelection, - ], - editReadme: [ - FollowUpTypes.NewTask, - FollowUpTypes.UpdateDocumentation, - FollowUpTypes.EditDocumentation, - FollowUpTypes.ProceedFolderSelection, - ], - makeChanges: [FollowUpTypes.MakeChanges], - acceptContent: [FollowUpTypes.AcceptChanges], -} diff --git a/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts b/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts deleted file mode 100644 index 7848d0561b0..00000000000 --- a/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts +++ /dev/null @@ -1,717 +0,0 @@ -/*! - * 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 * as path from 'path' -import sinon from 'sinon' -import { waitUntil } from '../../../../shared/utilities/timeoutUtils' -import { ControllerSetup, createController, createSession, generateVirtualMemoryUri } from '../../../amazonq/utils' -import { - CurrentWsFolders, - DeletedFileInfo, - MetricDataOperationName, - MetricDataResult, - NewFileInfo, -} from '../../../../amazonq/commons/types' -import { Session } from '../../../../amazonqFeatureDev/session/session' -import { Prompter } from '../../../../shared/ui/prompter' -import { assertTelemetry, toFile } from '../../../testUtil' -import { - CodeIterationLimitError, - ContentLengthError, - createUserFacingErrorMessage, - FeatureDevServiceError, - getMetricResult, - MonthlyConversationLimitError, - NoChangeRequiredException, - PrepareRepoFailedError, - PromptRefusalException, - SelectedFolderNotInWorkspaceFolderError, - TabIdNotFoundError, - UploadCodeError, - UploadURLExpired, - UserMessageNotFoundError, - ZipFileError, -} from '../../../../amazonqFeatureDev/errors' -import { - FeatureDevCodeGenState, - FeatureDevPrepareCodeGenState, -} from '../../../../amazonqFeatureDev/session/sessionState' -import { FeatureDevClient } from '../../../../amazonqFeatureDev/client/featureDev' -import { createAmazonQUri } from '../../../../amazonq/commons/diff' -import { AuthUtil } from '../../../../codewhisperer' -import { featureDevScheme, featureName, messageWithConversationId } from '../../../../amazonqFeatureDev' -import { i18n } from '../../../../shared/i18n-helper' -import { FollowUpTypes } from '../../../../amazonq/commons/types' -import { ToolkitError } from '../../../../shared' -import { MessengerTypes } from '../../../../amazonqFeatureDev/controllers/chat/messenger/constants' - -let mockGetCodeGeneration: sinon.SinonStub -describe('Controller', () => { - const tabID = '123' - const conversationID = '456' - const uploadID = '789' - - let session: Session - let controllerSetup: ControllerSetup - - const getFilePaths = (controllerSetup: ControllerSetup): NewFileInfo[] => [ - { - zipFilePath: 'myfile1.js', - relativePath: 'myfile1.js', - fileContent: '', - rejected: false, - virtualMemoryUri: generateVirtualMemoryUri(uploadID, 'myfile1.js', featureDevScheme), - workspaceFolder: controllerSetup.workspaceFolder, - changeApplied: false, - }, - { - zipFilePath: 'myfile2.js', - relativePath: 'myfile2.js', - fileContent: '', - rejected: true, - virtualMemoryUri: generateVirtualMemoryUri(uploadID, 'myfile2.js', featureDevScheme), - workspaceFolder: controllerSetup.workspaceFolder, - changeApplied: false, - }, - ] - - const getDeletedFiles = (): DeletedFileInfo[] => [ - { - zipFilePath: 'myfile3.js', - relativePath: 'myfile3.js', - rejected: false, - workspaceFolder: controllerSetup.workspaceFolder, - changeApplied: false, - }, - { - zipFilePath: 'myfile4.js', - relativePath: 'myfile4.js', - rejected: true, - workspaceFolder: controllerSetup.workspaceFolder, - changeApplied: false, - }, - ] - - async function createCodeGenState() { - mockGetCodeGeneration = sinon.stub().resolves({ codeGenerationStatus: { status: 'Complete' } }) - - const workspaceFolders = [controllerSetup.workspaceFolder] as CurrentWsFolders - const testConfig = { - conversationId: conversationID, - proxyClient: { - createConversation: () => sinon.stub(), - createUploadUrl: () => sinon.stub(), - generatePlan: () => sinon.stub(), - startCodeGeneration: () => sinon.stub(), - getCodeGeneration: () => mockGetCodeGeneration(), - exportResultArchive: () => sinon.stub(), - } as unknown as FeatureDevClient, - workspaceRoots: [''], - uploadId: uploadID, - workspaceFolders, - } - - const codeGenState = new FeatureDevCodeGenState(testConfig, getFilePaths(controllerSetup), [], [], tabID, 0, {}) - const newSession = await createSession({ - messenger: controllerSetup.messenger, - sessionState: codeGenState, - conversationID, - tabID, - uploadID, - scheme: featureDevScheme, - }) - return newSession - } - - before(() => { - sinon.stub(performance, 'now').returns(0) - }) - - beforeEach(async () => { - controllerSetup = await createController() - session = await createSession({ - messenger: controllerSetup.messenger, - conversationID, - tabID, - uploadID, - scheme: featureDevScheme, - }) - - sinon.stub(AuthUtil.instance, 'getChatAuthState').resolves({ - codewhispererCore: 'connected', - codewhispererChat: 'connected', - amazonQ: 'connected', - }) - }) - - afterEach(() => { - sinon.restore() - }) - - describe('openDiff', async () => { - async function openDiff(filePath: string, deleted = false) { - const executeDiff = sinon.stub(vscode.commands, 'executeCommand').returns(Promise.resolve(undefined)) - controllerSetup.emitters.openDiff.fire({ tabID, conversationID, filePath, deleted }) - - // Wait until the controller has time to process the event - await waitUntil(() => { - return Promise.resolve(executeDiff.callCount > 0) - }, {}) - - return executeDiff - } - - it('uses empty file when file is not found locally', async () => { - sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - const executedDiff = await openDiff(path.join('src', 'mynewfile.js')) - assert.strictEqual( - executedDiff.calledWith( - 'vscode.diff', - createAmazonQUri('empty', tabID, featureDevScheme), - createAmazonQUri(path.join(uploadID, 'src', 'mynewfile.js'), tabID, featureDevScheme) - ), - true - ) - - assertTelemetry('amazonq_isReviewedChanges', { amazonqConversationId: conversationID, enabled: true }) - }) - - it('uses file location when file is found locally and /src is not available', async () => { - sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - const newFileLocation = path.join(controllerSetup.workspaceFolder.uri.fsPath, 'mynewfile.js') - await toFile('', newFileLocation) - const executedDiff = await openDiff('mynewfile.js') - assert.strictEqual( - executedDiff.calledWith( - 'vscode.diff', - vscode.Uri.file(newFileLocation), - createAmazonQUri(path.join(uploadID, 'mynewfile.js'), tabID, featureDevScheme) - ), - true - ) - - assertTelemetry('amazonq_isReviewedChanges', { amazonqConversationId: conversationID, enabled: true }) - }) - - it('uses file location when file is found locally and /src is available', async () => { - sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - const newFileLocation = path.join(controllerSetup.workspaceFolder.uri.fsPath, 'src', 'mynewfile.js') - await toFile('', newFileLocation) - const executedDiff = await openDiff(path.join('src', 'mynewfile.js')) - assert.strictEqual( - executedDiff.calledWith( - 'vscode.diff', - vscode.Uri.file(newFileLocation), - createAmazonQUri(path.join(uploadID, 'src', 'mynewfile.js'), tabID, featureDevScheme) - ), - true - ) - - assertTelemetry('amazonq_isReviewedChanges', { amazonqConversationId: conversationID, enabled: true }) - }) - - it('uses file location when file is found locally and source folder was picked', async () => { - sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - const newFileLocation = path.join(controllerSetup.workspaceFolder.uri.fsPath, 'foo', 'fi', 'mynewfile.js') - await toFile('', newFileLocation) - sinon.stub(vscode.workspace, 'getWorkspaceFolder').returns(controllerSetup.workspaceFolder) - session.config.workspaceRoots = [path.join(controllerSetup.workspaceFolder.uri.fsPath, 'foo', 'fi')] - const executedDiff = await openDiff(path.join('foo', 'fi', 'mynewfile.js')) - assert.strictEqual( - executedDiff.calledWith( - 'vscode.diff', - vscode.Uri.file(newFileLocation), - createAmazonQUri(path.join(uploadID, 'foo', 'fi', 'mynewfile.js'), tabID, featureDevScheme) - ), - true - ) - - assertTelemetry('amazonq_isReviewedChanges', { amazonqConversationId: conversationID, enabled: true }) - }) - }) - - describe('modifyDefaultSourceFolder', () => { - async function modifyDefaultSourceFolder(sourceRoot: string) { - const promptStub = sinon.stub(Prompter.prototype, 'prompt').resolves(vscode.Uri.file(sourceRoot)) - controllerSetup.emitters.followUpClicked.fire({ - tabID, - followUp: { - type: FollowUpTypes.ModifyDefaultSourceFolder, - }, - }) - - // Wait until the controller has time to process the event - await waitUntil(() => { - return Promise.resolve(promptStub.callCount > 0) - }, {}) - - return controllerSetup.sessionStorage.getSession(tabID) - } - - it('fails if selected folder is not under a workspace folder', async () => { - sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - sinon.stub(vscode.workspace, 'getWorkspaceFolder').returns(undefined) - const messengerSpy = sinon.spy(controllerSetup.messenger, 'sendAnswer') - await modifyDefaultSourceFolder('../../') - assert.deepStrictEqual( - messengerSpy.calledWith({ - tabID, - type: 'answer', - message: new SelectedFolderNotInWorkspaceFolderError().message, - canBeVoted: true, - }), - true - ) - assert.deepStrictEqual( - messengerSpy.calledWith({ - tabID, - type: 'system-prompt', - followUps: sinon.match.any, - }), - true - ) - }) - - it('accepts valid source folders under a workspace root', async () => { - sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - sinon.stub(vscode.workspace, 'getWorkspaceFolder').returns(controllerSetup.workspaceFolder) - const expectedSourceRoot = path.join(controllerSetup.workspaceFolder.uri.fsPath, 'src') - const modifiedSession = await modifyDefaultSourceFolder(expectedSourceRoot) - assert.strictEqual(modifiedSession.config.workspaceRoots.length, 1) - assert.strictEqual(modifiedSession.config.workspaceRoots[0], expectedSourceRoot) - }) - }) - - describe('newTask', () => { - async function newTaskClicked() { - const getSessionStub = sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - - controllerSetup.emitters.followUpClicked.fire({ - tabID, - followUp: { - type: FollowUpTypes.NewTask, - }, - }) - - // Wait until the controller has time to process the event - await waitUntil(() => { - return Promise.resolve(getSessionStub.callCount > 0) - }, {}) - } - - it('end chat telemetry is sent', async () => { - await newTaskClicked() - - assertTelemetry('amazonq_endChat', { amazonqConversationId: conversationID, result: 'Succeeded' }) - }) - }) - - describe('fileClicked', () => { - async function fileClicked( - getSessionStub: sinon.SinonStub<[tabID: string], Promise>, - action: string, - filePath: string - ) { - controllerSetup.emitters.fileClicked.fire({ - tabID, - conversationID, - filePath, - action, - }) - - // Wait until the controller has time to process the event - await waitUntil(() => { - return Promise.resolve(getSessionStub.callCount > 0) - }, {}) - return getSessionStub.getCall(0).returnValue - } - - it('clicking the "Reject File" button updates the file state to "rejected: true"', async () => { - const filePath = getFilePaths(controllerSetup)[0].zipFilePath - const session = await createCodeGenState() - const getSessionStub = sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - - const rejectFile = await fileClicked(getSessionStub, 'reject-change', filePath) - assert.strictEqual(rejectFile.state.filePaths?.find((i) => i.relativePath === filePath)?.rejected, true) - }) - - it('clicking the "Reject File" button and then "Revert Reject File", updates the file state to "rejected: false"', async () => { - const filePath = getFilePaths(controllerSetup)[0].zipFilePath - const session = await createCodeGenState() - const getSessionStub = sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - - await fileClicked(getSessionStub, 'reject-change', filePath) - const revertRejection = await fileClicked(getSessionStub, 'revert-rejection', filePath) - assert.strictEqual( - revertRejection.state.filePaths?.find((i) => i.relativePath === filePath)?.rejected, - false - ) - }) - }) - - describe('insertCode', () => { - it('sets the number of files accepted counting also deleted files', async () => { - async function insertCode() { - const initialState = new FeatureDevPrepareCodeGenState( - { - conversationId: conversationID, - proxyClient: new FeatureDevClient(), - workspaceRoots: [''], - workspaceFolders: [controllerSetup.workspaceFolder], - uploadId: uploadID, - }, - getFilePaths(controllerSetup), - getDeletedFiles(), - [], - tabID, - 0 - ) - - const newSession = await createSession({ - messenger: controllerSetup.messenger, - sessionState: initialState, - conversationID, - tabID, - uploadID, - scheme: featureDevScheme, - }) - const getSessionStub = sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(newSession) - - controllerSetup.emitters.followUpClicked.fire({ - tabID, - conversationID, - followUp: { - type: FollowUpTypes.InsertCode, - }, - }) - - // Wait until the controller has time to process the event - await waitUntil(() => { - return Promise.resolve(getSessionStub.callCount > 0) - }, {}) - } - - await insertCode() - - assertTelemetry('amazonq_isAcceptedCodeChanges', { - amazonqConversationId: conversationID, - amazonqNumberOfFilesAccepted: 2, - enabled: true, - result: 'Succeeded', - }) - }) - }) - - describe('processUserChatMessage', function () { - // TODO: fix disablePreviousFileList error - const runs = [ - { name: 'ContentLengthError', error: new ContentLengthError() }, - { - name: 'MonthlyConversationLimitError', - error: new MonthlyConversationLimitError('Service Quota Exceeded'), - }, - { - name: 'FeatureDevServiceErrorGuardrailsException', - error: new FeatureDevServiceError( - i18n('AWS.amazonq.featureDev.error.codeGen.default'), - 'GuardrailsException' - ), - }, - { - name: 'FeatureDevServiceErrorEmptyPatchException', - error: new FeatureDevServiceError( - i18n('AWS.amazonq.featureDev.error.throttling'), - 'EmptyPatchException' - ), - }, - { - name: 'FeatureDevServiceErrorThrottlingException', - error: new FeatureDevServiceError( - i18n('AWS.amazonq.featureDev.error.codeGen.default'), - 'ThrottlingException' - ), - }, - { name: 'UploadCodeError', error: new UploadCodeError('403: Forbiden') }, - { name: 'UserMessageNotFoundError', error: new UserMessageNotFoundError() }, - { name: 'TabIdNotFoundError', error: new TabIdNotFoundError() }, - { name: 'PrepareRepoFailedError', error: new PrepareRepoFailedError() }, - { name: 'PromptRefusalException', error: new PromptRefusalException() }, - { name: 'ZipFileError', error: new ZipFileError() }, - { name: 'CodeIterationLimitError', error: new CodeIterationLimitError() }, - { name: 'UploadURLExpired', error: new UploadURLExpired() }, - { name: 'NoChangeRequiredException', error: new NoChangeRequiredException() }, - { name: 'default', error: new ToolkitError('Default', { code: 'Default' }) }, - ] - - async function fireChatMessage(session: Session) { - const getSessionStub = sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - - controllerSetup.emitters.processHumanChatMessage.fire({ - tabID, - conversationID, - message: 'test message', - }) - - /** - * Wait until the controller has time to process the event - * Sessions should be called twice: - * 1. When the session getWorkspaceRoot is called - * 2. When the controller processes preloader - */ - await waitUntil(() => { - return Promise.resolve(getSessionStub.callCount > 1) - }, {}) - } - - describe('onCodeGeneration', function () { - let session: any - let sendMetricDataTelemetrySpy: sinon.SinonStub - - async function verifyException(error: ToolkitError) { - sinon.stub(session, 'send').throws(error) - - await fireChatMessage(session) - await verifyMetricsCalled() - assert.ok( - sendMetricDataTelemetrySpy.calledWith( - MetricDataOperationName.StartCodeGeneration, - MetricDataResult.Success - ) - ) - const metricResult = getMetricResult(error) - assert.ok( - sendMetricDataTelemetrySpy.calledWith(MetricDataOperationName.EndCodeGeneration, metricResult) - ) - } - - async function verifyMetricsCalled() { - await waitUntil(() => Promise.resolve(sendMetricDataTelemetrySpy.callCount >= 2), {}) - } - - async function verifyMessage( - expectedMessage: string, - type: MessengerTypes, - remainingIterations?: number, - totalIterations?: number - ) { - sinon.stub(session, 'send').resolves() - sinon.stub(session, 'sendLinesOfCodeGeneratedTelemetry').resolves() // Avoid sending extra telemetry - const sendAnswerSpy = sinon.stub(controllerSetup.messenger, 'sendAnswer') - sinon.stub(session.state, 'codeGenerationRemainingIterationCount').value(remainingIterations) - sinon.stub(session.state, 'codeGenerationTotalIterationCount').value(totalIterations) - - await fireChatMessage(session) - await verifyMetricsCalled() - - assert.ok( - sendAnswerSpy.calledWith({ - type, - tabID, - message: expectedMessage, - }) - ) - } - - beforeEach(async () => { - session = await createCodeGenState() - sinon.stub(session, 'preloader').resolves() - sendMetricDataTelemetrySpy = sinon.stub(session, 'sendMetricDataTelemetry') - }) - - it('sends success operation telemetry', async () => { - sinon.stub(session, 'send').resolves() - sinon.stub(session, 'sendLinesOfCodeGeneratedTelemetry').resolves() // Avoid sending extra telemetry - - await fireChatMessage(session) - await verifyMetricsCalled() - - assert.ok( - sendMetricDataTelemetrySpy.calledWith( - MetricDataOperationName.StartCodeGeneration, - MetricDataResult.Success - ) - ) - assert.ok( - sendMetricDataTelemetrySpy.calledWith( - MetricDataOperationName.EndCodeGeneration, - MetricDataResult.Success - ) - ) - }) - - for (const { name, error } of runs) { - it(`sends failure operation telemetry on ${name}`, async () => { - await verifyException(error) - }) - } - - // Using 3 to avoid spamming the tests - for (let remainingIterations = 0; remainingIterations <= 3; remainingIterations++) { - it(`verifies add code messages for remaining iterations at ${remainingIterations}`, async () => { - const totalIterations = 10 - const expectedMessage = (() => { - if (remainingIterations > 2) { - return 'Would you like me to add this code to your project, or provide feedback for new code?' - } else if (remainingIterations > 0) { - return `Would you like me to add this code to your project, or provide feedback for new code? You have ${remainingIterations} out of ${totalIterations} code generations left.` - } else { - return 'Would you like me to add this code to your project?' - } - })() - await verifyMessage(expectedMessage, 'answer', remainingIterations, totalIterations) - }) - } - - for (let remainingIterations = -1; remainingIterations <= 3; remainingIterations++) { - let remaining: number | undefined = remainingIterations - if (remainingIterations < 0) { - remaining = undefined - } - it(`verifies messages after cancellation for remaining iterations at ${remaining !== undefined ? remaining : 'undefined'}`, async () => { - const totalIterations = 10 - const expectedMessage = (() => { - if (remaining === undefined || remaining > 2) { - return 'I stopped generating your code. If you want to continue working on this task, provide another description.' - } else if (remaining > 0) { - return `I stopped generating your code. If you want to continue working on this task, provide another description. You have ${remaining} out of ${totalIterations} code generations left.` - } else { - return "I stopped generating your code. You don't have more iterations left, however, you can start a new session." - } - })() - session.state.tokenSource.cancel() - await verifyMessage( - expectedMessage, - 'answer-part', - remaining, - remaining === undefined ? undefined : totalIterations - ) - }) - } - }) - - describe('processErrorChatMessage', function () { - function createTestErrorMessage(message: string) { - return createUserFacingErrorMessage(`${featureName} request failed: ${message}`) - } - - async function verifyException(error: ToolkitError) { - sinon.stub(session, 'preloader').throws(error) - const sendAnswerSpy = sinon.stub(controllerSetup.messenger, 'sendAnswer') - const sendErrorMessageSpy = sinon.stub(controllerSetup.messenger, 'sendErrorMessage') - const sendMonthlyLimitErrorSpy = sinon.stub(controllerSetup.messenger, 'sendMonthlyLimitError') - - await fireChatMessage(session) - - switch (error.constructor.name) { - case ContentLengthError.name: - assert.ok( - sendAnswerSpy.calledWith({ - type: 'answer', - tabID, - message: error.message + messageWithConversationId(session?.conversationIdUnsafe), - canBeVoted: true, - }) - ) - break - case MonthlyConversationLimitError.name: - assert.ok(sendMonthlyLimitErrorSpy.calledWith(tabID)) - break - case FeatureDevServiceError.name: - case UploadCodeError.name: - case UserMessageNotFoundError.name: - case TabIdNotFoundError.name: - case PrepareRepoFailedError.name: - assert.ok( - sendErrorMessageSpy.calledWith( - createTestErrorMessage(error.message), - tabID, - session?.retries, - session?.conversationIdUnsafe - ) - ) - break - case PromptRefusalException.name: - case ZipFileError.name: - assert.ok( - sendErrorMessageSpy.calledWith( - createTestErrorMessage(error.message), - tabID, - 0, - session?.conversationIdUnsafe, - true - ) - ) - break - case NoChangeRequiredException.name: - case CodeIterationLimitError.name: - case UploadURLExpired.name: - assert.ok( - sendAnswerSpy.calledWith({ - type: 'answer', - tabID, - message: error.message, - canBeVoted: true, - }) - ) - break - default: - assert.ok( - sendErrorMessageSpy.calledWith( - i18n('AWS.amazonq.featureDev.error.codeGen.default'), - tabID, - session?.retries, - session?.conversationIdUnsafe, - true - ) - ) - break - } - } - - for (const run of runs) { - it(`should handle ${run.name}`, async function () { - await verifyException(run.error) - }) - } - }) - }) - - describe('stopResponse', () => { - it('should emit ui_click telemetry with elementId amazonq_stopCodeGeneration', async () => { - const getSessionStub = sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - controllerSetup.emitters.stopResponse.fire({ tabID, conversationID }) - await waitUntil(() => { - return Promise.resolve(getSessionStub.callCount > 0) - }, {}) - assertTelemetry('ui_click', { elementId: 'amazonq_stopCodeGeneration' }) - }) - }) - - describe('closeSession', async () => { - async function closeSessionClicked() { - const getSessionStub = sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - - controllerSetup.emitters.followUpClicked.fire({ - tabID, - followUp: { - type: FollowUpTypes.CloseSession, - }, - }) - - // Wait until the controller has time to process the event - await waitUntil(() => { - return Promise.resolve(getSessionStub.callCount > 0) - }, {}) - } - - it('end chat telemetry is sent', async () => { - await closeSessionClicked() - - assertTelemetry('amazonq_endChat', { amazonqConversationId: conversationID, result: 'Succeeded' }) - }) - }) -}) diff --git a/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts b/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts deleted file mode 100644 index 2d68654ee00..00000000000 --- a/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -/*! - * 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 from 'sinon' -import { - MockCodeGenState, - FeatureDevPrepareCodeGenState, - FeatureDevCodeGenState, -} from '../../../amazonqFeatureDev/session/sessionState' -import { ToolkitError } from '../../../shared/errors' -import * as crypto from '../../../shared/crypto' -import { createMockSessionStateAction } from '../../amazonq/utils' - -import { createTestContext, setupTestHooks } from '../../amazonq/session/testSetup' - -describe('sessionStateFeatureDev', () => { - const context = createTestContext() - setupTestHooks(context) - - describe('MockCodeGenState', () => { - it('loops forever in the same state', async () => { - sinon.stub(crypto, 'randomUUID').returns('upload-id' as ReturnType<(typeof crypto)['randomUUID']>) - const testAction = createMockSessionStateAction() - const state = new MockCodeGenState(context.testConfig, context.tabId) - const result = await state.interact(testAction) - - assert.deepStrictEqual(result, { - nextState: state, - interaction: {}, - }) - }) - }) - - describe('FeatureDevPrepareCodeGenState', () => { - it('error when failing to prepare repo information', async () => { - sinon.stub(vscode.workspace, 'findFiles').throws() - context.testMocks.createUploadUrl!.resolves({ uploadId: '', uploadUrl: '' }) - const testAction = createMockSessionStateAction() - - await assert.rejects(() => { - return new FeatureDevPrepareCodeGenState(context.testConfig, [], [], [], context.tabId, 0).interact( - testAction - ) - }) - }) - }) - - describe('FeatureDevCodeGenState', () => { - it('transitions to FeatureDevPrepareCodeGenState when codeGenerationStatus ready ', async () => { - context.testMocks.getCodeGeneration!.resolves({ - codeGenerationStatus: { status: 'Complete' }, - codeGenerationRemainingIterationCount: 2, - codeGenerationTotalIterationCount: 3, - }) - - context.testMocks.exportResultArchive!.resolves({ newFileContents: [], deletedFiles: [], references: [] }) - - const testAction = createMockSessionStateAction() - const state = new FeatureDevCodeGenState(context.testConfig, [], [], [], context.tabId, 0, {}, 2, 3) - const result = await state.interact(testAction) - - const nextState = new FeatureDevPrepareCodeGenState( - context.testConfig, - [], - [], - [], - context.tabId, - 1, - 2, - 3, - undefined - ) - - assert.deepStrictEqual(result.nextState?.deletedFiles, nextState.deletedFiles) - assert.deepStrictEqual(result.nextState?.filePaths, result.nextState?.filePaths) - assert.deepStrictEqual(result.nextState?.references, result.nextState?.references) - }) - - it('fails when codeGenerationStatus failed ', async () => { - context.testMocks.getCodeGeneration!.rejects(new ToolkitError('Code generation failed')) - const testAction = createMockSessionStateAction() - const state = new FeatureDevCodeGenState(context.testConfig, [], [], [], context.tabId, 0, {}) - try { - await state.interact(testAction) - assert.fail('failed code generations should throw an error') - } catch (e: any) { - assert.deepStrictEqual(e.message, 'Code generation failed') - } - }) - }) -}) diff --git a/packages/core/src/test/amazonqGumby/transformationJobHistory.test.ts b/packages/core/src/test/amazonqGumby/transformationJobHistory.test.ts new file mode 100644 index 00000000000..f5a92299255 --- /dev/null +++ b/packages/core/src/test/amazonqGumby/transformationJobHistory.test.ts @@ -0,0 +1,375 @@ +/*! + * 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 os from 'os' +import * as path from 'path' +import fs from '../../shared/fs/fs' +import * as datetime from '../../shared/datetime' +import { codeWhispererClient } from '../../codewhisperer/client/codewhisperer' +import { + readHistoryFile, + writeToHistoryFile, + createMetadataFile, + cleanupTempJobFiles, + refreshJob, + JobMetadata, +} from '../../codewhisperer/service/transformByQ/transformationHistoryHandler' +import { copyArtifacts } from '../../codewhisperer/service/transformByQ/transformFileHandler' +import * as transformApiHandler from '../../codewhisperer/service/transformByQ/transformApiHandler' +import { ExportResultArchiveStructure } from '../../shared/utilities/download' +import { JDKVersion, TransformationType } from '../../codewhisperer' + +describe('Transformation History Handler', function () { + function setupFileSystemMocks() { + const createdFiles = new Map() + const createdDirs = new Set() + + // Mock file operations to track what gets created + sinon.stub(fs, 'mkdir').callsFake(async (dirPath: any) => { + createdDirs.add(dirPath.toString()) + }) + sinon.stub(fs, 'copy').callsFake(async (src: any, dest: any) => { + createdFiles.set(dest.toString(), `copied from ${src.toString()}`) + }) + sinon.stub(fs, 'writeFile').callsFake(async (filePath: any, content: any) => { + createdFiles.set(filePath.toString(), content.toString()) + }) + sinon.stub(fs, 'delete').callsFake(async (filePath: any) => { + createdFiles.delete(filePath.toString()) + }) + + return { createdFiles, createdDirs } + } + + afterEach(function () { + sinon.restore() + }) + + describe('Reading history file', function () { + it('Returns empty array when history file does not exist', async function () { + sinon.stub(fs, 'existsFile').resolves(false) + + const result = await readHistoryFile() + + assert.strictEqual(result.length, 0) + }) + + it('Limits results to 10 most recent jobs', async function () { + sinon.stub(fs, 'existsFile').resolves(true) + sinon.stub(datetime, 'isWithin30Days').returns(true) + + let historyContent = 'date\tproject_name\tstatus\tduration\tdiff_patch\tsummary\tjob_id\n' + for (let i = 1; i <= 15; i++) { + historyContent += `01/${i}/25, 10:00 AM\tproject-${i}\tCOMPLETED\t5 min\t\t\tjob-${i}\n` + } + + sinon.stub(fs, 'readFileText').resolves(historyContent) + + const result = await readHistoryFile() + + assert.strictEqual(result.length, 10) + assert.strictEqual(result[0].jobId, 'job-15') // most recent first + assert.strictEqual(result[9].jobId, 'job-6') + }) + }) + + describe('Writing to history file', function () { + let writtenFiles: Map + + beforeEach(function () { + writtenFiles = new Map() + + // Mock file operations to capture what gets written + sinon.stub(fs, 'mkdir').resolves() + sinon.stub(fs, 'writeFile').callsFake(async (filePath: any, content: any) => { + writtenFiles.set(filePath.toString(), content.toString()) + }) + sinon.stub(fs, 'appendFile').callsFake(async (filePath: any, content: any) => { + const existing = writtenFiles.get(filePath.toString()) || '' + writtenFiles.set(filePath.toString(), existing + content.toString()) + }) + sinon.stub(vscode.commands, 'executeCommand').resolves() + }) + + 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', + '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\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', + '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) + + const lines = fileContent?.split('\n') || [] + const jobLine = lines.find((line) => line.includes('job-123')) + const fields = jobLine?.split('\t') || [] + + assert.strictEqual(fields[4], '') // diff path should be empty + assert.strictEqual(fields[5], '') // summary path should be empty + }) + + 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\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'), + existingContent + ) + + sinon.stub(fs, 'existsFile').resolves(true) + + 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\t/old/path\tLANGUAGE_UPGRADE\tJDK8\tJDK17\t/old/path2\tclean test-compile\n' + ) + ) + + // Verify new data is added + assert(fileContent?.includes('new-project\tFAILED\t2 min\t\t\tnew-job-789')) + + // Verify both jobs are present and that new job is at bottom of file + const lines = fileContent?.split('\n').filter((line) => line.trim()) || [] + assert.strictEqual(lines.length, 3) // header + 2 job lines + assert(lines[1].includes('old-job-456')) + assert(lines[2].includes('new-job-789')) + }) + }) + + describe('Metadata file operations', function () { + let createdFiles: Map + let createdDirs: Set + + const mockMetadata: JobMetadata = { + jobId: 'test-job-123', + projectName: 'test-project', + transformationType: TransformationType.LANGUAGE_UPGRADE, + sourceJDKVersion: JDKVersion.JDK8, + targetJDKVersion: JDKVersion.JDK17, + customDependencyVersionFilePath: '', + customBuildCommand: '', + targetJavaHome: '', + projectPath: '/path/to/project', + startTime: '01/01/24, 10:00 AM', + } + + beforeEach(function () { + const mocks = setupFileSystemMocks() + createdFiles = mocks.createdFiles + createdDirs = mocks.createdDirs + }) + + it('Creates job history directory and metadata files', async function () { + const result = await createMetadataFile('/path/to/payload.zip', mockMetadata) + + const expectedPath = path.join(os.homedir(), '.aws', 'transform', 'test-project', 'test-job-123') + assert.strictEqual(result, expectedPath) + + // Verify directory was created + assert(createdDirs.has(expectedPath)) + + // Verify zipped-code.zip was copied + const zipPath = path.join(expectedPath, 'zipped-code.zip') + assert(createdFiles.has(zipPath)) + assert.strictEqual(createdFiles.get(zipPath), 'copied from /path/to/payload.zip') + + // Verify metadata.json was created with correct content + const metadataPath = path.join(expectedPath, 'metadata.json') + assert(createdFiles.has(metadataPath)) + assert.strictEqual(createdFiles.get(metadataPath), JSON.stringify(mockMetadata)) + }) + + it('Deletes payload, build logs, and metadata files', async function () { + // Pre-populate files that would exist + createdFiles.set('/payload.zip', 'payload content') + createdFiles.set(path.join(os.tmpdir(), 'build-logs.txt'), 'build logs') + createdFiles.set(path.join('/job/path', 'metadata.json'), 'metadata') + createdFiles.set(path.join('/job/path', 'zipped-code.zip'), 'zip content') + + await cleanupTempJobFiles('/job/path', 'COMPLETED', '/payload.zip') + + // Verify files were deleted (no longer exist in createdFiles) + assert(!createdFiles.has('/payload.zip')) + assert(!createdFiles.has(path.join(os.tmpdir(), 'build-logs.txt'))) + assert(!createdFiles.has(path.join('/job/path', 'metadata.json'))) + assert(!createdFiles.has(path.join('/job/path', 'zipped-code.zip'))) + }) + + it('Preserves metadata for failed jobs', async function () { + // Pre-populate files that would exist + createdFiles.set(path.join('/job/path', 'metadata.json'), 'metadata') + createdFiles.set(path.join('/job/path', 'zipped-code.zip'), 'zip content') + + await cleanupTempJobFiles('/job/path', 'FAILED') + + // Verify metadata files still exist (were NOT deleted) + assert(createdFiles.has(path.join('/job/path', 'metadata.json'))) + assert(createdFiles.has(path.join('/job/path', 'zipped-code.zip'))) + }) + }) + + describe('Copying artifacts', function () { + let createdFiles: Map + let createdDirs: Set + + beforeEach(function () { + const mocks = setupFileSystemMocks() + createdFiles = mocks.createdFiles + createdDirs = mocks.createdDirs + }) + + it('Copies diff patch and summary files to destination', async function () { + await copyArtifacts(path.join('archive', 'path'), path.join('destination', 'path')) + + // Verify directories were created + assert(createdDirs.has(path.join('destination', 'path'))) + assert(createdDirs.has(path.join('destination', 'path', 'summary'))) + + // Verify files were copied to correct locations + assert(createdFiles.has(path.join('destination', 'path', 'diff.patch'))) + assert(createdFiles.has(path.join('destination', 'path', 'summary', 'summary.md'))) + + // Verify source paths are correct + const diffSource = createdFiles.get(path.join('destination', 'path', 'diff.patch')) + const summarySource = createdFiles.get(path.join('destination', 'path', 'summary', 'summary.md')) + assert(diffSource?.includes(path.normalize(ExportResultArchiveStructure.PathToDiffPatch))) + assert(summarySource?.includes(path.normalize(ExportResultArchiveStructure.PathToSummary))) + }) + }) + + describe('Refreshing jobs', function () { + let createdFiles: Map + let createdDirs: Set + let writtenFiles: Map + + beforeEach(function () { + createdFiles = new Map() + createdDirs = new Set() + writtenFiles = new Map() + + // Mock file operations to track what gets created/written + sinon.stub(vscode.commands, 'executeCommand').resolves() + sinon.stub(fs, 'mkdir').callsFake(async (dirPath: any) => { + createdDirs.add(dirPath.toString()) + }) + sinon.stub(fs, 'copy').callsFake(async (src: any, dest: any) => { + createdFiles.set(dest.toString(), `copied from ${src.toString()}`) + }) + sinon.stub(fs, 'delete').resolves() + sinon.stub(fs, 'writeFile').callsFake(async (filePath: any, content: any) => { + writtenFiles.set(filePath.toString(), content.toString()) + }) + sinon.stub(fs, 'appendFile').callsFake(async (filePath: any, content: any) => { + const existing = writtenFiles.get(filePath.toString()) || '' + writtenFiles.set(filePath.toString(), existing + content.toString()) + }) + sinon.stub(transformApiHandler, 'downloadAndExtractResultArchive').resolves() + }) + + it('Updates job status and downloads artifacts', async function () { + const mockResponse = { + transformationJob: { + status: 'COMPLETED', + endExecutionTime: new Date(), + creationTime: new Date(Date.now() - 300000), + }, + } as any + sinon.stub(codeWhispererClient, 'codeModernizerGetCodeTransformation').resolves(mockResponse) + + // Mock existsFile to return false for diff.patch but true for history file + sinon.stub(fs, 'existsFile').callsFake(async (filePath: any) => { + const pathStr = filePath.toString() + if (pathStr.includes('diff.patch')) { + return false // Artifacts don't exist yet, need to download + } + return true // History file exists + }) + + // Mock history file content for update + const historyContent = + 'date\tproject_name\tstatus\tduration\tdiff_patch\tsummary\tjob_id\n' + + '01/01/24, 10:00 AM\ttest-project\tFAILED\t\t\t\tjob-123\n' + sinon.stub(fs, 'readFileText').resolves(historyContent) + + await refreshJob('job-123', 'FAILED', 'test-project') + + // Verify artifacts were copied to job history path + const jobHistoryPath = path.join(os.homedir(), '.aws', 'transform', 'test-project', 'job-123') + assert(createdFiles.has(path.join(jobHistoryPath, 'diff.patch'))) + assert(createdFiles.has(path.join(jobHistoryPath, 'summary', 'summary.md'))) + + // Verify history file was updated with new status and artifact paths + const historyFilePath = path.join(os.homedir(), '.aws', 'transform', 'transformation_history.tsv') + const updatedContent = writtenFiles.get(historyFilePath) + assert(updatedContent?.includes('COMPLETED')) + assert(updatedContent?.includes('diff.patch')) + assert(updatedContent?.includes('summary.md')) + }) + }) +}) diff --git a/packages/core/src/test/auth/activation.test.ts b/packages/core/src/test/auth/activation.test.ts new file mode 100644 index 00000000000..f203033acba --- /dev/null +++ b/packages/core/src/test/auth/activation.test.ts @@ -0,0 +1,146 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as sinon from 'sinon' +import assert from 'assert' +import { initialize, SagemakerCookie } from '../../auth/activation' +import { LoginManager } from '../../auth/deprecated/loginManager' +import * as extensionUtilities from '../../shared/extensionUtilities' +import * as authUtils from '../../auth/utils' +import * as errors from '../../shared/errors' + +describe('auth/activation', function () { + let sandbox: sinon.SinonSandbox + let mockLoginManager: LoginManager + let executeCommandStub: sinon.SinonStub + let isAmazonQStub: sinon.SinonStub + let isSageMakerStub: sinon.SinonStub + let initializeCredentialsProviderManagerStub: sinon.SinonStub + let getErrorMsgStub: sinon.SinonStub + let mockLogger: any + + beforeEach(function () { + sandbox = sinon.createSandbox() + + // Create mocks + mockLoginManager = { + login: sandbox.stub(), + logout: sandbox.stub(), + } as any + + mockLogger = { + warn: sandbox.stub(), + info: sandbox.stub(), + error: sandbox.stub(), + debug: sandbox.stub(), + } + + // Stub external dependencies + executeCommandStub = sandbox.stub(vscode.commands, 'executeCommand') + isAmazonQStub = sandbox.stub(extensionUtilities, 'isAmazonQ') + isSageMakerStub = sandbox.stub(extensionUtilities, 'isSageMaker') + initializeCredentialsProviderManagerStub = sandbox.stub(authUtils, 'initializeCredentialsProviderManager') + getErrorMsgStub = sandbox.stub(errors, 'getErrorMsg') + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('initialize', function () { + it('should not execute sagemaker.parseCookies when not in AmazonQ and SageMaker environment', async function () { + isAmazonQStub.returns(false) + isSageMakerStub.returns(false) + + await initialize(mockLoginManager) + + assert.ok(!executeCommandStub.called) + assert.ok(!initializeCredentialsProviderManagerStub.called) + }) + + it('should not execute sagemaker.parseCookies when only in AmazonQ environment', async function () { + isAmazonQStub.returns(true) + isSageMakerStub.returns(false) + + await initialize(mockLoginManager) + + assert.ok(!executeCommandStub.called) + assert.ok(!initializeCredentialsProviderManagerStub.called) + }) + + it('should not execute sagemaker.parseCookies when only in SageMaker environment', async function () { + isAmazonQStub.returns(false) + isSageMakerStub.returns(true) + + await initialize(mockLoginManager) + + assert.ok(!executeCommandStub.called) + assert.ok(!initializeCredentialsProviderManagerStub.called) + }) + + it('should execute sagemaker.parseCookies when in both AmazonQ and SageMaker environment', async function () { + isAmazonQStub.returns(true) + isSageMakerStub.returns(true) + executeCommandStub.withArgs('sagemaker.parseCookies').resolves({ authMode: 'Sso' } as SagemakerCookie) + + await initialize(mockLoginManager) + + assert.ok(executeCommandStub.calledOnceWith('sagemaker.parseCookies')) + assert.ok(!initializeCredentialsProviderManagerStub.called) + }) + + it('should initialize credentials provider manager when authMode is not Sso', async function () { + isAmazonQStub.returns(true) + isSageMakerStub.returns(true) + executeCommandStub.withArgs('sagemaker.parseCookies').resolves({ authMode: 'Iam' } as SagemakerCookie) + + await initialize(mockLoginManager) + + assert.ok(executeCommandStub.calledOnceWith('sagemaker.parseCookies')) + assert.ok(initializeCredentialsProviderManagerStub.calledOnce) + }) + + it('should initialize credentials provider manager when authMode is undefined', async function () { + isAmazonQStub.returns(true) + isSageMakerStub.returns(true) + executeCommandStub.withArgs('sagemaker.parseCookies').resolves({} as SagemakerCookie) + + await initialize(mockLoginManager) + + assert.ok(executeCommandStub.calledOnceWith('sagemaker.parseCookies')) + assert.ok(initializeCredentialsProviderManagerStub.calledOnce) + }) + + it('should warn and not throw when sagemaker.parseCookies command is not found', async function () { + isAmazonQStub.returns(true) + isSageMakerStub.returns(true) + const error = new Error("command 'sagemaker.parseCookies' not found") + executeCommandStub.withArgs('sagemaker.parseCookies').rejects(error) + getErrorMsgStub.returns("command 'sagemaker.parseCookies' not found") + + await initialize(mockLoginManager) + + assert.ok(executeCommandStub.calledOnceWith('sagemaker.parseCookies')) + assert.ok(getErrorMsgStub.calledOnceWith(error)) + assert.ok(!initializeCredentialsProviderManagerStub.called) + }) + + it('should throw when sagemaker.parseCookies fails with non-command-not-found error', async function () { + isAmazonQStub.returns(true) + isSageMakerStub.returns(true) + const error = new Error('Some other error') + executeCommandStub.withArgs('sagemaker.parseCookies').rejects(error) + getErrorMsgStub.returns('Some other error') + + await assert.rejects(initialize(mockLoginManager), /Some other error/) + + assert.ok(executeCommandStub.calledOnceWith('sagemaker.parseCookies')) + assert.ok(getErrorMsgStub.calledOnceWith(error)) + assert.ok(!mockLogger.warn.called) + assert.ok(!initializeCredentialsProviderManagerStub.called) + }) + }) +}) 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 new file mode 100644 index 00000000000..cb8ce40821b --- /dev/null +++ b/packages/core/src/test/auth/providers/sharedCredentialsProvider.test.ts @@ -0,0 +1,186 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { SharedCredentialsProvider } from '../../../auth/providers/sharedCredentialsProvider' +import { createTestSections } from '../../credentials/testUtil' +import { DefaultStsClient } from '../../../shared/clients/stsClient' +import { oneDay } from '../../../shared/datetime' +import sinon from 'sinon' +import { SsoAccessTokenProvider } from '../../../auth/sso/ssoAccessTokenProvider' +import { SsoClient } from '../../../auth/sso/clients' + +describe('SharedCredentialsProvider - Role Chaining with SSO', function () { + let sandbox: sinon.SinonSandbox + + beforeEach(function () { + sandbox = sinon.createSandbox() + }) + + afterEach(function () { + sandbox.restore() + }) + + it('should handle role chaining from SSO profile', async function () { + // Mock the SSO authentication + sandbox.stub(SsoAccessTokenProvider.prototype, 'getToken').resolves({ + accessToken: 'test-token', + expiresAt: new Date(Date.now() + oneDay), + }) + + // Mock SSO getRoleCredentials + sandbox.stub(SsoClient.prototype, 'getRoleCredentials').resolves({ + accessKeyId: 'sso-access-key', + secretAccessKey: 'sso-secret-key', + sessionToken: 'sso-session-token', + expiration: new Date(Date.now() + oneDay), + }) + + // Mock STS assumeRole + sandbox.stub(DefaultStsClient.prototype, 'assumeRole').callsFake(async (request) => { + assert.strictEqual(request.RoleArn, 'arn:aws:iam::123456789012:role/dev') + return { + Credentials: { + AccessKeyId: 'assumed-access-key', + SecretAccessKey: 'assumed-secret-key', + SessionToken: 'assumed-session-token', + Expiration: new Date(Date.now() + oneDay), + }, + } + }) + + const sections = await createTestSections(` + [sso-session aws1_session] + sso_start_url = https://example.awsapps.com/start + sso_region = us-east-1 + sso_registration_scopes = sso:account:access + + [profile Landing] + sso_session = aws1_session + sso_account_id = 111111111111 + sso_role_name = Landing + region = us-east-1 + + [profile dev] + region = us-east-1 + role_arn = arn:aws:iam::123456789012:role/dev + source_profile = Landing + `) + + const provider = new SharedCredentialsProvider('dev', sections) + const credentials = await provider.getCredentials() + + assert.strictEqual(credentials.accessKeyId, 'assumed-access-key') + assert.strictEqual(credentials.secretAccessKey, 'assumed-secret-key') + 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/lambda2sam/lambda2sam.test.ts b/packages/core/src/test/awsService/appBuilder/lambda2sam/lambda2sam.test.ts new file mode 100644 index 00000000000..f07343b33a9 --- /dev/null +++ b/packages/core/src/test/awsService/appBuilder/lambda2sam/lambda2sam.test.ts @@ -0,0 +1,273 @@ +/*! + * 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 { LambdaFunctionNode } from '../../../../lambda/explorer/lambdaFunctionNode' +import { DefaultLambdaClient } from '../../../../shared/clients/lambdaClient' +import { Template } from '../../../../shared/cloudformation/cloudformation' +import * as lambda2sam from '../../../../awsService/appBuilder/lambda2sam/lambda2sam' +import * as authUtils from '../../../../auth/utils' +import * as utils from '../../../../awsService/appBuilder/utils' + +describe('lambda2sam', function () { + let sandbox: sinon.SinonSandbox + + beforeEach(function () { + sandbox = sinon.createSandbox() + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('ifSamTemplate', function () { + it('returns true when transform is a string and starts with AWS::Serverless', function () { + const template: Template = { + Transform: 'AWS::Serverless-2016-10-31', + } + assert.strictEqual(lambda2sam.ifSamTemplate(template), true) + }) + + it('returns false when transform is a string and does not start with AWS::Serverless', function () { + const template: Template = { + Transform: 'AWS::Other-Transform', + } + assert.strictEqual(lambda2sam.ifSamTemplate(template), false) + }) + + it('returns true when transform is an array and at least one starts with AWS::Serverless', function () { + const template: Template = { + Transform: ['AWS::Serverless-2016-10-31', 'AWS::Other-Transform'] as any, + } + assert.strictEqual(lambda2sam.ifSamTemplate(template), true) + }) + + it('returns false when transform is an array and none start with AWS::Serverless', function () { + const template: Template = { + Transform: ['AWS::Other-Transform-1', 'AWS::Other-Transform-2'] as any, + } + assert.strictEqual(lambda2sam.ifSamTemplate(template), false) + }) + + it('returns false when transform is not present', function () { + const template: Template = {} + assert.strictEqual(lambda2sam.ifSamTemplate(template), false) + }) + + it('returns false when transform is an unsupported type', function () { + const template: Template = { + Transform: { some: 'object' } as any, + } + assert.strictEqual(lambda2sam.ifSamTemplate(template), false) + }) + }) + + describe('extractLogicalIdFromIntrinsic', function () { + it('extracts logical ID from Ref intrinsic', function () { + const value = { Ref: 'MyResource' } + assert.strictEqual(lambda2sam.extractLogicalIdFromIntrinsic(value), 'MyResource') + }) + + it('extracts logical ID from GetAtt intrinsic with Arn attribute', function () { + const value = { 'Fn::GetAtt': ['MyResource', 'Arn'] } + assert.strictEqual(lambda2sam.extractLogicalIdFromIntrinsic(value), 'MyResource') + }) + + it('returns undefined for GetAtt intrinsic with non-Arn attribute', function () { + const value = { 'Fn::GetAtt': ['MyResource', 'Name'] } + assert.strictEqual(lambda2sam.extractLogicalIdFromIntrinsic(value), undefined) + }) + + it('returns undefined for non-intrinsic values', function () { + assert.strictEqual(lambda2sam.extractLogicalIdFromIntrinsic('not-an-intrinsic'), undefined) + assert.strictEqual(lambda2sam.extractLogicalIdFromIntrinsic({ NotIntrinsic: 'value' }), undefined) + assert.strictEqual(lambda2sam.extractLogicalIdFromIntrinsic(undefined), undefined) + }) + }) + + describe('callExternalApiForCfnTemplate', function () { + let lambdaClientStub: sinon.SinonStubbedInstance + let cfnClientStub: any + + beforeEach(function () { + lambdaClientStub = sandbox.createStubInstance(DefaultLambdaClient) + // Stub at prototype level to avoid TypeScript errors + sandbox + .stub(DefaultLambdaClient.prototype, 'getFunction') + .callsFake((name) => lambdaClientStub.getFunction(name)) + + // Mock CloudFormation client for the new external API calls - now returns Promises directly + cfnClientStub = { + getGeneratedTemplate: sandbox.stub().resolves({ + Status: 'COMPLETE', + TemplateBody: JSON.stringify({ + AWSTemplateFormatVersion: '2010-09-09', + Resources: { + testFunc: { + DeletionPolicy: 'Retain', + Properties: { + Code: { + S3Bucket: 'aws-sam-cli-managed-default-samclisourcebucket-1n8tvb0jdhsd', + S3Key: '1d1c93ec17af7e2666ee20ea1a215c77', + }, + Environment: { + Variables: { + KEY: 'value', + }, + }, + FunctionName: 'myFunction', + Handler: 'index.handler', + MemorySize: 128, + PackageType: 'Zip', + Role: 'arn:aws:iam::123456789012:role/lambda-role', + Runtime: 'nodejs18.x', + Timeout: 3, + }, + Type: 'AWS::Lambda::Function', + }, + }, + }), + }), + describeGeneratedTemplate: sandbox.stub().resolves({ + Status: 'COMPLETE', + Resources: [ + { + LogicalResourceId: 'testFunc', + ResourceType: 'AWS::Lambda::Function', + ResourceIdentifier: { + FunctionName: 'myFunction', + }, + }, + ], + }), + } + sandbox.stub(utils, 'getCFNClient').resolves(cfnClientStub) + + // 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', + }), + endpointUrl: undefined, + } + 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) + }) + + it('creates a basic CloudFormation template for the Lambda function', async function () { + const lambdaNode = { + name: 'myFunction', + regionCode: 'us-east-2', + arn: 'arn:aws:lambda:us-east-2:123456789012:function:myFunction', + } as LambdaFunctionNode + + lambdaClientStub.getFunction.resolves({ + Configuration: { + FunctionName: 'myFunction', + Handler: 'index.handler', + Role: 'arn:aws:iam::123456789012:role/lambda-role', + Runtime: 'nodejs18.x', + Timeout: 3, + MemorySize: 128, + Environment: { Variables: { KEY: 'value' } }, + }, + }) + + // Create a simple mock template that matches the Template type + const mockTemplate = { + AWSTemplateFormatVersion: '2010-09-09', + Resources: { + testFunc: { + DeletionPolicy: 'Retain', + Properties: { + Code: { + S3Bucket: 'aws-sam-cli-managed-default-samclisourcebucket-1n8tvb0jdhsd', + S3Key: '1d1c93ec17af7e2666ee20ea1a215c77', + }, + Environment: { + Variables: { + KEY: 'value', + }, + }, + FunctionName: 'myFunction', + Handler: 'index.handler', + MemorySize: 128, + PackageType: 'Zip', + Role: 'arn:aws:iam::123456789012:role/lambda-role', + Runtime: 'nodejs18.x', + Timeout: 3, + }, + Type: 'AWS::Lambda::Function', + }, + }, + } + const mockList = [ + { + LogicalResourceId: 'testFunc', + ResourceIdentifier: { + FunctionName: 'myFunction', + }, + ResourceType: 'AWS::Lambda::Function', + }, + ] + + const result = await lambda2sam.callExternalApiForCfnTemplate(lambdaNode) + // Verify the result structure matches expected format + assert.strictEqual(Array.isArray(result), true) + assert.strictEqual(result.length, 2) + const [template, resourcesToImport] = result + assert.strictEqual(typeof template, 'object') + assert.strictEqual(Array.isArray(resourcesToImport), true) + assert.strictEqual(resourcesToImport.length, 1) + assert.strictEqual(resourcesToImport[0].ResourceType, 'AWS::Lambda::Function') + assert.strictEqual(resourcesToImport[0].LogicalResourceId, 'testFunc') + assert.deepStrictEqual(result, [mockTemplate, mockList]) + }) + }) + + describe('determineStackAssociation', function () { + let lambdaClientStub: sinon.SinonStubbedInstance + + beforeEach(function () { + lambdaClientStub = sandbox.createStubInstance(DefaultLambdaClient) + sandbox + .stub(DefaultLambdaClient.prototype, 'getFunction') + .callsFake((name) => lambdaClientStub.getFunction(name)) + }) + + it('returns undefined when Lambda has no tags', async function () { + const lambdaNode = { + name: 'myFunction', + regionCode: 'us-west-2', + } as LambdaFunctionNode + + lambdaClientStub.getFunction.resolves({}) + + // Skip CloudFormation mocking for now + // This is difficult to mock correctly without errors and would be better tested with integration tests + + const result = await lambda2sam.determineStackAssociation(lambdaNode) + + assert.strictEqual(result, undefined) + assert.strictEqual(lambdaClientStub.getFunction.calledOnceWith(lambdaNode.name), true) + }) + + // For this function, additional testing would require complex mocking of the AWS SDK + // Consider adding more specific test cases in an integration test + }) +}) diff --git a/packages/core/src/test/awsService/appBuilder/lambda2sam/lambda2samCoreLogic.test.ts b/packages/core/src/test/awsService/appBuilder/lambda2sam/lambda2samCoreLogic.test.ts new file mode 100644 index 00000000000..c618dac2197 --- /dev/null +++ b/packages/core/src/test/awsService/appBuilder/lambda2sam/lambda2samCoreLogic.test.ts @@ -0,0 +1,754 @@ +/*! + * 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 * as vscode from 'vscode' +import * as lambda2sam from '../../../../awsService/appBuilder/lambda2sam/lambda2sam' +import * as cloudFormation from '../../../../shared/cloudformation/cloudformation' +import * as utils from '../../../../awsService/appBuilder/utils' +import * as walkthrough from '../../../../awsService/appBuilder/walkthrough' +import * as authUtils from '../../../../auth/utils' +import { getTestWindow } from '../../../shared/vscode/window' +import { fs } from '../../../../shared' +import { DefaultLambdaClient } from '../../../../shared/clients/lambdaClient' +import { LambdaFunctionNode } from '../../../../lambda/explorer/lambdaFunctionNode' +import { ToolkitError } from '../../../../shared/errors' +import os from 'os' +import path from 'path' +import { LAMBDA_FUNCTION_TYPE } from '../../../../shared/cloudformation/cloudformation' +import { ResourceToImport } from '@aws-sdk/client-cloudformation' + +describe('lambda2samCoreLogic', function () { + let sandbox: sinon.SinonSandbox + let tempDir: string + let lambdaClientStub: sinon.SinonStubbedInstance + let cfnClientStub: any + let downloadUnzipStub: sinon.SinonStub + + beforeEach(async function () { + sandbox = sinon.createSandbox() + tempDir = path.join(os.tmpdir(), `aws-toolkit-test-${Date.now()}`) + + // Create temp directory for tests - actually create it, don't stub + if (!(await fs.exists(vscode.Uri.file(tempDir)))) { + await fs.mkdir(vscode.Uri.file(tempDir)) + } + + // Create Lambda client stub with necessary properties + lambdaClientStub = sandbox.createStubInstance(DefaultLambdaClient) + Object.defineProperty(lambdaClientStub, 'defaultTimeoutInMs', { + value: 5 * 60 * 1000, // 5 minutes + configurable: true, + }) + Object.defineProperty(lambdaClientStub, 'createSdkClient', { + value: () => Promise.resolve({}), + configurable: true, + }) + + sandbox.stub(utils, 'getLambdaClient').returns(lambdaClientStub as any) + + // Mock CloudFormation client - now returns Promises directly (no .promise() method) + cfnClientStub = { + describeStackResource: sandbox.stub().resolves({ + StackResourceDetail: { + PhysicalResourceId: 'test-physical-id', + }, + }), + describeStackResources: sandbox.stub().resolves({ + StackResources: [ + { LogicalResourceId: 'testResource', PhysicalResourceId: 'test-physical-id' }, + { LogicalResourceId: 'prefixTestResource', PhysicalResourceId: 'prefix-test-physical-id' }, + ], + }), + describeStacks: sandbox.stub().resolves({ + Stacks: [ + { + StackId: 'stack-id', + StackName: 'test-stack', + StackStatus: 'CREATE_COMPLETE', + }, + ], + }), + getTemplate: sandbox.stub().resolves({ + TemplateBody: '{"Resources": {"TestFunc": {"Type": "AWS::Lambda::Function"}}}', + }), + getGeneratedTemplate: sandbox.stub().resolves({ + Status: 'COMPLETE', + TemplateBody: + '{"Resources": {"TestFunc": {"Type": "AWS::Lambda::Function", "Properties": {"FunctionName": "test-function"}}}}', + }), + describeGeneratedTemplate: sandbox.stub().resolves({ + Status: 'COMPLETE', + Resources: [ + { + LogicalResourceId: 'TestFunc', + ResourceType: 'AWS::Lambda::Function', + ResourceIdentifier: { + FunctionName: 'arn:aws:lambda:us-east-2:123456789012:function:test-function', + }, + }, + ], + }), + createChangeSet: sandbox.stub().resolves({ Id: 'change-set-id' }), + waitFor: sandbox.stub().resolves(), + executeChangeSet: sandbox.stub().resolves(), + describeChangeSet: sandbox.stub().resolves({ + StatusReason: 'Test reason', + }), + } + sandbox.stub(utils, 'getCFNClient').resolves(cfnClientStub) + + // Setup test window to return appropriate values + getTestWindow().onDidShowMessage((msg) => { + if (msg.message.includes('Enter Stack Name')) { + msg.selectItem('test-stack') + } + }) + + getTestWindow().onDidShowDialog((dialog) => { + dialog.selectItem(vscode.Uri.file(tempDir)) + }) + + // Stub downloadUnzip function + downloadUnzipStub = sandbox.stub(utils, 'downloadUnzip').callsFake(async (url, outputPath) => { + // Create a mock file structure for testing purposes + if (!(await fs.exists(outputPath))) { + await fs.mkdir(outputPath) + } + + await fs.writeFile( + vscode.Uri.joinPath(outputPath, 'index.js'), + 'exports.handler = async (event) => { return "Hello World" };' + ) + await fs.writeFile( + vscode.Uri.joinPath(outputPath, 'package.json'), + JSON.stringify( + { + name: 'test-lambda', + version: '1.0.0', + description: 'Test Lambda function', + }, + undefined, + 2 + ) + ) + }) + + // Stub workspace functions + sandbox.stub(vscode.workspace, 'openTextDocument').resolves({} as any) + sandbox.stub(vscode.window, 'showTextDocument').resolves() + }) + + afterEach(async function () { + sandbox.restore() + + // Clean up the temp directory + if (await fs.exists(vscode.Uri.file(tempDir))) { + await fs.delete(vscode.Uri.file(tempDir), { recursive: true, force: true }) + } + }) + + describe('processLambdaUrlResources', function () { + it('converts Lambda URL resources to FunctionUrlConfig', async function () { + // Setup resources with Lambda URL - using 'as any' to bypass strict typing for tests + const resources: cloudFormation.TemplateResources = { + TestFunc: { + Type: cloudFormation.SERVERLESS_FUNCTION_TYPE, + Properties: { + FunctionName: 'test-function', + PackageType: 'Zip', + }, + }, + TestFuncUrl: { + Type: cloudFormation.LAMBDA_URL_TYPE, + Properties: { + TargetFunctionArn: { Ref: 'TestFunc' }, + AuthType: 'NONE', + }, + }, + } as any + + // Call the function + await lambda2sam.processLambdaUrlResources(resources) + + // Verify URL resource is removed + assert.strictEqual(resources['TestFuncUrl'], undefined) + + // Verify FunctionUrlConfig added to function resource using non-null assertion + assert.deepStrictEqual(resources['TestFunc']!.Properties!.FunctionUrlConfig, { + AuthType: 'NONE', + Cors: undefined, + InvokeMode: undefined, + }) + }) + + it('skips URL resources with Qualifier property', async function () { + // Setup resources with Lambda URL including Qualifier - using 'as any' to bypass strict typing for tests + const resources: cloudFormation.TemplateResources = { + TestFunc: { + Type: cloudFormation.SERVERLESS_FUNCTION_TYPE, + Properties: { + FunctionName: 'test-function', + PackageType: 'Zip', + }, + }, + TestFuncUrl: { + Type: cloudFormation.LAMBDA_URL_TYPE, + Properties: { + TargetFunctionArn: { Ref: 'TestFunc' }, + AuthType: 'NONE', + Qualifier: 'prod', + }, + }, + } as any + + // Call the function + await lambda2sam.processLambdaUrlResources(resources) + + // Verify URL resource is still there (not transformed) + assert.notStrictEqual(resources['TestFuncUrl'], undefined) + + // Verify function resource doesn't have FunctionUrlConfig using non-null assertion + assert.strictEqual(resources['TestFunc']!.Properties!.FunctionUrlConfig, undefined) + }) + }) + + describe('processLambdaResources', function () { + it('transforms AWS::Lambda::Function to AWS::Serverless::Function', async function () { + // Setup resources with Lambda function - using 'as any' to bypass strict typing for tests + const resources: cloudFormation.TemplateResources = { + TestFunc: { + Type: cloudFormation.LAMBDA_FUNCTION_TYPE, + Properties: { + FunctionName: 'test-function', + Handler: 'index.handler', + Runtime: 'nodejs18.x', + Code: { + S3Bucket: 'test-bucket', + S3Key: 'test-key', + }, + Tags: [ + { Key: 'test-key', Value: 'test-value' }, + { Key: 'lambda:createdBy', Value: 'test' }, + ], + TracingConfig: { + Mode: 'Active', + }, + PackageType: 'Zip', + }, + }, + } as any + + const stackInfo = { + stackId: 'stack-id', + stackName: 'test-stack', + isSamTemplate: false, + template: {}, + } + + const projectDir = vscode.Uri.file(tempDir) + + // Add necessary stub for getFunction + lambdaClientStub.getFunction.resolves({ + Code: { Location: 'https://lambda-function-code.zip' }, + }) + + // Call the function + await lambda2sam.processLambdaResources(resources, projectDir, stackInfo, 'us-west-2') + + // Verify function type was transformed using non-null assertions + assert.strictEqual(resources['TestFunc']!.Type, cloudFormation.SERVERLESS_FUNCTION_TYPE) + + // Verify properties were transformed correctly using non-null assertions + assert.strictEqual(resources['TestFunc']!.Properties!.Code, undefined) + assert.strictEqual(resources['TestFunc']!.Properties!.CodeUri, 'TestFunc') + assert.strictEqual(resources['TestFunc']!.Properties!.Tracing, 'Active') + assert.strictEqual(resources['TestFunc']!.Properties!.TracingConfig, undefined) + assert.deepStrictEqual(resources['TestFunc']!.Properties!.Tags, { + 'test-key': 'test-value', + }) + + // Verify downloadLambdaFunctionCode was called + assert.strictEqual(downloadUnzipStub.calledOnce, true) + }) + + it('updates CodeUri for AWS::Serverless::Function', async function () { + // Setup resources with Serverless function - using 'as any' to bypass strict typing for tests + const resources: cloudFormation.TemplateResources = { + TestFunc: { + Type: cloudFormation.SERVERLESS_FUNCTION_TYPE, + Properties: { + FunctionName: 'test-function', + Handler: 'index.handler', + Runtime: 'nodejs18.x', + CodeUri: 's3://test-bucket/test-key', + PackageType: 'Zip', + }, + }, + } as any + + const stackInfo = { + stackId: 'stack-id', + stackName: 'test-stack', + isSamTemplate: false, + template: {}, + } + + const projectDir = vscode.Uri.file(tempDir) + + // Add necessary stub for getFunction + lambdaClientStub.getFunction.resolves({ + Code: { Location: 'https://lambda-function-code.zip' }, + }) + + // Call the function + await lambda2sam.processLambdaResources(resources, projectDir, stackInfo, 'us-west-2') + + // Verify CodeUri was updated using non-null assertions + assert.strictEqual(resources['TestFunc']!.Properties!.CodeUri, 'TestFunc') + + // Verify downloadLambdaFunctionCode was called + assert.strictEqual(downloadUnzipStub.calledOnce, true) + }) + }) + + describe('processLambdaLayerResources', function () { + it('transforms AWS::Lambda::LayerVersion to AWS::Serverless::LayerVersion', async function () { + // Setup resources with Lambda layer - using 'as any' to bypass strict typing for tests + const resources: cloudFormation.TemplateResources = { + TestLayer: { + Type: cloudFormation.LAMBDA_LAYER_TYPE, + Properties: { + LayerName: 'test-layer', + Content: { + S3Bucket: 'test-bucket', + S3Key: 'test-key', + }, + CompatibleRuntimes: ['nodejs18.x'], + }, + }, + } as any + + const stackInfo = { + stackId: 'stack-id', + stackName: 'test-stack', + isSamTemplate: false, + template: {}, + } + + const projectDir = vscode.Uri.file(tempDir) + + // Setup layer version stub + cfnClientStub.describeStackResource.resolves({ + StackResourceDetail: { + PhysicalResourceId: 'arn:aws:lambda:us-west-2:123456789012:layer:my-layer:1', + }, + }) + + lambdaClientStub.getLayerVersion.resolves({ + Content: { Location: 'https://lambda-layer-code.zip' }, + }) + + // Call the function + await lambda2sam.processLambdaLayerResources(resources, projectDir, stackInfo, 'us-west-2') + + // Verify layer type was transformed using non-null assertions + assert.strictEqual(resources['TestLayer']!.Type, cloudFormation.SERVERLESS_LAYER_TYPE) + + // Verify properties were transformed correctly using non-null assertions + assert.strictEqual(resources['TestLayer']!.Properties!.Content, undefined) + assert.strictEqual(resources['TestLayer']!.Properties!.ContentUri, 'TestLayer') + assert.deepStrictEqual(resources['TestLayer']!.Properties!.CompatibleRuntimes, ['nodejs18.x']) + + // Verify downloadLayerVersionResrouceByName was called (through downloadUnzip) + assert.strictEqual(downloadUnzipStub.calledOnce, true) + }) + + it('preserves AWS::Serverless::LayerVersion properties', async function () { + // Setup resources with Serverless layer - using 'as any' to bypass strict typing for tests + const resources: cloudFormation.TemplateResources = { + TestLayer: { + Type: cloudFormation.SERVERLESS_LAYER_TYPE, + Properties: { + LayerName: 'test-layer', + ContentUri: 's3://test-bucket/test-key', + CompatibleRuntimes: ['nodejs18.x'], + }, + }, + } as any + + const stackInfo = { + stackId: 'stack-id', + stackName: 'test-stack', + isSamTemplate: false, + template: {}, + } + + const projectDir = vscode.Uri.file(tempDir) + + // Setup layer version stub + cfnClientStub.describeStackResource.resolves({ + StackResourceDetail: { + PhysicalResourceId: 'arn:aws:lambda:us-west-2:123456789012:layer:my-layer:1', + }, + }) + + lambdaClientStub.getLayerVersion.resolves({ + Content: { Location: 'https://lambda-layer-code.zip' }, + }) + + // Call the function + await lambda2sam.processLambdaLayerResources(resources, projectDir, stackInfo, 'us-west-2') + + // Verify layer type is still serverless using non-null assertions + assert.strictEqual(resources['TestLayer']!.Type, cloudFormation.SERVERLESS_LAYER_TYPE) + + // Verify ContentUri was updated using non-null assertions + assert.strictEqual(resources['TestLayer']!.Properties!.ContentUri, 'TestLayer') + + // Verify downloadLayerVersionResrouceByName was called (through downloadUnzip) + assert.strictEqual(downloadUnzipStub.calledOnce, true) + }) + }) + + describe('deployCfnTemplate', function () { + it('deploys a CloudFormation template and returns stack info', async function () { + // Setup CloudFormation template + const template: cloudFormation.Template = mockCloudFormationTemplate() + + // Setup Lambda node + const lambdaNode = mockLambdaNode() + + const resourceToImport: ResourceToImport[] = [ + { + ResourceType: LAMBDA_FUNCTION_TYPE, + LogicalResourceId: 'TestFunc', + ResourceIdentifier: { + FunctionName: lambdaNode.name, + }, + }, + ] + + // Call the function + const result = await lambda2sam.deployCfnTemplate( + template, + resourceToImport, + 'test-stack', + lambdaNode.regionCode + ) + + // Verify createChangeSet was called with correct parameters + assert.strictEqual(cfnClientStub.createChangeSet.called, true) + const createChangeSetArgs = cfnClientStub.createChangeSet.firstCall.args[0] + assert.strictEqual(createChangeSetArgs.StackName, 'test-stack') + assert.strictEqual(createChangeSetArgs.ChangeSetType, 'IMPORT') + + // Verify waitFor and executeChangeSet were called + assert.strictEqual(cfnClientStub.waitFor.calledWith('changeSetCreateComplete'), true) + assert.strictEqual(cfnClientStub.executeChangeSet.called, true) + + // Verify describeStacks was called to get stack ID + assert.strictEqual(cfnClientStub.describeStacks.called, true) + + // Verify result structure + assert.strictEqual(result.stackId, 'stack-id') + assert.strictEqual(result.stackName, 'test-stack') + assert.strictEqual(result.isSamTemplate, false) + assert.deepStrictEqual(result.template, template) + }) + + it('throws an error when change set creation fails', async function () { + // Setup CloudFormation template + const template: cloudFormation.Template = mockCloudFormationTemplate() + + // Setup Lambda node + const lambdaNode = mockLambdaNode() + + // Make createChangeSet fail + cfnClientStub.createChangeSet.resolves({}) // No Id + + const resourceToImport: ResourceToImport[] = [ + { + ResourceType: LAMBDA_FUNCTION_TYPE, + LogicalResourceId: 'TestFunc', + ResourceIdentifier: { + FunctionName: lambdaNode.name, + }, + }, + ] + + // Call the function and expect error + await assert.rejects( + lambda2sam.deployCfnTemplate(template, resourceToImport, 'test-stack', lambdaNode.regionCode), + (err: ToolkitError) => { + assert.strictEqual(err.message.includes('Failed to create change set'), true) + return true + } + ) + }) + }) + + describe('callExternalApiForCfnTemplate', function () { + it('extracts function name from ARN in ResourceIdentifier', async function () { + // Setup Lambda node + const lambdaNode = mockLambdaNode(true) + + // Mock IAM connection + const mockConnection = mockIamConnection() + sandbox.stub(authUtils, 'getIAMConnection').resolves(mockConnection) + + // Mock fetch response + const mockFetch = mockFetchResponse(sandbox) + + // Setup CloudFormation client to return ARN in ResourceIdentifier + cfnClientStub.describeGeneratedTemplate.resolves({ + Status: 'COMPLETE', + Resources: [ + { + LogicalResourceId: 'TestFunc', + ResourceType: 'AWS::Lambda::Function', + ResourceIdentifier: { + FunctionName: 'arn:aws:lambda:us-east-2:123456789012:function:test-function', + }, + }, + ], + }) + + // Call the function + const [_, resourcesToImport] = await lambda2sam.callExternalApiForCfnTemplate(lambdaNode) + + // Verify that the ARN was converted to just the function name + assert.strictEqual(resourcesToImport.length, 1) + assert.strictEqual(resourcesToImport[0].ResourceType, 'AWS::Lambda::Function') + assert.strictEqual(resourcesToImport[0].LogicalResourceId, 'TestFunc') + assert.strictEqual(resourcesToImport[0].ResourceIdentifier!.FunctionName, 'test-function') + + // Verify API calls were made + assert.strictEqual(mockFetch.calledOnce, true) + assert.strictEqual(cfnClientStub.getGeneratedTemplate.calledOnce, true) + assert.strictEqual(cfnClientStub.describeGeneratedTemplate.calledOnce, true) + }) + + it('preserves function name when not an ARN', async function () { + // Setup Lambda node + const lambdaNode = mockLambdaNode(true) + + // Mock IAM connection + const mockConnection = mockIamConnection() + sandbox.stub(authUtils, 'getIAMConnection').resolves(mockConnection) + + // Mock fetch response + mockFetchResponse(sandbox) + + // Setup CloudFormation client to return plain function name + cfnClientStub.describeGeneratedTemplate.resolves({ + Status: 'COMPLETE', + Resources: [ + { + LogicalResourceId: 'TestFunc', + ResourceType: 'AWS::Lambda::Function', + ResourceIdentifier: { + FunctionName: 'test-function', + }, + }, + ], + }) + + // Call the function + const [_, resourcesToImport] = await lambda2sam.callExternalApiForCfnTemplate(lambdaNode) + + // Verify that the function name was preserved + assert.strictEqual(resourcesToImport.length, 1) + assert.strictEqual(resourcesToImport[0].ResourceIdentifier!.FunctionName, 'test-function') + }) + + it('handles non-Lambda resources without modification', async function () { + // Setup Lambda node + const lambdaNode = mockLambdaNode(true) + + // Mock IAM connection + const mockConnection = mockIamConnection() + sandbox.stub(authUtils, 'getIAMConnection').resolves(mockConnection) + + // Mock fetch response + mockFetchResponse(sandbox) + + // Setup CloudFormation client to return mixed resource types + cfnClientStub.describeGeneratedTemplate.resolves({ + Status: 'COMPLETE', + Resources: [ + { + LogicalResourceId: 'TestFunc', + ResourceType: 'AWS::Lambda::Function', + ResourceIdentifier: { + FunctionName: 'arn:aws:lambda:us-east-2:123456789012:function:test-function', + }, + }, + { + LogicalResourceId: 'TestRole', + ResourceType: 'AWS::IAM::Role', + ResourceIdentifier: { + RoleName: 'test-role', + }, + }, + ], + }) + + // Call the function + const [_, resourcesToImport] = await lambda2sam.callExternalApiForCfnTemplate(lambdaNode) + + // Verify that Lambda function ARN was converted but IAM role was not + assert.strictEqual(resourcesToImport.length, 2) + + const lambdaResource = resourcesToImport.find((r) => r.ResourceType === 'AWS::Lambda::Function') + const iamResource = resourcesToImport.find((r) => r.ResourceType === 'AWS::IAM::Role') + + assert.strictEqual(lambdaResource!.ResourceIdentifier!.FunctionName, 'test-function') + assert.strictEqual(iamResource!.ResourceIdentifier!.RoleName, 'test-role') + }) + }) + + describe('lambdaToSam', function () { + it('converts a Lambda function to a SAM project', async function () { + // Setup Lambda node + const lambdaNode = mockLambdaNode() + + // Setup AWS Lambda client responses + lambdaClientStub.getFunction.resolves({ + Tags: { + 'aws:cloudformation:stack-id': 'stack-id', + 'aws:cloudformation:stack-name': 'test-stack', + }, + Configuration: { + FunctionName: 'test-function', + Handler: 'index.handler', + Runtime: 'nodejs18.x', + }, + Code: { + Location: 'https://lambda-function-code.zip', + }, + }) + + // Setup CloudFormation client responses + cfnClientStub.describeStacks.resolves({ + Stacks: [ + { + StackId: 'stack-id', + StackName: 'test-stack', + StackStatus: 'CREATE_COMPLETE', + }, + ], + }) + + cfnClientStub.getTemplate.resolves({ + TemplateBody: JSON.stringify({ + AWSTemplateFormatVersion: '2010-09-09', + Transform: 'AWS::Serverless-2016-10-31', + Resources: { + TestFunc: { + Type: 'AWS::Serverless::Function', + Properties: { + FunctionName: 'test-function', + Handler: 'index.handler', + Runtime: 'nodejs18.x', + CodeUri: 's3://test-bucket/test-key', + PackageType: 'Zip', + }, + }, + }, + }), + }) + + // Setup test window to return a project directory + getTestWindow().onDidShowDialog((dialog) => { + dialog.selectItem(vscode.Uri.file(tempDir)) + }) + // Spy on walkthrough.openProjectInWorkspace + const openProjectStub = sandbox.stub(walkthrough, 'openProjectInWorkspace') + + // Call the function + await lambda2sam.lambdaToSam(lambdaNode) + + assert.strictEqual( + await fs.exists(vscode.Uri.joinPath(vscode.Uri.file(tempDir), 'test-stack', 'template.yaml').fsPath), + true, + 'template.yaml was not written' + ) + assert.strictEqual( + await fs.exists(vscode.Uri.joinPath(vscode.Uri.file(tempDir), 'test-stack', 'README.md').fsPath), + true, + 'README.md was not written' + ) + assert.strictEqual( + await fs.exists(vscode.Uri.joinPath(vscode.Uri.file(tempDir), 'test-stack', 'samconfig.toml').fsPath), + true, + 'samconfig.toml was not written' + ) + + // Verify that project was opened in workspace + assert.strictEqual(openProjectStub.calledOnce, true) + assert.strictEqual( + openProjectStub.firstCall.args[0].fsPath, + vscode.Uri.joinPath(vscode.Uri.file(tempDir), 'test-stack').fsPath + ) + }) + }) + + 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/lambda2sam/lambda2samDownload.test.ts b/packages/core/src/test/awsService/appBuilder/lambda2sam/lambda2samDownload.test.ts new file mode 100644 index 00000000000..9c4d3122918 --- /dev/null +++ b/packages/core/src/test/awsService/appBuilder/lambda2sam/lambda2samDownload.test.ts @@ -0,0 +1,312 @@ +/*! + * 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 * as vscode from 'vscode' +import * as lambda2sam from '../../../../awsService/appBuilder/lambda2sam/lambda2sam' +import * as utils from '../../../../awsService/appBuilder/utils' +import { fs } from '../../../../shared' +import { DefaultLambdaClient } from '../../../../shared/clients/lambdaClient' +import os from 'os' +import path from 'path' +import { LAMBDA_FUNCTION_TYPE, LAMBDA_LAYER_TYPE } from '../../../../shared/cloudformation/cloudformation' + +describe('lambda2samDownload', function () { + let sandbox: sinon.SinonSandbox + let tempDir: string + let lambdaClientStub: sinon.SinonStubbedInstance + let cfnClientStub: any + let downloadUnzipStub: sinon.SinonStub + + beforeEach(async function () { + sandbox = sinon.createSandbox() + tempDir = path.join(os.tmpdir(), `aws-toolkit-test-${Date.now()}`) + + // Create temp directory for tests - actually create it, don't stub + if (!(await fs.exists(vscode.Uri.file(tempDir)))) { + await fs.mkdir(vscode.Uri.file(tempDir)) + } + + // Create Lambda client stub with necessary properties + lambdaClientStub = sandbox.createStubInstance(DefaultLambdaClient) + // Add required properties that aren't stubbed automatically + Object.defineProperty(lambdaClientStub, 'defaultTimeoutInMs', { + value: 5 * 60 * 1000, // 5 minutes + configurable: true, + }) + Object.defineProperty(lambdaClientStub, 'createSdkClient', { + value: () => Promise.resolve({}), + configurable: true, + }) + + sandbox.stub(utils, 'getLambdaClient').returns(lambdaClientStub as any) + + // Stub CloudFormation client - now returns Promises directly (no .promise() method) + cfnClientStub = { + describeStackResource: sandbox.stub().resolves({ + StackResourceDetail: { + PhysicalResourceId: 'test-physical-id', + }, + }), + describeStackResources: sandbox.stub().resolves({ + StackResources: [ + { LogicalResourceId: 'testResource', PhysicalResourceId: 'test-physical-id' }, + { LogicalResourceId: 'prefixTestResource', PhysicalResourceId: 'prefix-test-physical-id' }, + ], + }), + } + sandbox.stub(utils, 'getCFNClient').resolves(cfnClientStub) + + // Stub downloadUnzip function to create actual files in the temp directory + downloadUnzipStub = sandbox.stub(utils, 'downloadUnzip').callsFake(async (url, outputPath) => { + // Create a mock file structure for testing purposes + + // Create the output directory if it doesn't exist + if (!(await fs.exists(outputPath))) { + await fs.mkdir(outputPath) + } + + // Create a simple file to simulate extracted content + await fs.writeFile( + vscode.Uri.joinPath(outputPath, 'index.js'), + 'exports.handler = async (event) => { return "Hello World" };' + ) + + // Create a package.json file + await fs.writeFile( + vscode.Uri.joinPath(outputPath, 'package.json'), + JSON.stringify( + { + name: 'test-lambda', + version: '1.0.0', + description: 'Test Lambda function', + }, + undefined, + 2 + ) + ) + }) + }) + + afterEach(async function () { + sandbox.restore() + + // Clean up the temp directory after each test + if (await fs.exists(vscode.Uri.file(tempDir))) { + await fs.delete(vscode.Uri.file(tempDir), { recursive: true, force: true }) + } + }) + + describe('getPhysicalIdfromCFNResourceName', function () { + it('returns the physical ID when an exact match is found', async function () { + const result = await lambda2sam.getPhysicalIdfromCFNResourceName( + 'testResource', + 'us-west-2', + 'stack-id', + LAMBDA_FUNCTION_TYPE + ) + + assert.strictEqual(cfnClientStub.describeStackResource.calledOnce, true) + assert.strictEqual(cfnClientStub.describeStackResource.firstCall.args[0].StackName, 'stack-id') + assert.strictEqual(cfnClientStub.describeStackResource.firstCall.args[0].LogicalResourceId, 'testResource') + assert.strictEqual(result, 'test-physical-id') + }) + + it('returns a prefix match when exact match fails', async function () { + // Make exact match fail + cfnClientStub.describeStackResource.rejects(new Error('Resource not found')) + + const result = await lambda2sam.getPhysicalIdfromCFNResourceName( + 'prefix', + 'us-west-2', + 'stack-id', + LAMBDA_LAYER_TYPE + ) + + assert.strictEqual(cfnClientStub.describeStackResources.calledOnce, true) + assert.strictEqual(cfnClientStub.describeStackResources.firstCall.args[0].StackName, 'stack-id') + assert.strictEqual(result, 'prefix-test-physical-id') + }) + + it('returns undefined when no match is found', async function () { + // Make exact match fail + cfnClientStub.describeStackResource.rejects(new Error('Resource not found')) + + // Return empty resources + cfnClientStub.describeStackResources.resolves({ StackResources: [] }) + + const result = await lambda2sam.getPhysicalIdfromCFNResourceName( + 'nonexistent', + 'us-west-2', + 'stack-id', + LAMBDA_LAYER_TYPE + ) + assert.strictEqual(result, undefined) + }) + }) + + describe('downloadLambdaFunctionCode', function () { + it('uses physical ID from CloudFormation when not provided', async function () { + const targetDir = vscode.Uri.file(tempDir) + const resourceName = 'testResource' + const stackInfo = { stackId: 'stack-id', stackName: 'test-stack', isSamTemplate: false, template: {} } + + lambdaClientStub.getFunction.resolves({ + Code: { Location: 'https://lambda-function-code.zip' }, + }) + + await lambda2sam.downloadLambdaFunctionCode(resourceName, stackInfo, targetDir, 'us-west-2') + + // Verify CloudFormation was called to get physical ID + assert.strictEqual(cfnClientStub.describeStackResource.calledOnce, true) + + // Verify Lambda client was called with correct physical ID + assert.strictEqual(lambdaClientStub.getFunction.calledOnce, true) + assert.strictEqual(lambdaClientStub.getFunction.firstCall.args[0], 'test-physical-id') + + // Verify downloadUnzip was called with correct parameters + assert.strictEqual(downloadUnzipStub.calledOnce, true) + assert.strictEqual(downloadUnzipStub.firstCall.args[0], 'https://lambda-function-code.zip') + assert.strictEqual( + downloadUnzipStub.firstCall.args[1].fsPath, + vscode.Uri.joinPath(targetDir, resourceName).fsPath + ) + + // Verify files were actually created in the temp directory + const outputDir = vscode.Uri.joinPath(targetDir, resourceName) + assert.strictEqual(await fs.exists(outputDir), true) + assert.strictEqual(await fs.exists(vscode.Uri.joinPath(outputDir, 'index.js')), true) + assert.strictEqual(await fs.exists(vscode.Uri.joinPath(outputDir, 'package.json')), true) + }) + + it('uses provided physical ID when available', async function () { + const targetDir = vscode.Uri.file(tempDir) + const resourceName = 'testResource' + const physicalResourceId = 'provided-physical-id' + const stackInfo = { stackId: 'stack-id', stackName: 'test-stack', isSamTemplate: false, template: {} } + + lambdaClientStub.getFunction.resolves({ + Code: { Location: 'https://lambda-function-code.zip' }, + }) + + await lambda2sam.downloadLambdaFunctionCode( + resourceName, + stackInfo, + targetDir, + 'us-west-2', + physicalResourceId + ) + + // Verify CloudFormation was NOT called to get physical ID + assert.strictEqual(cfnClientStub.describeStackResource.called, false) + + // Verify Lambda client was called with provided physical ID + assert.strictEqual(lambdaClientStub.getFunction.calledOnce, true) + assert.strictEqual(lambdaClientStub.getFunction.firstCall.args[0], physicalResourceId) + + // Verify files were actually created in the temp directory + const outputDir = vscode.Uri.joinPath(targetDir, resourceName) + assert.strictEqual(await fs.exists(outputDir), true) + assert.strictEqual(await fs.exists(vscode.Uri.joinPath(outputDir, 'index.js')), true) + assert.strictEqual(await fs.exists(vscode.Uri.joinPath(outputDir, 'package.json')), true) + }) + + it('throws an error when code location is missing', async function () { + const targetDir = vscode.Uri.file(tempDir) + const resourceName = 'testResource' + const stackInfo = { stackId: 'stack-id', stackName: 'test-stack', isSamTemplate: false, template: {} } + + lambdaClientStub.getFunction.resolves({ + Code: {}, // No Location + }) + + await assert.rejects( + lambda2sam.downloadLambdaFunctionCode(resourceName, stackInfo, targetDir, 'us-west-2'), + /Could not determine code location/ + ) + }) + }) + + describe('downloadLayerVersionResourceByName', function () { + it('extracts layer name and version from ARN and downloads content', async function () { + const targetDir = vscode.Uri.file(tempDir) + const resourceName = 'testLayer' + const stackInfo = { stackId: 'stack-id', stackName: 'test-stack', isSamTemplate: false, template: {} } + + // Return an ARN for a layer version + cfnClientStub.describeStackResource.resolves({ + StackResourceDetail: { + PhysicalResourceId: 'arn:aws:lambda:us-west-2:123456789012:layer:my-layer:1', + }, + }) + + lambdaClientStub.getLayerVersion.resolves({ + Content: { Location: 'https://lambda-layer-code.zip' }, + }) + + await lambda2sam.downloadLayerVersionResourceByName(resourceName, stackInfo, targetDir, 'us-west-2') + + // Verify Lambda client was called with correct layer name and version + assert.strictEqual(lambdaClientStub.getLayerVersion.calledOnce, true) + assert.strictEqual(lambdaClientStub.getLayerVersion.firstCall.args[0], 'my-layer') + assert.strictEqual(lambdaClientStub.getLayerVersion.firstCall.args[1], 1) + + // Verify downloadUnzip was called with correct parameters + assert.strictEqual(downloadUnzipStub.calledOnce, true) + assert.strictEqual(downloadUnzipStub.firstCall.args[0], 'https://lambda-layer-code.zip') + assert.strictEqual( + downloadUnzipStub.firstCall.args[1].fsPath, + vscode.Uri.joinPath(targetDir, resourceName).fsPath + ) + + // Verify files were actually created in the temp directory + const outputDir = vscode.Uri.joinPath(targetDir, resourceName) + assert.strictEqual(await fs.exists(outputDir), true) + assert.strictEqual(await fs.exists(vscode.Uri.joinPath(outputDir, 'index.js')), true) + assert.strictEqual(await fs.exists(vscode.Uri.joinPath(outputDir, 'package.json')), true) + }) + + it('throws an error when ARN format is invalid', async function () { + const targetDir = vscode.Uri.file(tempDir) + const resourceName = 'testLayer' + const stackInfo = { stackId: 'stack-id', stackName: 'test-stack', isSamTemplate: false, template: {} } + + // Return an invalid ARN + cfnClientStub.describeStackResource.resolves({ + StackResourceDetail: { + PhysicalResourceId: 'arn:aws:lambda:us-west-2:123456789012:layer:my-layer', // Missing version + }, + }) + + await assert.rejects( + lambda2sam.downloadLayerVersionResourceByName(resourceName, stackInfo, targetDir, 'us-west-2'), + /Invalid layer ARN format/ + ) + }) + + it('throws an error when layer content location is missing', async function () { + const targetDir = vscode.Uri.file(tempDir) + const resourceName = 'testLayer' + const stackInfo = { stackId: 'stack-id', stackName: 'test-stack', isSamTemplate: false, template: {} } + + // Return an ARN for a layer version + cfnClientStub.describeStackResource.resolves({ + StackResourceDetail: { + PhysicalResourceId: 'arn:aws:lambda:us-west-2:123456789012:layer:my-layer:1', + }, + }) + + lambdaClientStub.getLayerVersion.resolves({ + Content: {}, // No Location + }) + + await assert.rejects( + lambda2sam.downloadLayerVersionResourceByName(resourceName, stackInfo, targetDir, 'us-west-2'), + /Could not determine code location for layer/ + ) + }) + }) +}) diff --git a/packages/core/src/test/awsService/appBuilder/serverlessLand/main.test.ts b/packages/core/src/test/awsService/appBuilder/serverlessLand/main.test.ts index 05caee605ec..5ed49638dc8 100644 --- a/packages/core/src/test/awsService/appBuilder/serverlessLand/main.test.ts +++ b/packages/core/src/test/awsService/appBuilder/serverlessLand/main.test.ts @@ -23,6 +23,7 @@ import { fs } from '../../../../shared/fs/fs' import * as downloadPatterns from '../../../../shared/utilities/downloadPatterns' import { ExtContext } from '../../../../shared/extensions' import { workspaceUtils } from '../../../../shared' +import * as messages from '../../../../shared/utilities/messages' import * as downloadPattern from '../../../../shared/utilities/downloadPatterns' import * as wizardModule from '../../../../awsService/appBuilder/serverlessLand/wizard' @@ -80,63 +81,75 @@ describe('createNewServerlessLandProject', () => { }) }) +function assertDownloadPatternCall(getPatternStub: sinon.SinonStub, mockConfig: any) { + const mockAssetName = 'test-project-sam-python.zip' + const serverlessLandOwner = 'aws-samples' + const serverlessLandRepo = 'serverless-patterns' + const mockLocation = vscode.Uri.joinPath(mockConfig.location, mockConfig.name) + + assert(getPatternStub.calledOnce) + assert(getPatternStub.firstCall.args[0] === serverlessLandOwner) + assert(getPatternStub.firstCall.args[1] === serverlessLandRepo) + assert(getPatternStub.firstCall.args[2] === mockAssetName) + assert(getPatternStub.firstCall.args[3].toString() === mockLocation.toString()) + assert(getPatternStub.firstCall.args[4] === true) +} + describe('downloadPatternCode', () => { let sandbox: sinon.SinonSandbox let getPatternStub: sinon.SinonStub + let mockConfig: any beforeEach(function () { sandbox = sinon.createSandbox() getPatternStub = sandbox.stub(downloadPatterns, 'getPattern') + mockConfig = { + name: 'test-project', + location: vscode.Uri.file('/test'), + pattern: 'test-project-sam-python', + runtime: 'python', + iac: 'sam', + assetName: 'test-project-sam-python', + } }) afterEach(function () { sandbox.restore() + getPatternStub.restore() }) - const mockConfig = { - name: 'test-project', - location: vscode.Uri.file('/test'), - pattern: 'test-project-sam-python', - runtime: 'python', - iac: 'sam', - assetName: 'test-project-sam-python', - } it('successfully downloads pattern code', async () => { - const mockAssetName = 'test-project-sam-python.zip' - const serverlessLandOwner = 'aws-samples' - const serverlessLandRepo = 'serverless-patterns' - const mockLocation = vscode.Uri.joinPath(mockConfig.location, mockConfig.name) - - await downloadPatternCode(mockConfig, 'test-project-sam-python') - assert(getPatternStub.calledOnce) - assert(getPatternStub.firstCall.args[0] === serverlessLandOwner) - assert(getPatternStub.firstCall.args[1] === serverlessLandRepo) - assert(getPatternStub.firstCall.args[2] === mockAssetName) - assert(getPatternStub.firstCall.args[3].toString() === mockLocation.toString()) - assert(getPatternStub.firstCall.args[4] === true) - }) - it('handles download failure', async () => { - const mockAssetName = 'test-project-sam-python.zip' - const error = new Error('Download failed') - getPatternStub.rejects(error) - try { - await downloadPatternCode(mockConfig, mockAssetName) - assert.fail('Expected an error to be thrown') - } catch (err: any) { - assert.strictEqual(err.message, 'Failed to download pattern: Error: Download failed') - } + sandbox.stub(messages, 'handleOverwriteConflict').resolves(true) + + await downloadPatternCode(mockConfig, mockConfig.assetName) + assertDownloadPatternCall(getPatternStub, mockConfig) + }) + it('downloads pattern when directory exists and user confirms overwrite', async function () { + sandbox.stub(messages, 'handleOverwriteConflict').resolves(true) + + await downloadPatternCode(mockConfig, mockConfig.assetName) + assertDownloadPatternCall(getPatternStub, mockConfig) + }) + it('throws error when directory exists and user cancels overwrite', async function () { + const handleOverwriteStub = sandbox.stub(messages, 'handleOverwriteConflict') + handleOverwriteStub.rejects(new Error('Folder already exists: test-project')) + + await assert.rejects( + () => downloadPatternCode(mockConfig, mockConfig.assetName), + /Folder already exists: test-project/ + ) }) }) describe('openReadmeFile', () => { - let sandbox: sinon.SinonSandbox + let testsandbox: sinon.SinonSandbox let spyExecuteCommand: sinon.SinonSpy beforeEach(function () { - sandbox = sinon.createSandbox() - spyExecuteCommand = sandbox.spy(vscode.commands, 'executeCommand') + testsandbox = sinon.createSandbox() + spyExecuteCommand = testsandbox.spy(vscode.commands, 'executeCommand') }) afterEach(function () { - sandbox.restore() + testsandbox.restore() }) const mockConfig = { name: 'test-project', @@ -148,35 +161,35 @@ describe('openReadmeFile', () => { } it('successfully opens README file', async () => { const mockReadmeUri = vscode.Uri.file('/test/README.md') - sandbox.stub(main, 'getProjectUri').resolves(mockReadmeUri) + testsandbox.stub(main, 'getProjectUri').resolves(mockReadmeUri) - sandbox.stub(fs, 'exists').resolves(true) + testsandbox.stub(fs, 'exists').resolves(true) // When await openReadmeFile(mockConfig) // Then - sandbox.assert.calledWith(spyExecuteCommand, 'workbench.action.focusFirstEditorGroup') - sandbox.assert.calledWith(spyExecuteCommand, 'markdown.showPreview') + testsandbox.assert.calledWith(spyExecuteCommand, 'workbench.action.focusFirstEditorGroup') + testsandbox.assert.calledWith(spyExecuteCommand, 'markdown.showPreview') }) it('handles missing README file', async () => { const mockReadmeUri = vscode.Uri.file('/test/file.md') - sandbox.stub(main, 'getProjectUri').resolves(mockReadmeUri) + testsandbox.stub(main, 'getProjectUri').resolves(mockReadmeUri) - sandbox.stub(fs, 'exists').resolves(false) + testsandbox.stub(fs, 'exists').resolves(false) // When await openReadmeFile(mockConfig) // Then - sandbox.assert.neverCalledWith(spyExecuteCommand, 'markdown.showPreview') + testsandbox.assert.neverCalledWith(spyExecuteCommand, 'markdown.showPreview') assert.ok(true, 'Function should return without throwing error when README is not found') }) it('handles error with opening README file', async () => { const mockReadmeUri = vscode.Uri.file('/test/README.md') - sandbox.stub(main, 'getProjectUri').resolves(mockReadmeUri) + testsandbox.stub(main, 'getProjectUri').resolves(mockReadmeUri) - sandbox.stub(fs, 'exists').rejects(new Error('File system error')) + testsandbox.stub(fs, 'exists').rejects(new Error('File system error')) // When await assert.rejects(() => openReadmeFile(mockConfig), { @@ -184,7 +197,7 @@ describe('openReadmeFile', () => { message: 'Error processing README file', }) // Then - sandbox.assert.neverCalledWith(spyExecuteCommand, 'markdown.showPreview') + testsandbox.assert.neverCalledWith(spyExecuteCommand, 'markdown.showPreview') }) }) diff --git a/packages/core/src/test/awsService/appBuilder/utils.test.ts b/packages/core/src/test/awsService/appBuilder/utils.test.ts index d74cfc77802..08edd14f47c 100644 --- a/packages/core/src/test/awsService/appBuilder/utils.test.ts +++ b/packages/core/src/test/awsService/appBuilder/utils.test.ts @@ -12,12 +12,25 @@ 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 { runOpenHandler, runOpenTemplate } from '../../../awsService/appBuilder/utils' +import { + runOpenHandler, + runOpenTemplate, + isPermissionError, + EnhancedLambdaClient, + EnhancedCloudFormationClient, + getLambdaClient, + getCFNClient, +} from '../../../awsService/appBuilder/utils' import { TreeNode } from '../../../shared/treeview/resourceTreeDataProvider' 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 @@ -303,4 +316,533 @@ describe('AppBuilder Utils', function () { assert(showCommand.notCalled) }) }) + + describe('Permission Error Handling', function () { + let sandbox: sinon.SinonSandbox + + beforeEach(function () { + sandbox = sinon.createSandbox() + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('isPermissionError', function () { + it('should return true for AccessDeniedException', function () { + const error = Object.assign(new Error('Access denied'), { + code: 'AccessDeniedException', + time: new Date(), + statusCode: 403, + }) + assert.strictEqual(isPermissionError(error), true) + }) + + it('should return true for UnauthorizedOperation', function () { + const error = Object.assign(new Error('Unauthorized'), { + code: 'UnauthorizedOperation', + time: new Date(), + statusCode: 403, + }) + assert.strictEqual(isPermissionError(error), true) + }) + + it('should return true for Forbidden', function () { + const error = Object.assign(new Error('Forbidden'), { + code: 'Forbidden', + time: new Date(), + statusCode: 403, + }) + assert.strictEqual(isPermissionError(error), true) + }) + + it('should return true for AccessDenied', function () { + const error = Object.assign(new Error('Access denied'), { + code: 'AccessDenied', + time: new Date(), + statusCode: 403, + }) + assert.strictEqual(isPermissionError(error), true) + }) + + it('should return true for 403 status code', function () { + const error = Object.assign(new Error('Forbidden'), { + code: 'SomeError', + statusCode: 403, + time: new Date(), + }) + assert.strictEqual(isPermissionError(error), true) + }) + + it('should return false for non-permission errors', function () { + const error = Object.assign(new Error('Resource not found'), { + code: 'ResourceNotFoundException', + time: new Date(), + statusCode: 404, + }) + assert.strictEqual(isPermissionError(error), false) + }) + + it('should return false for non-AWS errors', function () { + const error = new Error('Regular error') + assert.strictEqual(isPermissionError(error), false) + }) + + it('should return false for undefined', function () { + assert.strictEqual(isPermissionError(undefined), false) + }) + }) + + describe('EnhancedLambdaClient', function () { + let mockLambdaClient: sinon.SinonStubbedInstance + let enhancedClient: EnhancedLambdaClient + + beforeEach(function () { + mockLambdaClient = sandbox.createStubInstance(DefaultLambdaClient) + // Add missing properties that EnhancedLambdaClient expects + Object.defineProperty(mockLambdaClient, 'defaultTimeoutInMs', { + value: 5 * 60 * 1000, + configurable: true, + }) + Object.defineProperty(mockLambdaClient, 'createSdkClient', { + value: sandbox.stub().resolves({}), + configurable: true, + }) + enhancedClient = new EnhancedLambdaClient(mockLambdaClient as any, 'us-east-1') + }) + + it('should enhance permission errors for getFunction', async function () { + const permissionError = Object.assign(new Error('Access denied'), { + code: 'AccessDeniedException', + time: new Date(), + statusCode: 403, + }) + mockLambdaClient.getFunction.rejects(permissionError) + + try { + await enhancedClient.getFunction('test-function') + assert.fail('Expected error to be thrown') + } catch (error) { + assert(error instanceof ToolkitError) + assert( + error.message.includes('Permission denied: Missing required permissions for lambda:getFunction') + ) + assert(error.message.includes('lambda:GetFunction')) + assert(error.message.includes('arn:aws:lambda:us-east-1:*:function:test-function')) + assert(error.message.includes('To fix this issue:')) + assert(error.message.includes('Documentation:')) + } + }) + + it('should pass through non-permission errors for getFunction', async function () { + const nonPermissionError = new Error('Function not found') + mockLambdaClient.getFunction.rejects(nonPermissionError) + + try { + await enhancedClient.getFunction('test-function') + assert.fail('Expected error to be thrown') + } catch (error) { + assert.strictEqual(error, nonPermissionError) + } + }) + + it('should enhance permission errors for listFunctions', async function () { + const permissionError = Object.assign(new Error('Access denied'), { + code: 'AccessDeniedException', + time: new Date(), + statusCode: 403, + }) + + // Create a mock async generator that throws the error + const mockAsyncGenerator = async function* (): AsyncIterableIterator { + throw permissionError + yield // This line will never be reached but satisfies ESLint require-yield rule + } + mockLambdaClient.listFunctions.returns(mockAsyncGenerator()) + + try { + const iterator = enhancedClient.listFunctions() + await iterator.next() + assert.fail('Expected error to be thrown') + } catch (error) { + assert(error instanceof ToolkitError) + assert( + error.message.includes( + 'Permission denied: Missing required permissions for lambda:listFunctions' + ) + ) + assert(error.message.includes('lambda:ListFunctions')) + } + }) + + it('should enhance permission errors for deleteFunction', async function () { + const permissionError = Object.assign(new Error('Access denied'), { + code: 'AccessDeniedException', + time: new Date(), + statusCode: 403, + }) + mockLambdaClient.deleteFunction.rejects(permissionError) + + try { + await enhancedClient.deleteFunction('test-function') + assert.fail('Expected error to be thrown') + } catch (error) { + assert(error instanceof ToolkitError) + assert( + error.message.includes( + 'Permission denied: Missing required permissions for lambda:deleteFunction' + ) + ) + assert(error.message.includes('lambda:DeleteFunction')) + assert(error.message.includes('arn:aws:lambda:us-east-1:*:function:test-function')) + } + }) + + it('should enhance permission errors for invoke', async function () { + const permissionError = Object.assign(new Error('Access denied'), { + code: 'AccessDeniedException', + time: new Date(), + statusCode: 403, + }) + mockLambdaClient.invoke.rejects(permissionError) + + try { + await enhancedClient.invoke('test-function', new TextEncoder().encode('{}')) + assert.fail('Expected error to be thrown') + } catch (error) { + assert(error instanceof ToolkitError) + assert(error.message.includes('Permission denied: Missing required permissions for lambda:invoke')) + assert(error.message.includes('lambda:InvokeFunction')) + assert(error.message.includes('arn:aws:lambda:us-east-1:*:function:test-function')) + } + }) + + it('should enhance permission errors for getLayerVersion', async function () { + const permissionError = Object.assign(new Error('Access denied'), { + code: 'AccessDeniedException', + time: new Date(), + statusCode: 403, + }) + mockLambdaClient.getLayerVersion.rejects(permissionError) + + try { + await enhancedClient.getLayerVersion('test-layer', 1) + assert.fail('Expected error to be thrown') + } catch (error) { + assert(error instanceof ToolkitError) + assert( + error.message.includes( + 'Permission denied: Missing required permissions for lambda:getLayerVersion' + ) + ) + assert(error.message.includes('lambda:GetLayerVersion')) + assert(error.message.includes('arn:aws:lambda:us-east-1:*:layer:test-layer:1')) + } + }) + + it('should enhance permission errors for updateFunctionCode', async function () { + const permissionError = Object.assign(new Error('Access denied'), { + code: 'AccessDeniedException', + time: new Date(), + statusCode: 403, + }) + mockLambdaClient.updateFunctionCode.rejects(permissionError) + + try { + await enhancedClient.updateFunctionCode('test-function', new Uint8Array()) + assert.fail('Expected error to be thrown') + } catch (error) { + assert(error instanceof ToolkitError) + assert( + error.message.includes( + 'Permission denied: Missing required permissions for lambda:updateFunctionCode' + ) + ) + assert(error.message.includes('lambda:UpdateFunctionCode')) + assert(error.message.includes('arn:aws:lambda:us-east-1:*:function:test-function')) + } + }) + + it('should return successful results when no errors occur', async function () { + const mockResponse = { Configuration: { FunctionName: 'test-function' } } + mockLambdaClient.getFunction.resolves(mockResponse) + + const result = await enhancedClient.getFunction('test-function') + assert.strictEqual(result, mockResponse) + }) + }) + + describe('EnhancedCloudFormationClient', function () { + let mockCfnClient: sinon.SinonStubbedInstance + let enhancedClient: EnhancedCloudFormationClient + + beforeEach(function () { + // Create a mock CloudFormation client with all required methods + mockCfnClient = sandbox.createStubInstance(CloudFormationClient) + enhancedClient = new EnhancedCloudFormationClient(mockCfnClient as any, 'us-east-1') + }) + + it('should enhance permission errors for describeStacks', async function () { + const permissionError = Object.assign(new Error('Access denied'), { + code: 'AccessDeniedException', + time: new Date(), + statusCode: 403, + }) + mockCfnClient.send.rejects(permissionError) + + try { + await enhancedClient.describeStacks({ StackName: 'test-stack' }) + assert.fail('Expected error to be thrown') + } catch (error) { + assert(error instanceof ToolkitError) + assert( + error.message.includes( + 'Permission denied: Missing required permissions for cloudformation:describeStacks' + ) + ) + assert(error.message.includes('cloudformation:DescribeStacks')) + assert(error.message.includes('arn:aws:cloudformation:us-east-1:*:stack/test-stack/*')) + assert(error.message.includes('To fix this issue:')) + assert(error.message.includes('Documentation:')) + } + }) + + it('should enhance permission errors for getTemplate', async function () { + const permissionError = Object.assign(new Error('Access denied'), { + code: 'AccessDeniedException', + time: new Date(), + statusCode: 403, + }) + mockCfnClient.send.rejects(permissionError) + + try { + await enhancedClient.getTemplate({ StackName: 'test-stack' }) + assert.fail('Expected error to be thrown') + } catch (error) { + assert(error instanceof ToolkitError) + assert( + error.message.includes( + 'Permission denied: Missing required permissions for cloudformation:getTemplate' + ) + ) + assert(error.message.includes('cloudformation:GetTemplate')) + assert(error.message.includes('arn:aws:cloudformation:us-east-1:*:stack/test-stack/*')) + } + }) + + it('should enhance permission errors for createChangeSet', async function () { + const permissionError = Object.assign(new Error('Access denied'), { + code: 'AccessDeniedException', + time: new Date(), + statusCode: 403, + }) + mockCfnClient.send.rejects(permissionError) + + try { + await enhancedClient.createChangeSet({ + StackName: 'test-stack', + ChangeSetName: 'test-changeset', + TemplateBody: '{}', + }) + assert.fail('Expected error to be thrown') + } catch (error) { + assert(error instanceof ToolkitError) + assert( + error.message.includes( + 'Permission denied: Missing required permissions for cloudformation:createChangeSet' + ) + ) + assert(error.message.includes('cloudformation:CreateChangeSet')) + assert(error.message.includes('arn:aws:cloudformation:us-east-1:*:stack/test-stack/*')) + } + }) + + it('should enhance permission errors for describeStackResource', async function () { + const permissionError = Object.assign(new Error('Access denied'), { + code: 'AccessDeniedException', + time: new Date(), + statusCode: 403, + }) + mockCfnClient.send.rejects(permissionError) + + try { + await enhancedClient.describeStackResource({ + StackName: 'test-stack', + LogicalResourceId: 'TestResource', + }) + assert.fail('Expected error to be thrown') + } catch (error) { + assert(error instanceof ToolkitError) + assert( + error.message.includes( + 'Permission denied: Missing required permissions for cloudformation:describeStackResource' + ) + ) + assert(error.message.includes('cloudformation:DescribeStackResource')) + assert(error.message.includes('arn:aws:cloudformation:us-east-1:*:stack/test-stack/*')) + } + }) + + it('should enhance permission errors for describeStackResources', async function () { + const permissionError = Object.assign(new Error('Access denied'), { + code: 'AccessDeniedException', + time: new Date(), + statusCode: 403, + }) + mockCfnClient.send.rejects(permissionError) + + try { + await enhancedClient.describeStackResources({ StackName: 'test-stack' }) + assert.fail('Expected error to be thrown') + } catch (error) { + assert(error instanceof ToolkitError) + assert( + error.message.includes( + 'Permission denied: Missing required permissions for cloudformation:describeStackResources' + ) + ) + assert(error.message.includes('cloudformation:DescribeStackResources')) + assert(error.message.includes('arn:aws:cloudformation:us-east-1:*:stack/test-stack/*')) + } + }) + + it('should pass through non-permission errors', async function () { + const nonPermissionError = new Error('Stack not found') + mockCfnClient.send.rejects(nonPermissionError) + + try { + await enhancedClient.describeStacks({ StackName: 'test-stack' }) + assert.fail('Expected error to be thrown') + } catch (error) { + assert.strictEqual(error, nonPermissionError) + } + }) + + it('should return successful results when no errors occur', async function () { + const mockResponse = { Stacks: [{ StackName: 'test-stack' }] } + mockCfnClient.send.resolves(mockResponse) + + const result = await enhancedClient.describeStacks({ StackName: 'test-stack' }) + assert.strictEqual(result, mockResponse) + }) + }) + + describe('Client Factory Functions', function () { + beforeEach(function () { + // Stub the global SDK client builder + sandbox.stub(globals.sdkClientBuilderV3, 'createAwsService').resolves({} as any) + }) + + it('should return EnhancedLambdaClient from getLambdaClient', function () { + const client = getLambdaClient('us-east-1') + assert(client instanceof EnhancedLambdaClient) + }) + + it('should return EnhancedCloudFormationClient from getCFNClient', async function () { + const client = await getCFNClient('us-east-1') + assert(client instanceof EnhancedCloudFormationClient) + }) + }) + + describe('Error Message Content', function () { + let mockLambdaClient: sinon.SinonStubbedInstance + let enhancedClient: EnhancedLambdaClient + + beforeEach(function () { + mockLambdaClient = sandbox.createStubInstance(DefaultLambdaClient) + // Add missing properties that EnhancedLambdaClient expects + Object.defineProperty(mockLambdaClient, 'defaultTimeoutInMs', { + value: 5 * 60 * 1000, + configurable: true, + }) + Object.defineProperty(mockLambdaClient, 'createSdkClient', { + value: sandbox.stub().resolves({}), + configurable: true, + }) + enhancedClient = new EnhancedLambdaClient(mockLambdaClient as any, 'us-west-2') + }) + + it('should include all required elements in enhanced error message', async function () { + const permissionError = Object.assign(new Error('Access denied'), { + code: 'AccessDeniedException', + time: new Date(), + statusCode: 403, + }) + mockLambdaClient.getFunction.rejects(permissionError) + + try { + await enhancedClient.getFunction('my-test-function') + assert.fail('Expected error to be thrown') + } catch (error) { + assert(error instanceof ToolkitError) + + // Check that the error message contains all expected elements + const message = error.message + + // Main error description + assert(message.includes('Permission denied: Missing required permissions for lambda:getFunction')) + + // Required permissions section + assert(message.includes('Required permissions:')) + assert(message.includes('- lambda:GetFunction')) + + // Resource ARN + assert(message.includes('Resource: arn:aws:lambda:us-west-2:*:function:my-test-function')) + + // Instructions + assert(message.includes('To fix this issue:')) + assert(message.includes('1. Contact your AWS administrator')) + assert(message.includes('2. Add these permissions to your IAM user/role policy')) + assert(message.includes('3. If using IAM roles, ensure the role has these permissions attached')) + + // Documentation link + assert( + message.includes( + 'Documentation: https://docs.aws.amazon.com/lambda/latest/api/API_GetFunction.html' + ) + ) + + // Check error details + assert.strictEqual(error.code, 'InsufficientPermissions') + assert(error.details) + assert.strictEqual(error.details.service, 'lambda') + assert.strictEqual(error.details.action, 'getFunction') + assert.deepStrictEqual(error.details.requiredPermissions, ['lambda:GetFunction']) + assert.strictEqual( + error.details.resourceArn, + 'arn:aws:lambda:us-west-2:*:function:my-test-function' + ) + } + }) + + it('should handle errors without resource ARN', async function () { + const permissionError = Object.assign(new Error('Access denied'), { + code: 'AccessDeniedException', + time: new Date(), + statusCode: 403, + }) + + // Create a mock async generator that throws the error + const mockAsyncGenerator = async function* (): AsyncIterableIterator { + throw permissionError + yield // This line will never be reached but satisfies ESLint require-yield rule + } + mockLambdaClient.listFunctions.returns(mockAsyncGenerator()) + + try { + const iterator = enhancedClient.listFunctions() + await iterator.next() + assert.fail('Expected error to be thrown') + } catch (error) { + assert(error instanceof ToolkitError) + + const message = error.message + assert(message.includes('Permission denied: Missing required permissions for lambda:listFunctions')) + assert(message.includes('- lambda:ListFunctions')) + // Should not include Resource line for operations without specific resources + assert(!message.includes('Resource: arn:')) + } + }) + }) + }) }) 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/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/credentialMapping.test.ts b/packages/core/src/test/awsService/sagemaker/credentialMapping.test.ts new file mode 100644 index 00000000000..3134f11e5e0 --- /dev/null +++ b/packages/core/src/test/awsService/sagemaker/credentialMapping.test.ts @@ -0,0 +1,448 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as sinon from 'sinon' +import * as assert from 'assert' +import { + persistLocalCredentials, + persistSSMConnection, + persistSmusProjectCreds, + loadMappings, + saveMappings, + setSpaceIamProfile, + setSpaceSsoProfile, + setSmusSpaceSsoProfile, + 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', () => { + const appArn = 'arn:aws:sagemaker:us-west-2:123456789012:space/d-f0lwireyzpjp/test-space' + + let sandbox: sinon.SinonSandbox + + beforeEach(() => { + sandbox = sinon.createSandbox() + }) + + afterEach(() => { + sandbox.restore() + }) + + it('writes IAM profile to mappings', async () => { + sandbox.stub(Auth.instance, 'getCurrentProfileId').returns('profile:my-iam-profile') + sandbox.stub(fs, 'existsFile').resolves(false) // simulate no existing mapping file + const writeStub = sandbox.stub(fs, 'writeFile').resolves() + + await persistLocalCredentials(appArn) + + 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: 'iam', + profileName: 'profile:my-iam-profile', + }) + }) + + it('writes SSO credentials to mappings', async () => { + sandbox.stub(Auth.instance, 'getCurrentProfileId').returns('sso:my-sso-profile') + sandbox.stub(globals.loginManager.store, 'credentialsCache').value({ + 'sso:my-sso-profile': { + credentials: { + accessKeyId: 'AKIA123', + secretAccessKey: 'SECRET', + sessionToken: 'TOKEN', + }, + }, + }) + sandbox.stub(fs, 'existsFile').resolves(false) + const writeStub = sandbox.stub(fs, 'writeFile').resolves() + + await persistLocalCredentials(appArn) + + 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', + accessKey: 'AKIA123', + secret: 'SECRET', + token: 'TOKEN', + }) + }) + + it('throws if no current profile ID is available', async () => { + sandbox.stub(Auth.instance, 'getCurrentProfileId').returns(undefined) + + await assert.rejects(() => persistLocalCredentials(appArn), { + message: 'No current profile ID available for saving space credentials.', + }) + }) + }) + + describe('persistSSMConnection', () => { + const appArn = 'arn:aws:sagemaker:us-west-2:123456789012:space/d-f0lwireyzpjp/test-space' + const domain = 'd-f0lwireyzpjp' + let sandbox: sinon.SinonSandbox + + beforeEach(() => { + sandbox = sinon.createSandbox() + }) + + afterEach(() => { + sandbox.restore() + }) + + function assertRefreshUrlMatches(writtenUrl: string, expectedSubdomain: string) { + assert.ok( + writtenUrl.startsWith(`https://studio-${domain}.${expectedSubdomain}`), + `Expected refresh URL to start with https://studio-${domain}.${expectedSubdomain}, got ${writtenUrl}` + ) + } + + it('uses default (studio) endpoint if no custom endpoint is set', async () => { + sandbox.stub(DevSettings.instance, 'get').returns({}) + sandbox.stub(fs, 'existsFile').resolves(false) + const writeStub = sandbox.stub(fs, 'writeFile').resolves() + + // Stub the AWS API call + const mockDescribeSpace = sandbox.stub().resolves({ + SpaceSettings: { + AppType: 'JupyterLab', + }, + }) + sandbox.stub(require('../../../shared/clients/sagemaker'), 'SagemakerClient').returns({ + describeSpace: mockDescribeSpace, + }) + + await persistSSMConnection(appArn, domain) + + const raw = writeStub.firstCall.args[1] + const data = JSON.parse(typeof raw === 'string' ? raw : raw.toString()) + + assertRefreshUrlMatches(data.deepLink?.[appArn]?.refreshUrl, 'studio.us-west-2.sagemaker.aws') + assert.deepStrictEqual(data.deepLink?.[appArn]?.requests['initial-connection'], { + sessionId: '-', + url: '-', + token: '-', + status: 'fresh', + }) + }) + + it('uses devo subdomain for beta endpoint', async () => { + sandbox.stub(DevSettings.instance, 'get').returns({ sagemaker: 'https://beta.whatever' }) + sandbox.stub(fs, 'existsFile').resolves(false) + const writeStub = sandbox.stub(fs, 'writeFile').resolves() + + // Stub the AWS API call + const mockDescribeSpace = sandbox.stub().resolves({ + SpaceSettings: { + AppType: 'JupyterLab', + }, + }) + sandbox.stub(require('../../../shared/clients/sagemaker'), 'SagemakerClient').returns({ + describeSpace: mockDescribeSpace, + }) + + await persistSSMConnection(appArn, domain, 'sess', 'wss://ws', 'token') + + const raw = writeStub.firstCall.args[1] + const data = JSON.parse(typeof raw === 'string' ? raw : raw.toString()) + + assertRefreshUrlMatches(data.deepLink?.[appArn]?.refreshUrl, 'devo.studio.us-west-2.asfiovnxocqpcry.com') + assert.deepStrictEqual(data.deepLink?.[appArn]?.requests['initial-connection'], { + sessionId: 'sess', + url: 'wss://ws', + token: 'token', + status: 'fresh', + }) + }) + + it('uses loadtest subdomain for gamma endpoint', async () => { + sandbox.stub(DevSettings.instance, 'get').returns({ sagemaker: 'https://gamma.example' }) + sandbox.stub(fs, 'existsFile').resolves(false) + const writeStub = sandbox.stub(fs, 'writeFile').resolves() + + // Stub the AWS API call + const mockDescribeSpace = sandbox.stub().resolves({ + SpaceSettings: { + AppType: 'JupyterLab', + }, + }) + sandbox.stub(require('../../../shared/clients/sagemaker'), 'SagemakerClient').returns({ + describeSpace: mockDescribeSpace, + }) + + await persistSSMConnection(appArn, domain) + + const raw = writeStub.firstCall.args[1] + const data = JSON.parse(typeof raw === 'string' ? raw : raw.toString()) + + assertRefreshUrlMatches( + data.deepLink?.[appArn]?.refreshUrl, + 'loadtest.studio.us-west-2.asfiovnxocqpcry.com' + ) + }) + + // TODO: Skipped due to hardcoded appSubDomain. Currently 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. Additionally, appType will be determined by + // the deeplink URI rather than the describeSpace call from the toolkit. + it.skip('throws error when app type is unsupported', async () => { + sandbox.stub(DevSettings.instance, 'get').returns({}) + sandbox.stub(fs, 'existsFile').resolves(false) + + // Stub the AWS API call to return an unsupported app type + const mockDescribeSpace = sandbox.stub().resolves({ + SpaceSettings: { + AppType: 'UnsupportedApp', + }, + }) + sandbox.stub(require('../../../shared/clients/sagemaker'), 'SagemakerClient').returns({ + describeSpace: mockDescribeSpace, + }) + + await assert.rejects(() => persistSSMConnection(appArn, domain), { + name: 'Error', + message: + 'Unsupported or missing app type for space. Expected JupyterLab or CodeEditor, got: UnsupportedApp', + }) + }) + }) + + 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(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('setSmusSpaceSsoProfile', () => { + 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 setSmusSpaceSsoProfile('test-space', 'project-id') + + 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 new file mode 100644 index 00000000000..3db189f8390 --- /dev/null +++ b/packages/core/src/test/awsService/sagemaker/detached-server/credentials.test.ts @@ -0,0 +1,152 @@ +/*! + * 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 utils from '../../../../awsService/sagemaker/detached-server/utils' +import { resolveCredentialsFor } from '../../../../awsService/sagemaker/detached-server/credentials' + +const connectionId = 'arn:aws:sagemaker:region:acct:space/name' + +describe('resolveCredentialsFor', () => { + afterEach(() => sinon.restore()) + + it('throws if no profile is found', async () => { + sinon.stub(utils, 'readMapping').resolves({ localCredential: {} }) + + await assert.rejects(() => resolveCredentialsFor(connectionId), { + message: `No profile found for "${connectionId}"`, + }) + }) + + it('throws if IAM profile name is malformed', async () => { + sinon.stub(utils, 'readMapping').resolves({ + localCredential: { + [connectionId]: { + type: 'iam', + profileName: 'dev-profile', // no colon + }, + }, + }) + + await assert.rejects(() => resolveCredentialsFor(connectionId), { + message: `Invalid IAM profile name for "${connectionId}"`, + }) + }) + + it('resolves SSO credentials correctly', async () => { + sinon.stub(utils, 'readMapping').resolves({ + localCredential: { + [connectionId]: { + type: 'sso', + accessKey: 'key', + secret: 'sec', + token: 'tok', + }, + }, + }) + + const creds = await resolveCredentialsFor(connectionId) + assert.deepStrictEqual(creds, { + accessKeyId: 'key', + secretAccessKey: 'sec', + sessionToken: 'tok', + }) + }) + + it('throws if SSO credentials are incomplete', async () => { + sinon.stub(utils, 'readMapping').resolves({ + localCredential: { + [connectionId]: { + type: 'sso', + accessKey: 'key', + secret: 'sec', + token: '', // token is required but intentionally left empty for this test + }, + }, + }) + + await assert.rejects(() => resolveCredentialsFor(connectionId), { + message: `Missing SSO credentials for "${connectionId}"`, + }) + }) + + 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: { + [connectionId]: { + type: 'unknown', + } as any, + }, + }) + + await assert.rejects(() => resolveCredentialsFor(connectionId), { + message: /Unsupported profile type/, // don't hard-code full value since object might be serialized + }) + }) +}) diff --git a/packages/core/src/test/awsService/sagemaker/detached-server/routes/getSession.test.ts b/packages/core/src/test/awsService/sagemaker/detached-server/routes/getSession.test.ts new file mode 100644 index 00000000000..5b3f176a29f --- /dev/null +++ b/packages/core/src/test/awsService/sagemaker/detached-server/routes/getSession.test.ts @@ -0,0 +1,102 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as http from 'http' +import * as sinon from 'sinon' +import assert from 'assert' +import { handleGetSession } from '../../../../../awsService/sagemaker/detached-server/routes/getSession' +import * as credentials from '../../../../../awsService/sagemaker/detached-server/credentials' +import * as utils from '../../../../../awsService/sagemaker/detached-server/utils' +import * as errorPage from '../../../../../awsService/sagemaker/detached-server/errorPage' + +describe('handleGetSession', () => { + let req: Partial + let res: Partial + let resWriteHead: sinon.SinonSpy + let resEnd: sinon.SinonSpy + + beforeEach(() => { + resWriteHead = sinon.spy() + resEnd = sinon.spy() + + res = { + writeHead: resWriteHead, + end: resEnd, + } + sinon.stub(errorPage, 'openErrorPage') + }) + + it('responds with 400 if connection_identifier is missing', async () => { + req = { url: '/session' } + await handleGetSession(req as http.IncomingMessage, res as http.ServerResponse) + + assert(resWriteHead.calledWith(400)) + assert(resEnd.calledWithMatch(/Missing required query parameter/)) + }) + + it('responds with 500 if resolveCredentialsFor throws', async () => { + req = { url: '/session?connection_identifier=arn:aws:sagemaker:region:acc:space/domain/name' } + sinon.stub(credentials, 'resolveCredentialsFor').rejects(new Error('creds error')) + sinon.stub(utils, 'parseArn').returns({ + region: 'us-west-2', + accountId: '123456789012', + spaceName: 'space-name', + }) + + await handleGetSession(req as http.IncomingMessage, res as http.ServerResponse) + + assert(resWriteHead.calledWith(500)) + assert(resEnd.calledWith('creds error')) + }) + + it('responds with 500 if startSagemakerSession throws', async () => { + req = { url: '/session?connection_identifier=arn:aws:sagemaker:region:acc:space/domain/name' } + sinon.stub(credentials, 'resolveCredentialsFor').resolves({}) + sinon.stub(utils, 'parseArn').returns({ + region: 'us-west-2', + accountId: '123456789012', + spaceName: 'space-name', + }) + sinon.stub(utils, 'startSagemakerSession').rejects(new Error('session error')) + + await handleGetSession(req as http.IncomingMessage, res as http.ServerResponse) + + assert(resWriteHead.calledWith(500)) + assert(resEnd.calledWith('Failed to start SageMaker session')) + }) + + it('responds with 200 and session data on success', async () => { + req = { url: '/session?connection_identifier=arn:aws:sagemaker:region:acc:space/domain/name' } + sinon.stub(credentials, 'resolveCredentialsFor').resolves({}) + sinon.stub(utils, 'parseArn').returns({ + region: 'us-west-2', + accountId: '123456789012', + spaceName: 'space-name', + }) + sinon.stub(utils, 'startSagemakerSession').resolves({ + SessionId: 'abc123', + StreamUrl: 'https://stream', + TokenValue: 'token123', + $metadata: { httpStatusCode: 200 }, + }) + + await handleGetSession(req as http.IncomingMessage, res as http.ServerResponse) + + assert(resWriteHead.calledWith(200)) + assert( + resEnd.calledWithMatch( + JSON.stringify({ + SessionId: 'abc123', + StreamUrl: 'https://stream', + TokenValue: 'token123', + }) + ) + ) + }) + + afterEach(() => { + sinon.restore() + }) +}) 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 new file mode 100644 index 00000000000..8d3ab8563ee --- /dev/null +++ b/packages/core/src/test/awsService/sagemaker/detached-server/routes/getSessionAsync.test.ts @@ -0,0 +1,99 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as http from 'http' +import * as sinon from 'sinon' +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' + +describe('handleGetSessionAsync', () => { + let req: Partial + let res: Partial + let resWriteHead: sinon.SinonSpy + let resEnd: sinon.SinonSpy + let storeStub: sinon.SinonStubbedInstance + + beforeEach(() => { + resWriteHead = sinon.spy() + resEnd = sinon.spy() + res = { writeHead: resWriteHead, end: resEnd } + + storeStub = sinon.createStubInstance(SessionStore) + sinon.stub(SessionStore.prototype, 'getFreshEntry').callsFake(storeStub.getFreshEntry) + sinon.stub(SessionStore.prototype, 'getStatus').callsFake(storeStub.getStatus) + sinon.stub(SessionStore.prototype, 'getRefreshUrl').callsFake(storeStub.getRefreshUrl) + sinon.stub(SessionStore.prototype, 'markPending').callsFake(storeStub.markPending) + }) + + it('responds with 400 if required query parameters are missing', async () => { + req = { url: '/session_async?connection_identifier=abc' } // missing request_id + await handleGetSessionAsync(req as http.IncomingMessage, res as http.ServerResponse) + + assert(resWriteHead.calledWith(400)) + assert(resEnd.calledWithMatch(/Missing required query parameters/)) + }) + + it('responds with 200 and session data if freshEntry exists', async () => { + req = { url: '/session_async?connection_identifier=abc&request_id=req123' } + storeStub.getFreshEntry.returns(Promise.resolve({ sessionId: 'sid', token: 'tok', url: 'wss://test' })) + + await handleGetSessionAsync(req as http.IncomingMessage, res as http.ServerResponse) + + assert(resWriteHead.calledWith(200)) + const actualJson = JSON.parse(resEnd.firstCall.args[0]) + assert.deepStrictEqual(actualJson, { + SessionId: 'sid', + TokenValue: 'tok', + StreamUrl: 'wss://test', + }) + }) + + it('responds with 204 if session is pending', async () => { + req = { url: '/session_async?connection_identifier=abc&request_id=req123' } + storeStub.getFreshEntry.returns(Promise.resolve(undefined)) + storeStub.getStatus.returns(Promise.resolve('pending')) + + await handleGetSessionAsync(req as http.IncomingMessage, res as http.ServerResponse) + + assert(resWriteHead.calledWith(204)) + assert(resEnd.calledOnce) + }) + + it('responds with 202 if status is not-started and opens browser', 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()) + + 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) + + assert(resWriteHead.calledWith(202)) + assert(resEnd.calledWithMatch(/Session is not ready yet/)) + assert(storeStub.markPending.calledWith('abc', 'req123')) + }) + + it('responds with 500 if unexpected error occurs', async () => { + req = { url: '/session_async?connection_identifier=abc&request_id=req123' } + storeStub.getFreshEntry.throws(new Error('fail')) + + await handleGetSessionAsync(req as http.IncomingMessage, res as http.ServerResponse) + + assert(resWriteHead.calledWith(500)) + assert(resEnd.calledWith('Unexpected error')) + }) + + afterEach(() => { + sinon.restore() + }) +}) diff --git a/packages/core/src/test/awsService/sagemaker/detached-server/routes/refreshToken.test.ts b/packages/core/src/test/awsService/sagemaker/detached-server/routes/refreshToken.test.ts new file mode 100644 index 00000000000..2fe6b3c648d --- /dev/null +++ b/packages/core/src/test/awsService/sagemaker/detached-server/routes/refreshToken.test.ts @@ -0,0 +1,74 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as http from 'http' +import * as sinon from 'sinon' +import assert from 'assert' +import { SessionStore } from '../../../../../awsService/sagemaker/detached-server/sessionStore' +import { handleRefreshToken } from '../../../../../awsService/sagemaker/detached-server/routes/refreshToken' + +describe('handleRefreshToken', () => { + let req: Partial + let res: Partial + let resWriteHead: sinon.SinonSpy + let resEnd: sinon.SinonSpy + let storeStub: sinon.SinonStubbedInstance + + beforeEach(() => { + resWriteHead = sinon.spy() + resEnd = sinon.spy() + + res = { + writeHead: resWriteHead, + end: resEnd, + } + + storeStub = sinon.createStubInstance(SessionStore) + sinon.stub(SessionStore.prototype, 'setSession').callsFake(storeStub.setSession) + }) + + it('responds with 400 if any required query parameter is missing', async () => { + req = { url: '/refresh?connection_identifier=abc&request_id=req123' } // missing others + + await handleRefreshToken(req as http.IncomingMessage, res as http.ServerResponse) + + assert(resWriteHead.calledWith(400)) + assert(resEnd.calledWithMatch(/Missing required parameters/)) + }) + + it('responds with 500 if setSession throws', async () => { + req = { + url: '/refresh?connection_identifier=abc&request_id=req123&ws_url=wss://abc&token=tok123&session=sess123', + } + storeStub.setSession.throws(new Error('store error')) + + await handleRefreshToken(req as http.IncomingMessage, res as http.ServerResponse) + + assert(resWriteHead.calledWith(500)) + assert(resEnd.calledWith('Failed to save session token')) + }) + + it('responds with 200 if session is saved successfully', async () => { + req = { + url: '/refresh?connection_identifier=abc&request_id=req123&ws_url=wss://abc&token=tok123&session=sess123', + } + + await handleRefreshToken(req as http.IncomingMessage, res as http.ServerResponse) + + assert(resWriteHead.calledWith(200)) + assert(resEnd.calledWith('Session token refreshed successfully')) + assert( + storeStub.setSession.calledWith('abc', 'req123', { + sessionId: 'sess123', + token: 'tok123', + url: 'wss://abc', + }) + ) + }) + + 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 new file mode 100644 index 00000000000..2a7828a4951 --- /dev/null +++ b/packages/core/src/test/awsService/sagemaker/detached-server/sessionStore.test.ts @@ -0,0 +1,145 @@ +/*! + * 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 * as utils from '../../../../awsService/sagemaker/detached-server/utils' +import { SessionStore } from '../../../../awsService/sagemaker/detached-server/sessionStore' +import { SsmConnectionInfo } from '../../../../awsService/sagemaker/types' + +describe('SessionStore', () => { + let readMappingStub: sinon.SinonStub + let writeMappingStub: sinon.SinonStub + const connectionId = 'abc' + const requestId = 'req123' + + const baseMapping = { + deepLink: { + [connectionId]: { + refreshUrl: 'https://refresh.url', + requests: { + [requestId]: { sessionId: 's1', token: 't1', url: 'u1', status: 'fresh' }, + 'initial-connection': { sessionId: 's0', token: 't0', url: 'u0', status: 'fresh' }, + }, + }, + }, + } + + beforeEach(() => { + readMappingStub = sinon.stub(utils, 'readMapping').returns(JSON.parse(JSON.stringify(baseMapping))) + writeMappingStub = sinon.stub(utils, 'writeMapping') + }) + + afterEach(() => sinon.restore()) + + it('gets refreshUrl', 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: {} }) + + await assert.rejects(() => store.getRefreshUrl('missing'), /No mapping found/) + }) + + it('returns fresh entry and marks consumed', async () => { + const store = new SessionStore() + const result = await store.getFreshEntry(connectionId, requestId) + assert.deepStrictEqual(result, { + sessionId: 's0', + token: 't0', + url: 'u0', + status: 'consumed', + }) + assert(writeMappingStub.calledOnce) + }) + + it('returns async fresh entry and deletes it', async () => { + const store = new SessionStore() + // Disable initial-connection freshness + readMappingStub.returns({ + deepLink: { + [connectionId]: { + refreshUrl: 'url', + requests: { + 'initial-connection': { status: 'consumed' }, + [requestId]: { sessionId: 'a', token: 'b', url: 'c', status: 'fresh' }, + }, + }, + }, + }) + const result = await store.getFreshEntry(connectionId, requestId) + assert.ok(result, 'Expected result to be defined') + assert.strictEqual(result.sessionId, 'a') + assert(writeMappingStub.calledOnce) + + // Verify the entry was deleted from the mapping + const updated = writeMappingStub.firstCall.args[0] + assert.strictEqual(updated.deepLink[connectionId].requests[requestId], undefined) + }) + + it('returns undefined if no fresh entries exist', async () => { + const store = new SessionStore() + readMappingStub.returns({ + deepLink: { + [connectionId]: { + refreshUrl: 'url', + requests: { + 'initial-connection': { status: 'consumed' }, + [requestId]: { status: 'pending' }, + }, + }, + }, + }) + const result = await store.getFreshEntry(connectionId, requestId) + assert.strictEqual(result, undefined) + }) + + it('gets status of known entry', async () => { + const store = new SessionStore() + const result = await store.getStatus(connectionId, requestId) + assert.strictEqual(result, 'fresh') + }) + + it('returns not-started if request not found', async () => { + const store = new SessionStore() + const result = await store.getStatus(connectionId, 'unknown') + assert.strictEqual(result, 'not-started') + }) + + it('marks entry as consumed', async () => { + const store = new SessionStore() + await store.markConsumed(connectionId, requestId) + const updated = writeMappingStub.firstCall.args[0] + assert.strictEqual(updated.deepLink[connectionId].requests[requestId].status, 'consumed') + }) + + it('marks request as pending', async () => { + const store = new SessionStore() + await store.markPending(connectionId, 'newReq') + const updated = writeMappingStub.firstCall.args[0] + assert.strictEqual(updated.deepLink[connectionId].requests['newReq'].status, 'pending') + }) + + it('sets session entry with default fresh status', async () => { + const store = new SessionStore() + const info: SsmConnectionInfo = { + sessionId: 's99', + token: 't99', + url: 'u99', + } + await store.setSession(connectionId, 'r99', info) + const written = writeMappingStub.firstCall.args[0] + assert.deepStrictEqual(written.deepLink[connectionId].requests['r99'], { + sessionId: 's99', + token: 't99', + url: 'u99', + status: 'fresh', + }) + }) +}) diff --git a/packages/core/src/test/awsService/sagemaker/detached-server/utils.test.ts b/packages/core/src/test/awsService/sagemaker/detached-server/utils.test.ts new file mode 100644 index 00000000000..bc8d0a8867b --- /dev/null +++ b/packages/core/src/test/awsService/sagemaker/detached-server/utils.test.ts @@ -0,0 +1,112 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable no-restricted-imports */ +import * as assert from 'assert' +import { parseArn, writeMapping, readMapping } from '../../../../awsService/sagemaker/detached-server/utils' +import { promises as fs } from 'fs' +import * as path from 'path' +import * as os from 'os' +import { SpaceMappings } from '../../../../awsService/sagemaker/types' + +describe('parseArn', () => { + it('parses a standard SageMaker ARN with forward slash', () => { + const arn = 'arn:aws:sagemaker:us-west-2:123456789012:space/domain-name/my-space-name' + const result = parseArn(arn) + assert.deepStrictEqual(result, { + region: 'us-west-2', + accountId: '123456789012', + spaceName: 'my-space-name', + }) + }) + + it('parses an ARN prefixed with sagemaker-user@', () => { + const arn = 'sagemaker-user@arn:aws:sagemaker:ap-southeast-1:123456789012:space/foo/my-space-name' + const result = parseArn(arn) + assert.deepStrictEqual(result, { + region: 'ap-southeast-1', + accountId: '123456789012', + spaceName: 'my-space-name', + }) + }) + + it('throws on malformed ARN', () => { + const invalidArn = 'arn:aws:invalid:format' + assert.throws(() => parseArn(invalidArn), /Invalid SageMaker ARN format/) + }) + + it('throws when missing region/account', () => { + const invalidArn = 'arn:aws:sagemaker:::space/xyz' + assert.throws(() => parseArn(invalidArn), /Invalid SageMaker ARN format/) + }) +}) + +describe('writeMapping', () => { + let testDir: string + + beforeEach(async () => { + testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'sagemaker-test-')) + }) + + afterEach(async () => { + await fs.rmdir(testDir, { recursive: true }) + }) + + it('handles concurrent writes without race conditions', async () => { + const mapping1: SpaceMappings = { + localCredential: { + 'space-1': { type: 'iam', profileName: 'profile1' }, + }, + } + const mapping2: SpaceMappings = { + localCredential: { + 'space-2': { type: 'iam', profileName: 'profile2' }, + }, + } + const mapping3: SpaceMappings = { + deepLink: { + 'space-3': { + requests: { + req1: { + sessionId: 'session-456', + url: 'wss://example3.com', + token: 'token-456', + }, + }, + refreshUrl: 'https://example3.com/refresh', + }, + }, + } + + const writePromises = [writeMapping(mapping1), writeMapping(mapping2), writeMapping(mapping3)] + + await Promise.all(writePromises) + + const finalContent = await readMapping() + const possibleResults = [mapping1, mapping2, mapping3] + const isValidResult = possibleResults.some( + (expected) => JSON.stringify(finalContent) === JSON.stringify(expected) + ) + assert.strictEqual(isValidResult, true, 'Final content should match one of the written mappings') + }) + + it('queues multiple writes and processes them sequentially', async () => { + const mappings = Array.from({ length: 5 }, (_, i) => ({ + localCredential: { + [`space-${i}`]: { type: 'iam' as const, profileName: `profile-${i}` }, + }, + })) + + const writePromises = mappings.map((mapping) => writeMapping(mapping)) + + await Promise.all(writePromises) + + const finalContent = await readMapping() + assert.strictEqual(typeof finalContent, 'object', 'Final content should be a valid object') + + const isValidResult = mappings.some((mapping) => JSON.stringify(finalContent) === JSON.stringify(mapping)) + assert.strictEqual(isValidResult, true, 'Final content should match one of the written mappings') + }) +}) diff --git a/packages/core/src/test/awsService/sagemaker/explorer/sagemakerParentNode.test.ts b/packages/core/src/test/awsService/sagemaker/explorer/sagemakerParentNode.test.ts new file mode 100644 index 00000000000..b7cf98496fc --- /dev/null +++ b/packages/core/src/test/awsService/sagemaker/explorer/sagemakerParentNode.test.ts @@ -0,0 +1,344 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as sinon from 'sinon' +import * as vscode from 'vscode' +import { DescribeDomainResponse } from '@amzn/sagemaker-client' +import { GetCallerIdentityResponse } from 'aws-sdk/clients/sts' +import { SagemakerClient, SagemakerSpaceApp } from '../../../../shared/clients/sagemaker' +import { SagemakerConstants } from '../../../../awsService/sagemaker/explorer/constants' +import { + SagemakerParentNode, + SelectedDomainUsers, + SelectedDomainUsersByRegion, +} from '../../../../awsService/sagemaker/explorer/sagemakerParentNode' +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 + let client: SagemakerClient + let fetchSpaceAppsAndDomainsStub: sinon.SinonStub< + [domainId?: string | undefined, filterSmusDomains?: boolean | undefined], + Promise<[Map, Map]> + > + let getCallerIdentityStub: sinon.SinonStub<[], Promise> + const testRegion = 'testRegion' + const domainsMap: Map = new Map([ + ['domain1', { DomainId: 'domain1', DomainName: 'domainName1' }], + ['domain2', { DomainId: 'domain2', DomainName: 'domainName2' }], + ]) + const spaceAppsMap: Map = new Map([ + [ + 'domain1__name1', + { + SpaceName: 'name1', + DomainId: 'domain1', + OwnershipSettingsSummary: { OwnerUserProfileName: 'user1-abcd' }, + Status: 'InService', + DomainSpaceKey: 'domain1__name1', + }, + ], + [ + 'domain2__name2', + { + SpaceName: 'name2', + DomainId: 'domain2', + OwnershipSettingsSummary: { OwnerUserProfileName: 'user2-efgh' }, + Status: 'InService', + DomainSpaceKey: 'domain2__name2', + }, + ], + ]) + const spaceAppsMapPending: Map = new Map([ + [ + 'domain1__name3', + { + SpaceName: 'name3', + DomainId: 'domain1', + OwnershipSettingsSummary: { OwnerUserProfileName: 'user1-abcd' }, + Status: 'InService', + DomainSpaceKey: 'domain1__name3', + App: { + Status: 'InService', + }, + }, + ], + [ + 'domain2__name4', + { + SpaceName: 'name4', + DomainId: 'domain2', + OwnershipSettingsSummary: { OwnerUserProfileName: 'user2-efgh' }, + Status: 'InService', + DomainSpaceKey: 'domain2__name4', + App: { + Status: 'Pending', + }, + }, + ], + ]) + const iamUser = { + UserId: 'test-userId', + Account: '123456789012', + Arn: 'arn:aws:iam::123456789012:user/user2', + } + const assumedRoleUser = { + UserId: 'test-userId', + Account: '123456789012', + Arn: 'arn:aws:sts::123456789012:assumed-role/UserRole/user2', + } + const ssoUser = { + UserId: 'test-userId', + Account: '123456789012', + Arn: 'arn:aws:sts::123456789012:assumed-role/AWSReservedSSO_MyPermissionSet_abcd1234/user2', + } + const getConfigTrue = { + get: () => true, + } + const getConfigFalse = { + get: () => false, + } + + before(function () { + client = new SagemakerClient(testRegion) + }) + + beforeEach(function () { + fetchSpaceAppsAndDomainsStub = sinon.stub(SagemakerClient.prototype, 'fetchSpaceAppsAndDomains') + getCallerIdentityStub = sinon.stub(DefaultStsClient.prototype, 'getCallerIdentity') + testNode = new SagemakerParentNode(testRegion, client) + }) + + afterEach(function () { + fetchSpaceAppsAndDomainsStub.restore() + getCallerIdentityStub.restore() + testNode.pollingSet.clear() + testNode.pollingSet.clearTimer() + sinon.restore() + }) + + it('returns placeholder node if no children are present', async function () { + fetchSpaceAppsAndDomainsStub.returns( + Promise.resolve([new Map(), new Map()]) + ) + getCallerIdentityStub.returns(Promise.resolve(iamUser)) + + const childNodes = await testNode.getChildren() + assertNodeListOnlyHasPlaceholderNode(childNodes) + }) + + it('has child nodes', async function () { + fetchSpaceAppsAndDomainsStub.returns(Promise.resolve([spaceAppsMap, domainsMap])) + getCallerIdentityStub.returns(Promise.resolve(iamUser)) + sinon + .stub(vscode.workspace, 'getConfiguration') + .returns(getConfigFalse as unknown as vscode.WorkspaceConfiguration) + + const childNodes = await testNode.getChildren() + assert.strictEqual(childNodes.length, spaceAppsMap.size, 'Unexpected child count') + assert.strictEqual(childNodes[0].label, 'name1 (Stopped)', 'Unexpected node label') + assert.strictEqual(childNodes[1].label, 'name2 (Stopped)', 'Unexpected node label') + }) + + it('adds pending nodes to polling nodes set', async function () { + fetchSpaceAppsAndDomainsStub.returns(Promise.resolve([spaceAppsMapPending, domainsMap])) + getCallerIdentityStub.returns(Promise.resolve(iamUser)) + + await testNode.updateChildren() + assert.strictEqual(testNode.pollingSet.size, 1) + fetchSpaceAppsAndDomainsStub.restore() + }) + + it('filters spaces owned by user profiles that match the IAM user', async function () { + fetchSpaceAppsAndDomainsStub.returns(Promise.resolve([spaceAppsMap, domainsMap])) + getCallerIdentityStub.returns(Promise.resolve(iamUser)) + sinon + .stub(vscode.workspace, 'getConfiguration') + .returns(getConfigTrue as unknown as vscode.WorkspaceConfiguration) + + const childNodes = await testNode.getChildren() + assert.strictEqual(childNodes.length, 1, 'Unexpected child count') + assert.strictEqual(childNodes[0].label, 'name2 (Stopped)', 'Unexpected node label') + }) + + it('filters spaces owned by user profiles that match the IAM assumed-role session name', async function () { + fetchSpaceAppsAndDomainsStub.returns(Promise.resolve([spaceAppsMap, domainsMap])) + getCallerIdentityStub.returns(Promise.resolve(assumedRoleUser)) + sinon + .stub(vscode.workspace, 'getConfiguration') + .returns(getConfigTrue as unknown as vscode.WorkspaceConfiguration) + + const childNodes = await testNode.getChildren() + assert.strictEqual(childNodes.length, 1, 'Unexpected child count') + assert.strictEqual(childNodes[0].label, 'name2 (Stopped)', 'Unexpected node label') + }) + + it('filters spaces owned by user profiles that match the Identity Center user', async function () { + fetchSpaceAppsAndDomainsStub.returns(Promise.resolve([spaceAppsMap, domainsMap])) + getCallerIdentityStub.returns(Promise.resolve(ssoUser)) + sinon + .stub(vscode.workspace, 'getConfiguration') + .returns(getConfigFalse as unknown as vscode.WorkspaceConfiguration) + + const childNodes = await testNode.getChildren() + assert.strictEqual(childNodes.length, 1, 'Unexpected child count') + assert.strictEqual(childNodes[0].label, 'name2 (Stopped)', 'Unexpected node label') + }) + + describe('getSelectedDomainUsers', function () { + let originalState: Map + + beforeEach(async function () { + testNode = new SagemakerParentNode(testRegion, client) + originalState = new Map( + globals.globalState.get(SagemakerConstants.SelectedDomainUsersState, []) + ) + }) + + afterEach(async function () { + await globals.globalState.update(SagemakerConstants.SelectedDomainUsersState, [...originalState]) + }) + + it('gets cached selectedDomainUsers for a given region', async function () { + await globals.globalState.update(SagemakerConstants.SelectedDomainUsersState, [ + [testRegion, [['arn:aws:iam::123456789012:user/user2', ['domain2__user-cached']]]], + ]) + testNode.callerIdentity = iamUser + sinon + .stub(vscode.workspace, 'getConfiguration') + .returns(getConfigTrue as unknown as vscode.WorkspaceConfiguration) + + const result = await testNode.getSelectedDomainUsers() + assert.deepStrictEqual( + [...result], + ['domain2__user-cached'], + 'Should match only cached selected domain user' + ) + }) + + it('gets default selectedDomainUsers', async function () { + await globals.globalState.update(SagemakerConstants.SelectedDomainUsersState, []) + testNode.spaceApps = spaceAppsMap + testNode.callerIdentity = iamUser + sinon + .stub(vscode.workspace, 'getConfiguration') + .returns(getConfigTrue as unknown as vscode.WorkspaceConfiguration) + + const result = await testNode.getSelectedDomainUsers() + assert.deepStrictEqual( + [...result], + ['domain2__user2-efgh'], + 'Should match only default selected domain user' + ) + }) + }) + + describe('saveSelectedDomainUsers', function () { + let originalState: Map + + beforeEach(async function () { + testNode = new SagemakerParentNode(testRegion, client) + originalState = new Map( + globals.globalState.get(SagemakerConstants.SelectedDomainUsersState, []) + ) + }) + + afterEach(async function () { + await globals.globalState.update(SagemakerConstants.SelectedDomainUsersState, [...originalState]) + }) + + it('saves selectedDomainUsers for a given region', async function () { + testNode.callerIdentity = iamUser + testNode.saveSelectedDomainUsers(['domain1__user-1', 'domain2__user-2']) + + const selectedDomainUsersByRegionMap = new Map( + globals.globalState.get(SagemakerConstants.SelectedDomainUsersState, []) + ) + const selectedDomainUsers = new Map(selectedDomainUsersByRegionMap.get(testRegion)) + + assert.deepStrictEqual(selectedDomainUsers.get(iamUser.Arn), ['domain1__user-1', 'domain2__user-2']) + }) + }) + + describe('getLocalSelectedDomainUsers', function () { + const createSpaceApp = (ownerName: string): SagemakerSpaceApp => ({ + SpaceName: 'space1', + DomainId: 'domain1', + Status: 'InService', + OwnershipSettingsSummary: { + OwnerUserProfileName: ownerName, + }, + DomainSpaceKey: 'domain1__name1', + }) + + beforeEach(function () { + testNode = new SagemakerParentNode(testRegion, client) + }) + + it('matches IAM user ARN when filtering is enabled', async function () { + testNode.callerIdentity = { + Arn: 'arn:aws:iam::123456789012:user/user1', + } + + testNode.spaceApps = new Map([ + ['domain1__space1', createSpaceApp('user1-abc')], + ['domain1__space2', createSpaceApp('user2-xyz')], + ]) + + sinon.stub(vscode.workspace, 'getConfiguration').returns(getConfigTrue as any) + + const result = await testNode.getLocalSelectedDomainUsers() + assert.deepStrictEqual(result, ['domain1__user1-abc'], 'Should match only user1-prefixed space') + }) + + it('matches IAM assumed-role ARN when filtering is enabled', async function () { + testNode.callerIdentity = { + Arn: 'arn:aws:sts::123456789012:assumed-role/SomeRole/user2', + } + + testNode.spaceApps = new Map([ + ['domain1__space1', createSpaceApp('user2-xyz')], + ['domain1__space2', createSpaceApp('user3-def')], + ]) + + sinon.stub(vscode.workspace, 'getConfiguration').returns(getConfigTrue as any) + + const result = await testNode.getLocalSelectedDomainUsers() + assert.deepStrictEqual(result, ['domain1__user2-xyz'], 'Should match only user2-prefixed space') + }) + + it('matches Identity Center ARN when IAM filtering is disabled', async function () { + testNode.callerIdentity = { + Arn: 'arn:aws:sts::123456789012:assumed-role/AWSReservedSSO_PermissionSet_abcd/user3', + } + + testNode.spaceApps = new Map([ + ['domain1__space1', createSpaceApp('user3-aaa')], + ['domain1__space2', createSpaceApp('other-user')], + ]) + + sinon.stub(vscode.workspace, 'getConfiguration').returns(getConfigFalse as any) + + const result = await testNode.getLocalSelectedDomainUsers() + assert.deepStrictEqual(result, ['domain1__user3-aaa'], 'Should match only user3-prefixed space') + }) + + it('returns empty array if no match is found', async function () { + testNode.callerIdentity = { + Arn: 'arn:aws:iam::123456789012:user/no-match', + } + + testNode.spaceApps = new Map([['domain1__space1', createSpaceApp('someone-else')]]) + + sinon.stub(vscode.workspace, 'getConfiguration').returns(getConfigTrue as any) + + const result = await testNode.getLocalSelectedDomainUsers() + assert.deepStrictEqual(result, [], 'Should return empty list when no prefix matches') + }) + }) +}) diff --git a/packages/core/src/test/awsService/sagemaker/explorer/sagemakerSpaceNode.test.ts b/packages/core/src/test/awsService/sagemaker/explorer/sagemakerSpaceNode.test.ts new file mode 100644 index 00000000000..b0fc6d78c0f --- /dev/null +++ b/packages/core/src/test/awsService/sagemaker/explorer/sagemakerSpaceNode.test.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 * as sinon from 'sinon' +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 { PollingSet } from '../../../../shared/utilities/pollingSet' + +describe('SagemakerSpaceNode', function () { + const testRegion = 'testRegion' + let client: SagemakerClient + let testParent: SagemakerParentNode + let testSpaceApp: SagemakerSpaceApp + let describeAppStub: sinon.SinonStub + let testSpaceAppNode: SagemakerSpaceNode + + beforeEach(function () { + testSpaceApp = { + SpaceName: 'TestSpace', + DomainId: 'd-12345', + App: { AppName: 'TestApp', Status: 'InService' }, + SpaceSettingsSummary: { AppType: AppType.JupyterLab }, + OwnershipSettingsSummary: { OwnerUserProfileName: 'test-user' }, + SpaceSharingSettingsSummary: { SharingType: 'Private' }, + Status: 'InService', + DomainSpaceKey: '123', + } + + sinon.stub(PollingSet.prototype, 'add') + client = new SagemakerClient(testRegion) + testParent = new SagemakerParentNode(testRegion, client) + + describeAppStub = sinon.stub(SagemakerClient.prototype, 'describeApp') + testSpaceAppNode = new SagemakerSpaceNode(testParent, client, testRegion, testSpaceApp) + }) + + afterEach(function () { + sinon.restore() + }) + + it('initializes with correct label, description, and tooltip', function () { + const node = new SagemakerSpaceNode(testParent, client, testRegion, testSpaceApp) + + assert.strictEqual(node.label, 'TestSpace (Running)') + assert.strictEqual(node.description, 'Private space') + assert.ok(node.tooltip instanceof vscode.MarkdownString) + assert.ok((node.tooltip as vscode.MarkdownString).value.includes('**Space:** TestSpace')) + }) + + it('falls back to defaults if optional fields are missing', function () { + const partialApp: SagemakerSpaceApp = { + SpaceName: undefined, + DomainId: 'domainId', + Status: 'Failed', + DomainSpaceKey: '123', + } + + const node = new SagemakerSpaceNode(testParent, client, testRegion, partialApp) + + assert.strictEqual(node.label, '(no name) (Failed)') + assert.strictEqual(node.description, 'Unknown space') + assert.ok((node.tooltip as vscode.MarkdownString).value.includes('**Space:** -')) + }) + + it('returns ARN from describeApp', async function () { + describeAppStub.resolves({ AppArn: 'arn:aws:sagemaker:1234:app/TestApp', $metadata: {} }) + + const arn = await testSpaceAppNode.getAppArn() + + assert.strictEqual(arn, 'arn:aws:sagemaker:1234:app/TestApp') + sinon.assert.calledOnce(describeAppStub) + sinon.assert.calledWithExactly(describeAppStub, { + DomainId: 'd-12345', + AppName: 'TestApp', + AppType: AppType.JupyterLab, + SpaceName: 'TestSpace', + }) + }) + + 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(), '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: {} }) + describeAppStub.resolves({ AppName: 'TestApp', Status: 'InService', $metadata: {} }) + + await testSpaceAppNode.updateSpaceAppStatus() + + sinon.assert.calledOnce(describeSpaceStub) + sinon.assert.calledOnce(describeAppStub) + }) +}) diff --git a/packages/core/src/test/awsService/sagemaker/model.test.ts b/packages/core/src/test/awsService/sagemaker/model.test.ts new file mode 100644 index 00000000000..7570e1bfe31 --- /dev/null +++ b/packages/core/src/test/awsService/sagemaker/model.test.ts @@ -0,0 +1,304 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as sinon from 'sinon' +import * as os from 'os' +import * as path from 'path' +import { DevSettings, fs, ToolkitError } from '../../../shared' +import { removeKnownHost, startLocalServer, stopLocalServer } from '../../../awsService/sagemaker/model' +import { assertLogsContain } from '../../globalSetup.test' +import assert from 'assert' + +describe('SageMaker Model', () => { + describe('startLocalServer', function () { + const ctx = { + globalStorageUri: vscode.Uri.file(path.join(os.tmpdir(), 'test-storage')), + extensionPath: path.join(os.tmpdir(), 'extension'), + asAbsolutePath: (relPath: string) => path.join(path.join(os.tmpdir(), 'extension'), relPath), + } as vscode.ExtensionContext + + let sandbox: sinon.SinonSandbox + + beforeEach(() => { + sandbox = sinon.createSandbox() + }) + + afterEach(() => { + sandbox.restore() + }) + + it('waits for info file and starts server', async function () { + // Simulate the file doesn't exist initially, then appears on 3rd check + const existsStub = sandbox.stub(fs, 'existsFile') + existsStub.onCall(0).resolves(false) + existsStub.onCall(1).resolves(false) + existsStub.onCall(2).resolves(true) + + sandbox.stub(require('fs'), 'openSync').returns(42) + + const stopStub = sandbox.stub().resolves() + sandbox.replace(require('../../../awsService/sagemaker/model'), 'stopLocalServer', stopStub) + + const spawnStub = sandbox.stub().returns({ unref: sandbox.stub() }) + sandbox.replace(require('../../../awsService/sagemaker/utils'), 'spawnDetachedServer', spawnStub) + + sandbox.stub(DevSettings.instance, 'get').returns({ sagemaker: 'https://fake-endpoint' }) + + await startLocalServer(ctx) + + sinon.assert.called(spawnStub) + sinon.assert.calledWith( + spawnStub, + process.execPath, + [ctx.asAbsolutePath('dist/src/awsService/sagemaker/detached-server/server.js')], + sinon.match.any + ) + + 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 () { + const ctx = { + globalStorageUri: vscode.Uri.file(path.join(os.tmpdir(), 'test-storage')), + } as vscode.ExtensionContext + + const infoFilePath = path.join(ctx.globalStorageUri.fsPath, 'sagemaker-local-server-info.json') + const validPid = 12345 + const validJson = JSON.stringify({ pid: validPid }) + let sandbox: sinon.SinonSandbox + + beforeEach(() => { + sandbox = sinon.createSandbox() + }) + + afterEach(() => { + sandbox.restore() + }) + + it('logs debug when successfully stops server and deletes file', async function () { + sandbox.stub(fs, 'existsFile').resolves(true) + sandbox.stub(fs, 'readFileText').resolves(validJson) + const killStub = sandbox.stub(process, 'kill').returns(true) + const deleteStub = sandbox.stub(fs, 'delete').resolves() + + await stopLocalServer(ctx) + + sinon.assert.calledWith(killStub, validPid) + sinon.assert.calledWith(deleteStub, infoFilePath) + assertLogsContain(`stopped local server with PID ${validPid}`, false, 'debug') + assertLogsContain('removed server info file.', false, 'debug') + }) + + it('throws ToolkitError when info file is invalid JSON', async function () { + sandbox.stub(fs, 'existsFile').resolves(true) + sandbox.stub(fs, 'readFileText').resolves('invalid json') + + try { + await stopLocalServer(ctx) + assert.ok(false, 'Expected error not thrown') + } catch (err) { + assert.ok(err instanceof ToolkitError) + assert.strictEqual(err.message, 'failed to parse server info file') + } + }) + + 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) + sandbox.stub(fs, 'delete').resolves() + sandbox.stub(process, 'kill').throws({ code: 'EPERM', message: 'permission denied' }) + + try { + await stopLocalServer(ctx) + assert.ok(false) + } catch (err) { + assert.ok(err instanceof ToolkitError) + 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 () { + const knownHostsPath = path.join(os.homedir(), '.ssh', 'known_hosts') + const hostname = 'test.host.com' + let sandbox: sinon.SinonSandbox + + beforeEach(function () { + sandbox = sinon.createSandbox() + }) + + afterEach(function () { + sandbox.restore() + }) + + it('removes line with hostname and writes updated file', async function () { + sandbox.stub(fs, 'existsFile').resolves(true) + + const inputContent = `${hostname} 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(hostname) + + sinon.assert.calledWith( + writeStub, + knownHostsPath, + 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 () { + sandbox.stub(fs, 'existsFile').resolves(false) + + await removeKnownHost('test.host.com') + + assertLogsContain(`known_hosts not found at`, false, 'warn') + }) + + it('throws ToolkitError when reading known_hosts fails', async function () { + sandbox.stub(fs, 'existsFile').resolves(true) + sandbox.stub(fs, 'readFileText').rejects(new Error('read failed')) + + try { + await removeKnownHost(hostname) + assert.ok(false, 'Expected error was not thrown') + } catch (err) { + assert.ok(err instanceof ToolkitError) + assert.strictEqual(err.message, 'Failed to read known_hosts file') + assert.strictEqual((err as ToolkitError).cause?.message, 'read failed') + } + }) + + it('throws ToolkitError when writing known_hosts fails', async function () { + sandbox.stub(fs, 'existsFile').resolves(true) + sandbox.stub(fs, 'readFileText').resolves(`${hostname} ssh-rsa key\nsomehost ssh-rsa key`) + sandbox.stub(fs, 'writeFile').rejects(new Error('write failed')) + + try { + await removeKnownHost(hostname) + assert.ok(false, 'Expected error was not thrown') + } catch (err) { + assert.ok(err instanceof ToolkitError) + assert.strictEqual(err.message, 'Failed to write updated known_hosts file') + assert.strictEqual((err as ToolkitError).cause?.message, 'write failed') + } + }) + }) +}) diff --git a/packages/core/src/test/awsService/sagemaker/remoteUtils.test.ts b/packages/core/src/test/awsService/sagemaker/remoteUtils.test.ts new file mode 100644 index 00000000000..4168dfdeee4 --- /dev/null +++ b/packages/core/src/test/awsService/sagemaker/remoteUtils.test.ts @@ -0,0 +1,76 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as sinon from 'sinon' +import * as assert from 'assert' +import { getRemoteAppMetadata } from '../../../awsService/sagemaker/remoteUtils' +import { fs } from '../../../shared/fs/fs' +import { SagemakerClient } from '../../../shared/clients/sagemaker' + +describe('getRemoteAppMetadata', function () { + let sandbox: sinon.SinonSandbox + let fsStub: sinon.SinonStub + let parseRegionStub: sinon.SinonStub + let describeSpaceStub: sinon.SinonStub + let loggerStub: sinon.SinonStub + + const mockMetadata = { + AppType: 'JupyterLab', + DomainId: 'd-f0lwireyzpjp', + SpaceName: 'test-ae-3', + ExecutionRoleArn: 'arn:aws:iam::177118115371:role/service-role/AmazonSageMaker-ExecutionRole-20250415T091941', + ResourceArn: 'arn:aws:sagemaker:us-west-2:177118115371:app/d-f0lwireyzpjp/test-ae-3/JupyterLab/default', + ResourceName: 'default', + AppImageVersion: '', + ResourceArnCaseSensitive: + 'arn:aws:sagemaker:us-west-2:177118115371:app/d-f0lwireyzpjp/test-ae-3/JupyterLab/default', + IpAddressType: 'ipv4', + } + + const mockSpaceDetails = { + OwnershipSettings: { + OwnerUserProfileName: 'test-user-profile', + }, + } + + beforeEach(() => { + sandbox = sinon.createSandbox() + fsStub = sandbox.stub(fs, 'readFileText') + parseRegionStub = sandbox + .stub() + .returns({ region: 'us-west-2', accountId: '123456789012', spaceName: 'test-space' }) + sandbox.replace(require('../../../awsService/sagemaker/detached-server/utils'), 'parseArn', parseRegionStub) + + describeSpaceStub = sandbox.stub().resolves(mockSpaceDetails) + sandbox.stub(SagemakerClient.prototype, 'describeSpace').callsFake(describeSpaceStub) + + loggerStub = sandbox.stub().returns({ + error: sandbox.stub(), + }) + sandbox.replace(require('../../../shared/logger/logger'), 'getLogger', loggerStub) + }) + + afterEach(() => { + sandbox.restore() + }) + + it('successfully reads metadata file and returns remote app metadata', async function () { + fsStub.resolves(JSON.stringify(mockMetadata)) + + const result = await getRemoteAppMetadata() + + assert.deepStrictEqual(result, { + DomainId: 'd-f0lwireyzpjp', + UserProfileName: 'test-user-profile', + }) + + sinon.assert.calledWith(fsStub, '/opt/ml/metadata/resource-metadata.json') + sinon.assert.calledWith(parseRegionStub, mockMetadata.ResourceArn) + sinon.assert.calledWith(describeSpaceStub, { + DomainId: 'd-f0lwireyzpjp', + SpaceName: 'test-ae-3', + }) + }) +}) 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..2a52b08a3a6 --- /dev/null +++ b/packages/core/src/test/awsService/sagemaker/sagemakerSpace.test.ts @@ -0,0 +1,129 @@ +/*! + * 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) + + 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.describeSpace.resolves(mockDescribeSpaceResponse) + mockClient.describeApp.resolves(mockDescribeAppResponse) + + 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) + }) + }) +}) diff --git a/packages/core/src/test/awsService/sagemaker/uriHandlers.test.ts b/packages/core/src/test/awsService/sagemaker/uriHandlers.test.ts new file mode 100644 index 00000000000..f27df1fcb11 --- /dev/null +++ b/packages/core/src/test/awsService/sagemaker/uriHandlers.test.ts @@ -0,0 +1,79 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as sinon from 'sinon' +import * as vscode from 'vscode' +import assert from 'assert' +import { UriHandler } from '../../../shared/vscode/uriHandler' +import { VSCODE_EXTENSION_ID } from '../../../shared/extensions' +import { register } from '../../../awsService/sagemaker/uriHandlers' + +function createConnectUri(params: { [key: string]: string }): vscode.Uri { + const query = Object.entries(params) + .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) + .join('&') + return vscode.Uri.parse(`vscode://${VSCODE_EXTENSION_ID.awstoolkit}/connect/sagemaker?${query}`) +} + +describe('SageMaker URI handler', function () { + let handler: UriHandler + let deeplinkConnectStub: sinon.SinonStub + + beforeEach(function () { + handler = new UriHandler() + deeplinkConnectStub = sinon.stub().resolves() + sinon.replace(require('../../../awsService/sagemaker/commands'), 'deeplinkConnect', deeplinkConnectStub) + + register({ + uriHandler: handler, + } as any) + }) + + afterEach(function () { + sinon.restore() + }) + + it('calls deeplinkConnect with all expected params', 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', + app_type: 'jupyterlab', + } + + const uri = createConnectUri(params) + await handler.handleUri(uri) + + assert.ok(deeplinkConnectStub.calledOnce) + assert.deepStrictEqual(deeplinkConnectStub.firstCall.args[1], 'abc123') + assert.deepStrictEqual(deeplinkConnectStub.firstCall.args[2], 'sess-xyz') + 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/awsService/sagemaker/utils.test.ts b/packages/core/src/test/awsService/sagemaker/utils.test.ts new file mode 100644 index 00000000000..c73fd6968fc --- /dev/null +++ b/packages/core/src/test/awsService/sagemaker/utils.test.ts @@ -0,0 +1,150 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { AppStatus, SpaceStatus } from '@aws-sdk/client-sagemaker' +import { generateSpaceStatus, ActivityCheckInterval } from '../../../awsService/sagemaker/utils' +import * as assert from 'assert' +import * as sinon from 'sinon' +import { fs } from '../../../shared/fs/fs' +import * as utils from '../../../awsService/sagemaker/utils' + +describe('generateSpaceStatus', function () { + it('returns Failed if space status is Failed', function () { + assert.strictEqual(generateSpaceStatus(SpaceStatus.Failed, AppStatus.InService), 'Failed') + }) + + it('returns Failed if space status is Delete_Failed', function () { + assert.strictEqual(generateSpaceStatus(SpaceStatus.Delete_Failed, AppStatus.InService), 'Failed') + }) + + it('returns Failed if space status is Update_Failed', function () { + assert.strictEqual(generateSpaceStatus(SpaceStatus.Update_Failed, AppStatus.InService), 'Failed') + }) + + it('returns Failed if app status is Failed and space status is not Updating', function () { + assert.strictEqual(generateSpaceStatus(SpaceStatus.Deleting, AppStatus.Failed), 'Failed') + }) + + it('does not return Failed if app status is Failed but space status is Updating', function () { + assert.strictEqual(generateSpaceStatus(SpaceStatus.Updating, AppStatus.Failed), 'Updating') + }) + + it('returns Running if both statuses are InService', function () { + assert.strictEqual(generateSpaceStatus(SpaceStatus.InService, AppStatus.InService), 'Running') + }) + + it('returns Starting if app is Pending and space is InService', function () { + assert.strictEqual(generateSpaceStatus(SpaceStatus.InService, AppStatus.Pending), 'Starting') + }) + + it('returns Updating if space status is Updating', function () { + assert.strictEqual(generateSpaceStatus(SpaceStatus.Updating, AppStatus.Deleting), 'Updating') + }) + + it('returns Stopping if app is Deleting and space is InService', function () { + assert.strictEqual(generateSpaceStatus(SpaceStatus.InService, AppStatus.Deleting), 'Stopping') + }) + + it('returns Stopped if app is Deleted and space is InService', function () { + assert.strictEqual(generateSpaceStatus(SpaceStatus.InService, AppStatus.Deleted), 'Stopped') + }) + + it('returns Stopped if app status is undefined and space is InService', function () { + assert.strictEqual(generateSpaceStatus(SpaceStatus.InService, undefined), 'Stopped') + }) + + it('returns Deleting if space is Deleting', function () { + assert.strictEqual(generateSpaceStatus(SpaceStatus.Deleting, AppStatus.InService), 'Deleting') + }) + + it('returns Unknown if none of the above match', function () { + assert.strictEqual(generateSpaceStatus(undefined, undefined), 'Unknown') + assert.strictEqual( + generateSpaceStatus('SomeOtherStatus' as SpaceStatus, 'RandomAppStatus' as AppStatus), + 'Unknown' + ) + }) +}) + +describe('checkTerminalActivity', function () { + let sandbox: sinon.SinonSandbox + let fsReaddirStub: sinon.SinonStub + let fsStatStub: sinon.SinonStub + let fsWriteFileStub: sinon.SinonStub + + beforeEach(function () { + sandbox = sinon.createSandbox() + fsReaddirStub = sandbox.stub(fs, 'readdir') + fsStatStub = sandbox.stub(fs, 'stat') + fsWriteFileStub = sandbox.stub(fs, 'writeFile') + }) + + afterEach(function () { + sandbox.restore() + }) + + it('should write to idle file when recent terminal activity is detected', async function () { + const idleFilePath = '/tmp/test-idle-file' + const recentTime = Date.now() - ActivityCheckInterval / 2 // Recent activity + + fsReaddirStub.resolves([ + ['pts1', 1], + ['pts2', 1], + ]) // Mock file entries + fsStatStub.onFirstCall().resolves({ mtime: new Date(recentTime) }) + fsWriteFileStub.resolves() + + await utils.checkTerminalActivity(idleFilePath) + + // Verify that fs.writeFile was called (which means updateIdleFile was called) + assert.strictEqual(fsWriteFileStub.callCount, 1) + assert.strictEqual(fsWriteFileStub.firstCall.args[0], idleFilePath) + + // Verify the timestamp is a valid ISO string + const timestamp = fsWriteFileStub.firstCall.args[1] + assert.strictEqual(typeof timestamp, 'string') + assert.ok(!isNaN(Date.parse(timestamp))) + }) + + it('should stop checking once activity is detected', async function () { + const idleFilePath = '/tmp/test-idle-file' + const recentTime = Date.now() - ActivityCheckInterval / 2 + + fsReaddirStub.resolves([ + ['pts1', 1], + ['pts2', 1], + ['pts3', 1], + ]) + fsStatStub.onFirstCall().resolves({ mtime: new Date(recentTime) }) // First file has activity + fsWriteFileStub.resolves() + + await utils.checkTerminalActivity(idleFilePath) + + // Should only call stat once since activity was detected on first file + assert.strictEqual(fsStatStub.callCount, 1) + // Should write to file once + assert.strictEqual(fsWriteFileStub.callCount, 1) + }) + + it('should handle stat error gracefully and continue checking other files', async function () { + const idleFilePath = '/tmp/test-idle-file' + const recentTime = Date.now() - ActivityCheckInterval / 2 + const statError = new Error('File not found') + + fsReaddirStub.resolves([ + ['pts1', 1], + ['pts2', 1], + ]) + fsStatStub.onFirstCall().rejects(statError) // First file fails + fsStatStub.onSecondCall().resolves({ mtime: new Date(recentTime) }) // Second file succeeds + fsWriteFileStub.resolves() + + await utils.checkTerminalActivity(idleFilePath) + + // Should continue and find activity on second file + assert.strictEqual(fsStatStub.callCount, 2) + assert.strictEqual(fsWriteFileStub.callCount, 1) + }) +}) diff --git a/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts b/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts index 936e7d84cd6..a57ff6fcea3 100644 --- a/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts +++ b/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts @@ -8,7 +8,7 @@ import assert from 'assert' import * as sinon from 'sinon' import * as CodeWhispererConstants from '../../../codewhisperer/models/constants' import { createCodeScanIssue, createMockDocument, resetCodeWhispererGlobalVariables } from '../testUtil' -import { assertNoTelemetryMatch, assertTelemetry, assertTelemetryCurried, tryRegister } from '../../testUtil' +import { assertTelemetry, assertTelemetryCurried, tryRegister } from '../../testUtil' import { toggleCodeSuggestions, showSecurityScan, @@ -19,10 +19,8 @@ import { reconnect, signoutCodeWhisperer, toggleCodeScans, - generateFix, rejectFix, ignoreIssue, - regenerateFix, ignoreAllIssues, } from '../../../codewhisperer/commands/basicCommands' import { FakeExtensionContext } from '../../fakeExtensionContext' @@ -30,7 +28,7 @@ import { testCommand } from '../../shared/vscode/testUtils' import { Command, placeholder } from '../../../shared/vscode/commands2' import { SecurityPanelViewProvider } from '../../../codewhisperer/views/securityPanelViewProvider' import { DefaultCodeWhispererClient } from '../../../codewhisperer/client/codewhisperer' -import { Stub, stub } from '../../utilities/stubber' +import { stub } from '../../utilities/stubber' import { AuthUtil } from '../../../codewhisperer/util/authUtil' import { getTestWindow } from '../../shared/vscode/window' import { ExtContext } from '../../../shared/extensions' @@ -45,7 +43,6 @@ import { createManageSubscription, createOpenReferenceLog, createReconnect, - createSecurityScan, createSelectCustomization, createSeparator, createSettingsNode, @@ -57,7 +54,7 @@ import { waitUntil } from '../../../shared/utilities/timeoutUtils' import { listCodeWhispererCommands } from '../../../codewhisperer/ui/statusBarMenu' import { CodeScanIssue, CodeScansState, CodeSuggestionsState, codeScanState } from '../../../codewhisperer/models/model' import { cwQuickPickSource } from '../../../codewhisperer/commands/types' -import { refreshStatusBar } from '../../../codewhisperer/service/inlineCompletionService' +import { refreshStatusBar } from '../../../codewhisperer/service/statusBar' import { focusAmazonQPanel } from '../../../codewhispererChat/commands/registerCommands' import * as diagnosticsProvider from '../../../codewhisperer/service/diagnosticsProvider' import { randomUUID } from '../../../shared/crypto' @@ -68,7 +65,6 @@ import { SecurityIssueProvider } from '../../../codewhisperer/service/securityIs import { CodeWhispererSettings } from '../../../codewhisperer/util/codewhispererSettings' import { confirm } from '../../../shared' import * as commentUtils from '../../../shared/utilities/commentUtils' -import * as startCodeFixGeneration from '../../../codewhisperer/commands/startCodeFixGeneration' import * as extUtils from '../../../shared/extensionUtilities' describe('CodeWhisperer-basicCommands', function () { @@ -509,7 +505,6 @@ describe('CodeWhisperer-basicCommands', function () { createOpenReferenceLog(), createGettingStarted(), createSeparator('Code Reviews'), - createSecurityScan(), createSeparator('Other Features'), switchToAmazonQNode(), createSeparator('Connect / Help'), @@ -790,173 +785,7 @@ def execute_input_compliant(): }) }) - describe('generateFix', function () { - let sandbox: sinon.SinonSandbox - let mockClient: Stub - let startCodeFixGenerationStub: sinon.SinonStub - let filePath: string - let codeScanIssue: CodeScanIssue - let issueItem: IssueItem - let updateSecurityIssueWebviewMock: sinon.SinonStub - let updateIssueMock: sinon.SinonStub - let refreshTreeViewMock: sinon.SinonStub - let mockExtContext: ExtContext - - beforeEach(async function () { - sandbox = sinon.createSandbox() - mockClient = stub(DefaultCodeWhispererClient) - startCodeFixGenerationStub = sinon.stub(startCodeFixGeneration, 'startCodeFixGeneration') - filePath = 'dummy/file.py' - codeScanIssue = createCodeScanIssue({ - findingId: randomUUID(), - ruleId: 'dummy-rule-id', - }) - issueItem = new IssueItem(filePath, codeScanIssue) - updateSecurityIssueWebviewMock = sinon.stub(securityIssueWebview, 'updateSecurityIssueWebview') - updateIssueMock = sinon.stub(SecurityIssueProvider.instance, 'updateIssue') - refreshTreeViewMock = sinon.stub(SecurityIssueTreeViewProvider.instance, 'refresh') - mockExtContext = await FakeExtensionContext.getFakeExtContext() - }) - - afterEach(function () { - sandbox.restore() - }) - - it('should call generateFix command successfully', async function () { - startCodeFixGenerationStub.resolves({ - suggestedFix: { - codeDiff: 'codeDiff', - description: 'description', - references: [], - }, - jobId: 'jobId', - }) - - targetCommand = testCommand(generateFix, mockClient, mockExtContext) - await targetCommand.execute(codeScanIssue, filePath, 'webview') - - assert.ok(updateSecurityIssueWebviewMock.calledWith(sinon.match({ isGenerateFixLoading: true }))) - assert.ok( - startCodeFixGenerationStub.calledWith(mockClient, codeScanIssue, filePath, codeScanIssue.findingId) - ) - - const expectedUpdatedIssue = { - ...codeScanIssue, - fixJobId: 'jobId', - suggestedFixes: [{ code: 'codeDiff', description: 'description', references: [] }], - } - assert.ok( - updateSecurityIssueWebviewMock.calledWith( - sinon.match({ - issue: expectedUpdatedIssue, - isGenerateFixLoading: false, - filePath: filePath, - shouldRefreshView: true, - }) - ) - ) - assert.ok(updateIssueMock.calledWith(expectedUpdatedIssue, filePath)) - assert.ok(refreshTreeViewMock.calledOnce) - - assertTelemetry('codewhisperer_codeScanIssueGenerateFix', { - detectorId: codeScanIssue.detectorId, - findingId: codeScanIssue.findingId, - ruleId: codeScanIssue.ruleId, - component: 'webview', - result: 'Succeeded', - }) - }) - - it('should call generateFix from tree view item', async function () { - startCodeFixGenerationStub.resolves({ - suggestedFix: { - codeDiff: 'codeDiff', - description: 'description', - references: [], - }, - jobId: 'jobId', - }) - - targetCommand = testCommand(generateFix, mockClient, mockExtContext) - await targetCommand.execute(issueItem, filePath, 'tree') - - assertTelemetry('codewhisperer_codeScanIssueGenerateFix', { - detectorId: codeScanIssue.detectorId, - findingId: codeScanIssue.findingId, - ruleId: codeScanIssue.ruleId, - component: 'tree', - result: 'Succeeded', - }) - }) - - it('should call generateFix with refresh=true to indicate fix regenerated', async function () { - startCodeFixGenerationStub.resolves({ - suggestedFix: { - codeDiff: 'codeDiff', - description: 'description', - references: [], - }, - jobId: 'jobId', - }) - - targetCommand = testCommand(generateFix, mockClient, mockExtContext) - await targetCommand.execute(codeScanIssue, filePath, 'webview', true) - - assertTelemetry('codewhisperer_codeScanIssueGenerateFix', { - detectorId: codeScanIssue.detectorId, - findingId: codeScanIssue.findingId, - ruleId: codeScanIssue.ruleId, - component: 'webview', - result: 'Succeeded', - variant: 'refresh', - }) - }) - - it('should handle generateFix error', async function () { - startCodeFixGenerationStub.throws(new Error('Unexpected error')) - - targetCommand = testCommand(generateFix, mockClient, mockExtContext) - await targetCommand.execute(codeScanIssue, filePath, 'webview') - - assert.ok(updateSecurityIssueWebviewMock.calledWith(sinon.match({ isGenerateFixLoading: true }))) - assert.ok( - updateSecurityIssueWebviewMock.calledWith( - sinon.match({ - issue: codeScanIssue, - isGenerateFixLoading: false, - generateFixError: 'Unexpected error', - shouldRefreshView: false, - }) - ) - ) - assert.ok(updateIssueMock.calledWith(codeScanIssue, filePath)) - assert.ok(refreshTreeViewMock.calledOnce) - - assertTelemetry('codewhisperer_codeScanIssueGenerateFix', { - detectorId: codeScanIssue.detectorId, - findingId: codeScanIssue.findingId, - ruleId: codeScanIssue.ruleId, - component: 'webview', - result: 'Failed', - reason: 'Error', - reasonDesc: 'Unexpected error', - }) - }) - - it('exits early for SAS findings', async function () { - targetCommand = testCommand(generateFix, mockClient, mockExtContext) - codeScanIssue = createCodeScanIssue({ - ruleId: CodeWhispererConstants.sasRuleId, - }) - issueItem = new IssueItem(filePath, codeScanIssue) - await targetCommand.execute(codeScanIssue, filePath, 'webview') - assert.ok(updateSecurityIssueWebviewMock.notCalled) - assert.ok(startCodeFixGenerationStub.notCalled) - assert.ok(updateIssueMock.notCalled) - assert.ok(refreshTreeViewMock.notCalled) - assertNoTelemetryMatch('codewhisperer_codeScanIssueGenerateFix') - }) - }) + // TODO: Add integ test for generateTest describe('rejectFix', function () { let mockExtensionContext: vscode.ExtensionContext @@ -1150,51 +979,4 @@ def execute_input_compliant(): }) }) }) - - describe('regenerateFix', function () { - let sandbox: sinon.SinonSandbox - let filePath: string - let codeScanIssue: CodeScanIssue - let issueItem: IssueItem - let rejectFixMock: sinon.SinonStub - let generateFixMock: sinon.SinonStub - - beforeEach(function () { - sandbox = sinon.createSandbox() - filePath = 'dummy/file.py' - codeScanIssue = createCodeScanIssue({ - findingId: randomUUID(), - suggestedFixes: [{ code: 'diff', description: 'description' }], - }) - issueItem = new IssueItem(filePath, codeScanIssue) - rejectFixMock = sinon.stub() - generateFixMock = sinon.stub() - }) - - afterEach(function () { - sandbox.restore() - }) - - it('should call regenerateFix command successfully', async function () { - const updatedIssue = createCodeScanIssue({ findingId: 'updatedIssue' }) - sinon.stub(rejectFix, 'execute').value(rejectFixMock.resolves(updatedIssue)) - sinon.stub(generateFix, 'execute').value(generateFixMock) - targetCommand = testCommand(regenerateFix) - await targetCommand.execute(codeScanIssue, filePath) - - assert.ok(rejectFixMock.calledWith(codeScanIssue, filePath)) - assert.ok(generateFixMock.calledWith(updatedIssue, filePath)) - }) - - it('should call regenerateFix from tree view item', async function () { - const updatedIssue = createCodeScanIssue({ findingId: 'updatedIssue' }) - sinon.stub(rejectFix, 'execute').value(rejectFixMock.resolves(updatedIssue)) - sinon.stub(generateFix, 'execute').value(generateFixMock) - targetCommand = testCommand(regenerateFix) - await targetCommand.execute(issueItem, filePath) - - assert.ok(rejectFixMock.calledWith(codeScanIssue, filePath)) - assert.ok(generateFixMock.calledWith(updatedIssue, filePath)) - }) - }) }) diff --git a/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts b/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts index edb2524ee68..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' @@ -53,18 +53,22 @@ import * as nodefs from 'fs' // eslint-disable-line no-restricted-imports describe('transformByQ', function () { let fetchStub: sinon.SinonStub let tempDir: string - const validCustomVersionsFile = `name: "custom-dependency-management" + const validCustomVersionsFile = `name: "dependency-upgrade" description: "Custom dependency version management for Java migration from JDK 8/11/17 to JDK 17/21" dependencyManagement: dependencies: - identifier: "com.example:library1" - targetVersion: "2.1.0" - versionProperty: "library1.version" - originType: "FIRST_PARTY" + targetVersion: "2.1.0" + versionProperty: "library1.version" # Optional + originType: "FIRST_PARTY" # or "THIRD_PARTY" + - identifier: "com.example:library2" + targetVersion: "3.0.0" + originType: "THIRD_PARTY" plugins: - - identifier: "com.example.plugin" - targetVersion: "1.2.0" - versionProperty: "plugin.version"` + - identifier: "plugin.id" + targetVersion: "1.2.0" + versionProperty: "plugin.version" # Optional + originType: "FIRST_PARTY" # or "THIRD_PARTY"` const validSctFile = ` @@ -119,6 +123,7 @@ dependencyManagement: }) afterEach(async function () { + fetchStub.restore() sinon.restore() await fs.delete(tempDir, { recursive: true }) }) @@ -278,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') @@ -405,7 +412,6 @@ dependencyManagement: path: tempDir, name: tempFileName, }, - humanInTheLoopFlag: false, projectPath: tempDir, zipManifest: transformManifest, }).then((zipCodeResult) => { @@ -462,7 +468,7 @@ dependencyManagement: ] for (const folder of m2Folders) { - const folderPath = path.join(tempDir, folder) + const folderPath = path.join(tempDir, 'dependencies', folder) await fs.mkdir(folderPath) for (const file of filesToAdd) { await fs.writeFile(path.join(folderPath, file), 'sample content for the test file') @@ -476,7 +482,6 @@ dependencyManagement: path: tempDir, name: tempFileName, }, - humanInTheLoopFlag: false, projectPath: tempDir, zipManifest: new ZipManifest(), }).then((zipCodeResult) => { @@ -500,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) }) @@ -568,15 +577,45 @@ dependencyManagement: assert.strictEqual(expectedWarning, warningMessage) }) - it(`WHEN validateCustomVersionsFile on fully valid .yaml file THEN passes validation`, async function () { - const isValidFile = await validateCustomVersionsFile(validCustomVersionsFile) - assert.strictEqual(isValidFile, true) + 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 isValidFile = await validateCustomVersionsFile(invalidFile) - assert.strictEqual(isValidFile, false) + 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 () { @@ -662,7 +701,6 @@ dependencyManagement: message: expectedMessage, } ) - sinon.assert.callCount(fetchStub, 4) }) it('should not retry upload on non-retriable error', async () => { diff --git a/packages/core/src/test/codewhisperer/mergeIssuesDisplayFindings.test.ts b/packages/core/src/test/codewhisperer/mergeIssuesDisplayFindings.test.ts new file mode 100644 index 00000000000..3a8c06a3c7d --- /dev/null +++ b/packages/core/src/test/codewhisperer/mergeIssuesDisplayFindings.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 { SecurityIssueProvider } from '../../codewhisperer/service/securityIssueProvider' +import { createCodeScanIssue } from './testUtil' +import { displayFindingsDetectorName } from '../../codewhisperer/models/constants' +import { AggregatedCodeScanIssue } from '../../codewhisperer/models/model' + +describe('mergeIssuesDisplayFindings', () => { + let provider: SecurityIssueProvider + const testFilePath = '/test/file.py' + + beforeEach(() => { + provider = Object.create(SecurityIssueProvider.prototype) + provider.issues = [] + }) + + it('should add new issues when no existing group', () => { + const newIssues: AggregatedCodeScanIssue = { + filePath: testFilePath, + issues: [createCodeScanIssue({ findingId: 'new-1' })], + } + + provider.mergeIssuesDisplayFindings(newIssues, true) + + assert.strictEqual(provider.issues.length, 1) + assert.strictEqual(provider.issues[0].filePath, testFilePath) + assert.strictEqual(provider.issues[0].issues.length, 1) + assert.strictEqual(provider.issues[0].issues[0].findingId, 'new-1') + }) + + it('should keep displayFindings when fromQCA is true', () => { + provider.issues = [ + { + filePath: testFilePath, + issues: [ + createCodeScanIssue({ findingId: 'qca-1', detectorName: 'QCA-detector' }), + createCodeScanIssue({ findingId: 'display-1', detectorName: displayFindingsDetectorName }), + ], + }, + ] + + const newIssues: AggregatedCodeScanIssue = { + filePath: testFilePath, + issues: [createCodeScanIssue({ findingId: 'new-qca-1', detectorName: 'QCA-detector' })], + } + + provider.mergeIssuesDisplayFindings(newIssues, true) + + assert.strictEqual(provider.issues.length, 1) + assert.strictEqual(provider.issues[0].issues.length, 2) + + const findingIds = provider.issues[0].issues.map((issue) => issue.findingId) + assert.ok(findingIds.includes('display-1')) + assert.ok(findingIds.includes('new-qca-1')) + assert.ok(!findingIds.includes('qca-1')) + }) + + it('should keep QCA findings when fromQCA is false', () => { + provider.issues = [ + { + filePath: testFilePath, + issues: [ + createCodeScanIssue({ findingId: 'qca-1', detectorName: 'QCA-detector' }), + createCodeScanIssue({ findingId: 'display-1', detectorName: displayFindingsDetectorName }), + ], + }, + ] + + const newIssues: AggregatedCodeScanIssue = { + filePath: testFilePath, + issues: [createCodeScanIssue({ findingId: 'new-display-1', detectorName: displayFindingsDetectorName })], + } + + provider.mergeIssuesDisplayFindings(newIssues, false) + + assert.strictEqual(provider.issues.length, 1) + assert.strictEqual(provider.issues[0].issues.length, 2) + + const findingIds = provider.issues[0].issues.map((issue) => issue.findingId) + assert.ok(findingIds.includes('qca-1')) + assert.ok(findingIds.includes('new-display-1')) + assert.ok(!findingIds.includes('display-1')) + }) +}) 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/codewhisperer/testUtil.ts b/packages/core/src/test/codewhisperer/testUtil.ts index f3b82fd3850..dd8188b1006 100644 --- a/packages/core/src/test/codewhisperer/testUtil.ts +++ b/packages/core/src/test/codewhisperer/testUtil.ts @@ -14,7 +14,6 @@ import { } from '../../codewhisperer/models/model' import { MockDocument } from '../fake/fakeDocument' import { getLogger } from '../../shared/logger' -import { CodeWhispererCodeCoverageTracker } from '../../codewhisperer/tracker/codewhispererCodeCoverageTracker' import globals from '../../shared/extensionGlobals' import { session } from '../../codewhisperer/util/codeWhispererSession' import { DefaultAWSClientBuilder, ServiceOptions } from '../../shared/awsClientBuilder' @@ -23,7 +22,6 @@ import { HttpResponse, Service } from 'aws-sdk' import userApiConfig = require('./../../codewhisperer/client/user-service-2.json') import CodeWhispererUserClient = require('../../codewhisperer/client/codewhispereruserclient') import { codeWhispererClient } from '../../codewhisperer/client/codewhisperer' -import { RecommendationHandler } from '../../codewhisperer/service/recommendationHandler' import * as model from '../../codewhisperer/models/model' import { stub } from '../utilities/stubber' import { Dirent } from 'fs' // eslint-disable-line no-restricted-imports @@ -31,12 +29,10 @@ import { Dirent } from 'fs' // eslint-disable-line no-restricted-imports export async function resetCodeWhispererGlobalVariables() { vsCodeState.isIntelliSenseActive = false vsCodeState.isCodeWhispererEditing = false - CodeWhispererCodeCoverageTracker.instances.clear() globals.telemetry.logger.clear() session.reset() await globals.globalState.clear() await CodeSuggestionsState.instance.setSuggestionsEnabled(true) - await RecommendationHandler.instance.clearInlineCompletionStates() } export function createMockDocument( diff --git a/packages/core/src/test/codewhisperer/zipUtil.test.ts b/packages/core/src/test/codewhisperer/zipUtil.test.ts index a82db4a6840..102bf2fc441 100644 --- a/packages/core/src/test/codewhisperer/zipUtil.test.ts +++ b/packages/core/src/test/codewhisperer/zipUtil.test.ts @@ -7,16 +7,12 @@ import assert from 'assert' import vscode from 'vscode' import sinon from 'sinon' import { join } from 'path' -import path from 'path' import JSZip from 'jszip' import { getTestWorkspaceFolder } from '../../testInteg/integrationTestsUtilities' import { ZipUtil } from '../../codewhisperer/util/zipUtil' import { CodeAnalysisScope, codeScanTruncDirPrefix } from '../../codewhisperer/models/constants' import { ToolkitError } from '../../shared/errors' import { fs } from '../../shared/fs/fs' -import { tempDirPath } from '../../shared/filesystemUtilities' -import { CodeWhispererConstants } from '../../codewhisperer/indexNode' -import { LspClient } from '../../amazonq/lsp/lspClient' describe('zipUtil', function () { const workspaceFolder = getTestWorkspaceFolder() @@ -141,61 +137,4 @@ describe('zipUtil', function () { assert.ok(files.includes(join('workspaceFolder', 'workspaceFolder', 'App.java'))) }) }) - - describe('generateZipTestGen', function () { - let zipUtil: ZipUtil - let getZipDirPathStub: sinon.SinonStub - let testTempDirPath: string - - beforeEach(function () { - zipUtil = new ZipUtil() - testTempDirPath = path.join(tempDirPath, CodeWhispererConstants.TestGenerationTruncDirPrefix) - getZipDirPathStub = sinon.stub(zipUtil, 'getZipDirPath') - getZipDirPathStub.callsFake(() => testTempDirPath) - }) - - afterEach(function () { - sinon.restore() - }) - - it('should generate zip for test generation successfully', async function () { - const mkdirSpy = sinon.spy(fs, 'mkdir') - - const result = await zipUtil.generateZipTestGen(appRoot, false) - - assert.ok(mkdirSpy.calledWith(path.join(testTempDirPath, 'utgRequiredArtifactsDir'))) - assert.ok( - mkdirSpy.calledWith(path.join(testTempDirPath, 'utgRequiredArtifactsDir', 'buildAndExecuteLogDir')) - ) - assert.ok(mkdirSpy.calledWith(path.join(testTempDirPath, 'utgRequiredArtifactsDir', 'repoMapData'))) - assert.ok(mkdirSpy.calledWith(path.join(testTempDirPath, 'utgRequiredArtifactsDir', 'testCoverageDir'))) - - assert.strictEqual(result.rootDir, testTempDirPath) - assert.strictEqual(result.zipFilePath, testTempDirPath + CodeWhispererConstants.codeScanZipExt) - assert.ok(result.srcPayloadSizeInBytes > 0) - assert.strictEqual(result.buildPayloadSizeInBytes, 0) - assert.ok(result.zipFileSizeInBytes > 0) - assert.strictEqual(result.lines, 150) - assert.strictEqual(result.language, 'java') - assert.strictEqual(result.scannedFiles.size, 4) - }) - - it('Should handle file system errors during directory creation', async function () { - sinon.stub(LspClient, 'instance').get(() => ({ - getRepoMapJSON: sinon.stub().resolves('{"mock": "data"}'), - })) - sinon.stub(fs, 'mkdir').rejects(new Error('Directory creation failed')) - - await assert.rejects(() => zipUtil.generateZipTestGen(appRoot, false), /Directory creation failed/) - }) - - it('Should handle zip project errors', async function () { - sinon.stub(LspClient, 'instance').get(() => ({ - getRepoMapJSON: sinon.stub().resolves('{"mock": "data"}'), - })) - sinon.stub(zipUtil, 'zipProject' as keyof ZipUtil).rejects(new Error('Zip failed')) - - await assert.rejects(() => zipUtil.generateZipTestGen(appRoot, false), /Zip failed/) - }) - }) }) diff --git a/packages/core/src/test/credentials/auth.test.ts b/packages/core/src/test/credentials/auth.test.ts index 022e2c5c6e7..a409f2ba2e2 100644 --- a/packages/core/src/test/credentials/auth.test.ts +++ b/packages/core/src/test/credentials/auth.test.ts @@ -570,6 +570,47 @@ describe('Auth', function () { assert.strictEqual((await promptForConnection(auth))?.id, conn.id) }) + it('shows a second quickPick for linked IAM profiles when selecting an SSO connection', async function () { + let quickPickCount = 0 + getTestWindow().onDidShowQuickPick(async (picker) => { + await picker.untilReady() + quickPickCount++ + + if (quickPickCount === 1) { + // First picker: select the SSO connection + const connItem = picker.findItemOrThrow(/IAM Identity Center/) + picker.acceptItem(connItem) + } else if (quickPickCount === 2) { + // Second picker: select the linked IAM profile + const linkedItem = picker.findItemOrThrow(/TestRole/) + picker.acceptItem(linkedItem) + } + }) + + const linkedSsoProfile = createSsoProfile({ scopes: scopesSsoAccountAccess }) + const conn = await auth.createConnection(linkedSsoProfile) + + // Mock the SSOClient to return account roles + auth.ssoClient.listAccounts.returns( + toCollection(async function* () { + yield [{ accountId: '123456789012' }] + }) + ) + auth.ssoClient.listAccountRoles.callsFake(() => + toCollection(async function* () { + yield [{ accountId: '123456789012', roleName: 'TestRole' }] + }) + ) + + // Should get a linked IAM profile back, not the SSO connection + const result = await promptForConnection(auth) + assert.ok(isIamConnection(result || undefined), 'Expected an IAM connection to be returned') + assert.ok( + result?.id.startsWith(`sso:${conn.id}#`), + 'Expected the IAM connection to be linked to the SSO connection' + ) + }) + it('refreshes when clicking the refresh button', async function () { getTestWindow().onDidShowQuickPick(async (picker) => { await picker.untilReady() 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..6c023a52676 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,7 @@ describe('getApiValueForSchemasDownload', function () { case 'python3.9': case 'python3.11': case 'python3.12': - case 'python3.13': + case 'python3.13' 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/feedback/commands/submitFeedbackListener.test.ts b/packages/core/src/test/feedback/commands/submitFeedbackListener.test.ts index 176e12974ea..3fbf666a6ea 100644 --- a/packages/core/src/test/feedback/commands/submitFeedbackListener.test.ts +++ b/packages/core/src/test/feedback/commands/submitFeedbackListener.test.ts @@ -10,9 +10,14 @@ import { FeedbackWebview } from '../../../feedback/vue/submitFeedback' import sinon from 'sinon' import { waitUntil } from '../../../shared' -const comment = 'comment' +const comment = + 'This is a detailed feedback comment that meets the minimum length requirement. ' + + 'It includes specific information about the issue, steps to reproduce, expected behavior, and actual behavior. ' + + 'This comment is long enough to pass the 188 character validation rule.' const sentiment = 'Positive' const message = { command: 'submitFeedback', comment: comment, sentiment: sentiment } +const shortComment = 'This is a short comment' +const shortMessage = { command: 'submitFeedback', comment: shortComment, sentiment: sentiment } describe('submitFeedbackListener', function () { let mockTelemetry: TelemetryService @@ -47,5 +52,14 @@ describe('submitFeedbackListener', function () { const result = await webview.submit(message) assert.strictEqual(result, expectedError) }) + + it(`validates ${productName} feedback comment length is at least 188 characters`, async function () { + const postStub = sinon.stub() + mockTelemetry.postFeedback = postStub + const webview = new FeedbackWebview(mockTelemetry, productName) + const result = await webview.submit(shortMessage) + assert.strictEqual(result, 'Please add atleast 100 characters in the template describing your issue.') + assert.strictEqual(postStub.called, false, 'postFeedback should not be called for short comments') + }) } }) diff --git a/packages/core/src/test/index.ts b/packages/core/src/test/index.ts index 9a01973e26d..c682b1f367e 100644 --- a/packages/core/src/test/index.ts +++ b/packages/core/src/test/index.ts @@ -22,6 +22,5 @@ export { getTestWorkspaceFolder } from '../testInteg/integrationTestsUtilities' export * from './codewhisperer/testUtil' export * from './credentials/testUtil' export * from './testUtil' -export * from './amazonq/utils' export * from './fake/mockFeatureConfigData' export * from './shared/ui/testUtils' 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/commands/deleteLambda.test.ts b/packages/core/src/test/lambda/commands/deleteLambda.test.ts index 366d7344ef6..8da82c2c21e 100644 --- a/packages/core/src/test/lambda/commands/deleteLambda.test.ts +++ b/packages/core/src/test/lambda/commands/deleteLambda.test.ts @@ -11,7 +11,7 @@ import { stub } from '../../utilities/stubber' describe('deleteLambda', async function () { function createLambdaClient() { - const client = stub(DefaultLambdaClient, { regionCode: 'region-1' }) + const client = stub(DefaultLambdaClient, { regionCode: 'region-1', userAgent: undefined }) client.deleteFunction.resolves() return client diff --git a/packages/core/src/test/lambda/commands/editLambda.test.ts b/packages/core/src/test/lambda/commands/editLambda.test.ts new file mode 100644 index 00000000000..44d874c14fe --- /dev/null +++ b/packages/core/src/test/lambda/commands/editLambda.test.ts @@ -0,0 +1,307 @@ +/*! + * 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 { + editLambda, + watchForUpdates, + promptForSync, + deployFromTemp, + getReadme, + deleteFilesInFolder, + overwriteChangesForEdit, +} from '../../../lambda/commands/editLambda' +import { LambdaFunction } from '../../../lambda/commands/uploadLambda' +import * as downloadLambda from '../../../lambda/commands/downloadLambda' +import * as uploadLambda from '../../../lambda/commands/uploadLambda' +import * as utils from '../../../lambda/utils' +import * as messages from '../../../shared/utilities/messages' +import fs from '../../../shared/fs/fs' +import { LambdaFunctionNodeDecorationProvider } from '../../../lambda/explorer/lambdaFunctionNodeDecorationProvider' +import path from 'path' +import globals from '../../../shared/extensionGlobals' +import { lambdaTempPath } from '../../../lambda/utils' + +describe('editLambda', function () { + let mockLambda: LambdaFunction + let mockTemp: string + let mockUri: vscode.Uri + + // Stub variables + let getFunctionInfoStub: sinon.SinonStub + let setFunctionInfoStub: sinon.SinonStub + let compareCodeShaStub: sinon.SinonStub + let downloadLambdaStub: sinon.SinonStub + let openLambdaFileStub: sinon.SinonStub + let runUploadDirectoryStub: sinon.SinonStub + let showConfirmationMessageStub: sinon.SinonStub + let createFileSystemWatcherStub: sinon.SinonStub + let existsDirStub: sinon.SinonStub + let mkdirStub: sinon.SinonStub + let promptDeployStub: sinon.SinonStub + let readdirStub: sinon.SinonStub + let readFileTextStub: sinon.SinonStub + let writeFileStub: sinon.SinonStub + let copyStub: sinon.SinonStub + let asAbsolutePathStub: sinon.SinonStub + let deleteStub: sinon.SinonStub + + beforeEach(function () { + mockLambda = { + name: 'test-function', + region: 'us-east-1', + configuration: { + FunctionName: 'test-function', + CodeSha256: 'test-sha', + Runtime: 'nodejs18.x', + }, + } + mockTemp = utils.getTempLocation(mockLambda.name, mockLambda.region) + mockUri = vscode.Uri.file(mockTemp) + + // Create stubs + getFunctionInfoStub = sinon.stub(utils, 'getFunctionInfo').resolves(undefined) + setFunctionInfoStub = sinon.stub(utils, 'setFunctionInfo').resolves() + compareCodeShaStub = sinon.stub(utils, 'compareCodeSha').resolves(true) + downloadLambdaStub = sinon.stub(downloadLambda, 'downloadLambdaInLocation').resolves() + openLambdaFileStub = sinon.stub(downloadLambda, 'openLambdaFile').resolves() + runUploadDirectoryStub = sinon.stub(uploadLambda, 'runUploadDirectory').resolves() + showConfirmationMessageStub = sinon.stub(messages, 'showConfirmationMessage').resolves(true) + createFileSystemWatcherStub = sinon.stub(vscode.workspace, 'createFileSystemWatcher').returns({ + onDidChange: sinon.stub(), + onDidCreate: sinon.stub(), + onDidDelete: sinon.stub(), + dispose: sinon.stub(), + } as any) + existsDirStub = sinon.stub(fs, 'existsDir').resolves(true) + mkdirStub = sinon.stub(fs, 'mkdir').resolves() + readdirStub = sinon.stub(fs, 'readdir').resolves([['file', vscode.FileType.File]]) + promptDeployStub = sinon.stub().resolves(true) + sinon.replace(require('../../../lambda/commands/editLambda'), 'promptDeploy', promptDeployStub) + readFileTextStub = sinon.stub(fs, 'readFileText').resolves('# Lambda Edit README') + writeFileStub = sinon.stub(fs, 'writeFile').resolves() + copyStub = sinon.stub(fs, 'copy').resolves() + asAbsolutePathStub = sinon.stub(globals.context, 'asAbsolutePath').callsFake((p) => `/absolute/${p}`) + deleteStub = sinon.stub(fs, 'delete').resolves() + + // Other stubs + sinon.stub(utils, 'getLambdaDetails').returns({ fileName: 'index.js', functionName: 'test-function' }) + sinon.stub(fs, 'stat').resolves({ ctime: Date.now() } as any) + sinon.stub(vscode.workspace, 'saveAll').resolves(true) + sinon.stub(LambdaFunctionNodeDecorationProvider.prototype, 'addBadge').resolves() + sinon.stub(LambdaFunctionNodeDecorationProvider.prototype, 'removeBadge').resolves() + sinon.stub(LambdaFunctionNodeDecorationProvider, 'getInstance').returns({ + addBadge: sinon.stub().resolves(), + removeBadge: sinon.stub().resolves(), + } as any) + }) + + afterEach(function () { + sinon.restore() + }) + + describe('editLambda', function () { + it('returns early if folder already exists in workspace', async function () { + sinon.stub(vscode.workspace, 'workspaceFolders').value([{ uri: vscode.Uri.file(mockTemp) }]) + + const result = await editLambda(mockLambda) + + assert.strictEqual(result, mockTemp) + }) + + it('downloads lambda when no local code exists', async function () { + await editLambda(mockLambda) + + assert(downloadLambdaStub.calledOnce) + }) + + it('prompts for overwrite when local code differs from remote', async function () { + getFunctionInfoStub.resolves('old-sha') + compareCodeShaStub.resolves(false) + + await editLambda(mockLambda) + + assert(showConfirmationMessageStub.calledOnce) + }) + + it('opens existing file when user declines overwrite', async function () { + getFunctionInfoStub.resolves('old-sha') + compareCodeShaStub.resolves(false) + showConfirmationMessageStub.resolves(false) + + // Specify that it's from the explorer because otherwise there's no need to open + await editLambda(mockLambda, 'explorer') + + assert(openLambdaFileStub.calledOnce) + }) + + it('downloads lambda when directory exists but is empty', async function () { + getFunctionInfoStub.resolves('old-sha') + readdirStub.resolves([]) + + await editLambda(mockLambda) + + assert(downloadLambdaStub.calledOnce) + assert(showConfirmationMessageStub.notCalled) + }) + + it('downloads lambda when directory does not exist', async function () { + getFunctionInfoStub.resolves('old-sha') + existsDirStub.resolves(false) + + await editLambda(mockLambda) + + assert(downloadLambdaStub.calledOnce) + assert(showConfirmationMessageStub.notCalled) + }) + + it('sets up file watcher after download', async function () { + const watcherStub = { + onDidChange: sinon.stub(), + onDidCreate: sinon.stub(), + onDidDelete: sinon.stub(), + } + createFileSystemWatcherStub.returns(watcherStub) + + await editLambda(mockLambda) + + assert(watcherStub.onDidChange.calledOnce) + assert(watcherStub.onDidCreate.calledOnce) + assert(watcherStub.onDidDelete.calledOnce) + }) + }) + + describe('watchForUpdates', function () { + it('creates file system watcher with correct pattern', function () { + const watcher = { + onDidChange: sinon.stub(), + onDidCreate: sinon.stub(), + onDidDelete: sinon.stub(), + } + createFileSystemWatcherStub.returns(watcher) + + watchForUpdates(mockLambda, mockUri) + + assert(createFileSystemWatcherStub.calledOnce) + const pattern = createFileSystemWatcherStub.firstCall.args[0] + assert(pattern instanceof vscode.RelativePattern) + }) + + it('sets up change, create, and delete handlers', function () { + const watcher = { + onDidChange: sinon.stub(), + onDidCreate: sinon.stub(), + onDidDelete: sinon.stub(), + } + createFileSystemWatcherStub.returns(watcher) + + watchForUpdates(mockLambda, mockUri) + + assert(watcher.onDidChange.calledOnce) + assert(watcher.onDidCreate.calledOnce) + assert(watcher.onDidDelete.calledOnce) + }) + }) + + describe('promptForSync', function () { + it('returns early if directory does not exist', async function () { + existsDirStub.resolves(false) + + await promptForSync(mockLambda, mockUri, vscode.Uri.file('/test/file.js')) + + assert(setFunctionInfoStub.notCalled) + }) + }) + + describe('deployFromTemp', function () { + it('uploads without confirmation when code is up to date', async function () { + await deployFromTemp(mockLambda, mockUri) + + assert(showConfirmationMessageStub.notCalled) + assert(runUploadDirectoryStub.calledOnce) + }) + + it('prompts for confirmation when code is outdated', async function () { + compareCodeShaStub.resolves(false) + + await deployFromTemp(mockLambda, mockUri) + + assert(showConfirmationMessageStub.calledOnce) + }) + + it('does not upload when user declines overwrite', async function () { + compareCodeShaStub.resolves(false) + showConfirmationMessageStub.resolves(false) + + await deployFromTemp(mockLambda, mockUri) + + assert(runUploadDirectoryStub.notCalled) + }) + + it('updates function info after successful upload', async function () { + await deployFromTemp(mockLambda, mockUri) + + assert(runUploadDirectoryStub.calledOnce) + assert( + setFunctionInfoStub.calledWith(mockLambda, { + lastDeployed: sinon.match.number, + undeployed: false, + }) + ) + }) + }) + + describe('deleteFilesInFolder', function () { + it('deletes all files in the specified folder', async function () { + readdirStub.resolves([ + ['file1.js', vscode.FileType.File], + ['file2.js', vscode.FileType.File], + ]) + + await deleteFilesInFolder(path.join('test', 'folder')) + + assert(deleteStub.calledTwice) + assert(deleteStub.calledWith(path.join('test', 'folder', 'file1.js'), { recursive: true, force: true })) + assert(deleteStub.calledWith(path.join('test', 'folder', 'file2.js'), { recursive: true, force: true })) + }) + }) + + describe('overwriteChangesForEdit', function () { + it('clears directory and downloads lambda code', async function () { + await overwriteChangesForEdit(mockLambda, mockTemp) + + assert(readdirStub.calledWith(mockTemp)) + assert(downloadLambdaStub.calledWith(mockLambda, 'local', mockTemp)) + assert(setFunctionInfoStub.calledWith(mockLambda, sinon.match.object)) + }) + + it('creates directory if it does not exist', async function () { + existsDirStub.resolves(false) + + await overwriteChangesForEdit(mockLambda, mockTemp) + + assert(mkdirStub.calledWith(mockTemp)) + }) + }) + + describe('getReadme', function () { + it('reads markdown file and writes README.md to temp path', async function () { + const result = await getReadme() + + assert(readFileTextStub.calledOnce) + assert(asAbsolutePathStub.calledWith(path.join('resources', 'markdown', 'lambdaEdit.md'))) + assert(writeFileStub.calledWith(path.join(lambdaTempPath, 'README.md'), '# Lambda Edit README')) + assert.strictEqual(result, path.join(lambdaTempPath, 'README.md')) + }) + + it('copies all required icon files', async function () { + await getReadme() + + assert.strictEqual(copyStub.callCount, 3) + }) + }) +}) diff --git a/packages/core/src/test/lambda/explorer/cloudFormationNodes.test.ts b/packages/core/src/test/lambda/explorer/cloudFormationNodes.test.ts index 8cbeedf25f3..ba8d7ccd516 100644 --- a/packages/core/src/test/lambda/explorer/cloudFormationNodes.test.ts +++ b/packages/core/src/test/lambda/explorer/cloudFormationNodes.test.ts @@ -26,7 +26,7 @@ import { getLabel } from '../../../shared/treeview/utils' const regionCode = 'someregioncode' function createLambdaClient(...functionNames: string[]) { - const client = stub(DefaultLambdaClient, { regionCode }) + const client = stub(DefaultLambdaClient, { regionCode, userAgent: undefined }) client.listFunctions.returns(asyncGenerator(functionNames.map((name) => ({ FunctionName: name })))) return client diff --git a/packages/core/src/test/lambda/explorer/lambdaFunctionFileNode.test.ts b/packages/core/src/test/lambda/explorer/lambdaFunctionFileNode.test.ts new file mode 100644 index 00000000000..59235bf7558 --- /dev/null +++ b/packages/core/src/test/lambda/explorer/lambdaFunctionFileNode.test.ts @@ -0,0 +1,58 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { LambdaFunctionNode } from '../../../lambda/explorer/lambdaFunctionNode' +import { TestAWSTreeNode } from '../../shared/treeview/nodes/testAWSTreeNode' +import { LambdaFunctionFileNode } from '../../../lambda/explorer/lambdaFunctionFileNode' +import path from 'path' + +describe('LambdaFunctionFileNode', function () { + const fakeFunctionConfig = { + FunctionName: 'testFunctionName', + FunctionArn: 'testFunctionARN', + } + const fakeFilename = 'fakeFile' + const fakeRegion = 'fakeRegion' + const functionNode = new LambdaFunctionNode(new TestAWSTreeNode('test node'), fakeRegion, fakeFunctionConfig) + const filePath = path.join( + '/tmp/aws-toolkit-vscode/lambda', + fakeRegion, + fakeFunctionConfig.FunctionName, + fakeFilename + ) + + let testNode: LambdaFunctionFileNode + + before(async function () { + testNode = new LambdaFunctionFileNode(functionNode, fakeFilename, filePath) + }) + + it('instantiates without issue', function () { + assert.ok(testNode) + }) + + it('initializes the parent node', function () { + assert.equal(testNode.parent, functionNode, 'unexpected parent node') + }) + + it('initializes the label', function () { + assert.equal(testNode.label, fakeFilename) + }) + + it('has no children', async function () { + const childNodes = await testNode.getChildren() + assert.ok(childNodes) + assert.strictEqual(childNodes.length, 0, 'Expected zero children') + }) + + it('has correct command', function () { + assert.deepStrictEqual(testNode.command, { + command: 'aws.openLambdaFile', + title: 'Open file', + arguments: [filePath], + }) + }) +}) diff --git a/packages/core/src/test/lambda/explorer/lambdaFunctionFolderNode.test.ts b/packages/core/src/test/lambda/explorer/lambdaFunctionFolderNode.test.ts new file mode 100644 index 00000000000..67471bc4e24 --- /dev/null +++ b/packages/core/src/test/lambda/explorer/lambdaFunctionFolderNode.test.ts @@ -0,0 +1,57 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { LambdaFunctionNode } from '../../../lambda/explorer/lambdaFunctionNode' +import { TestAWSTreeNode } from '../../shared/treeview/nodes/testAWSTreeNode' +import path from 'path' +import { LambdaFunctionFolderNode } from '../../../lambda/explorer/lambdaFunctionFolderNode' +import { fs } from '../../../shared/fs/fs' + +describe('LambdaFunctionFileNode', function () { + const fakeFunctionConfig = { + FunctionName: 'testFunctionName', + FunctionArn: 'testFunctionARN', + } + const fakeRegion = 'fakeRegion' + const fakeSubFolder = 'fakeSubFolder' + const fakeFile = 'fakeFilename' + const functionNode = new LambdaFunctionNode(new TestAWSTreeNode('test node'), fakeRegion, fakeFunctionConfig) + + const regionPath = path.join('/tmp/aws-toolkit-vscode/lambda', fakeRegion) + const functionPath = path.join(regionPath, fakeFunctionConfig.FunctionName) + const subFolderPath = path.join(functionPath, fakeSubFolder) + + let testNode: LambdaFunctionFolderNode + + before(async function () { + await fs.mkdir(subFolderPath) + await fs.writeFile(path.join(subFolderPath, fakeFile), 'fakefilecontent') + + testNode = new LambdaFunctionFolderNode(functionNode, fakeSubFolder, subFolderPath) + }) + + after(async function () { + await fs.delete(regionPath, { recursive: true }) + }) + + it('instantiates without issue', function () { + assert.ok(testNode) + }) + + it('initializes the parent node', function () { + assert.equal(testNode.parent, functionNode, 'unexpected parent node') + }) + + it('initializes the label', function () { + assert.equal(testNode.label, fakeSubFolder) + }) + + it('loads function files', async function () { + const functionFiles = await testNode.loadFunctionFiles() + assert.equal(functionFiles.length, 1) + assert.equal(functionFiles[0].label, fakeFile) + }) +}) diff --git a/packages/core/src/test/lambda/explorer/lambdaFunctionNode.test.ts b/packages/core/src/test/lambda/explorer/lambdaFunctionNode.test.ts index cb9ffc9b5f2..184bdd915b8 100644 --- a/packages/core/src/test/lambda/explorer/lambdaFunctionNode.test.ts +++ b/packages/core/src/test/lambda/explorer/lambdaFunctionNode.test.ts @@ -4,23 +4,54 @@ */ import assert from 'assert' -import { Lambda } from 'aws-sdk' import * as os from 'os' import { LambdaFunctionNode } from '../../../lambda/explorer/lambdaFunctionNode' import { TestAWSTreeNode } from '../../shared/treeview/nodes/testAWSTreeNode' +import path from 'path' +import { fs } from '../../../shared/fs/fs' +import { + contextValueLambdaFunction, + contextValueLambdaFunctionImportable, +} from '../../../lambda/explorer/lambdaFunctionNode' +import sinon from 'sinon' +import * as editLambdaModule from '../../../lambda/commands/editLambda' describe('LambdaFunctionNode', function () { const parentNode = new TestAWSTreeNode('test node') + const fakeRegion = 'fakeRegion' + const fakeFilename = 'fakeFilename' + + const fakeFunctionConfig = { + FunctionName: 'testFunctionName', + FunctionArn: 'testFunctionARN', + } + + const regionPath = path.join('/tmp/aws-toolkit-vscode/lambda', fakeRegion) + const functionPath = path.join(regionPath, fakeFunctionConfig.FunctionName) + const filePath = path.join(functionPath, fakeFilename) + let testNode: LambdaFunctionNode - let fakeFunctionConfig: Lambda.FunctionConfiguration - before(function () { - fakeFunctionConfig = { - FunctionName: 'testFunctionName', - FunctionArn: 'testFunctionARN', - } + let editLambdaStub: sinon.SinonStub + + before(async function () { + await fs.mkdir(functionPath) + await fs.writeFile(filePath, 'fakefilecontent') + + // Stub the editLambdaCommand to return the function path + editLambdaStub = sinon.stub(editLambdaModule, 'editLambdaCommand').resolves(functionPath) + + testNode = new LambdaFunctionNode( + parentNode, + 'someregioncode', + fakeFunctionConfig, + contextValueLambdaFunctionImportable + ) + }) - testNode = new LambdaFunctionNode(parentNode, 'someregioncode', fakeFunctionConfig) + after(async function () { + await fs.delete(regionPath, { recursive: true }) + editLambdaStub.restore() }) it('instantiates without issue', async function () { @@ -43,6 +74,11 @@ describe('LambdaFunctionNode', function () { assert.strictEqual(testNode.functionName, fakeFunctionConfig.FunctionName) }) + it('initializes resourceUri', async function () { + assert.strictEqual(testNode.resourceUri?.scheme, 'lambda') + assert.strictEqual(testNode.resourceUri?.path, `someregioncode/${fakeFunctionConfig.FunctionName}`) + }) + it('initializes the tooltip', async function () { assert.strictEqual( testNode.tooltip, @@ -50,9 +86,27 @@ describe('LambdaFunctionNode', function () { ) }) - it('has no children', async function () { + it('loads function files', async function () { + const functionFiles = await testNode.loadFunctionFiles(functionPath) + assert.equal(functionFiles.length, 1) + assert.equal(functionFiles[0].label, fakeFilename) + }) + + it('has child if importable', async function () { const childNodes = await testNode.getChildren() assert.ok(childNodes) - assert.strictEqual(childNodes.length, 0, 'Expected node to have no children') + assert.equal(childNodes.length, 1, 'Expected node to have one child, should be "failed to load resources"') + }) + + it('is not collapsible if not importable', async function () { + const nonImportableNode = new LambdaFunctionNode( + parentNode, + fakeRegion, + fakeFunctionConfig, + contextValueLambdaFunction + ) + const childNodes = await nonImportableNode.getChildren() + assert.ok(childNodes) + assert.equal(childNodes.length, 0, 'Expected node to have no children') }) }) diff --git a/packages/core/src/test/lambda/explorer/lambdaFunctionNodeDecorationProvider.test.ts b/packages/core/src/test/lambda/explorer/lambdaFunctionNodeDecorationProvider.test.ts new file mode 100644 index 00000000000..19a2662815f --- /dev/null +++ b/packages/core/src/test/lambda/explorer/lambdaFunctionNodeDecorationProvider.test.ts @@ -0,0 +1,147 @@ +/*! + * 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 path from 'path' +import { LambdaFunctionNodeDecorationProvider } from '../../../lambda/explorer/lambdaFunctionNodeDecorationProvider' +import * as utils from '../../../lambda/utils' +import { fs } from '../../../shared/fs/fs' + +describe('LambdaFunctionNodeDecorationProvider', function () { + let provider: LambdaFunctionNodeDecorationProvider + let getFunctionInfoStub: sinon.SinonStub + let fsStatStub: sinon.SinonStub + let fsReaddirStub: sinon.SinonStub + + const filepath = path.join(utils.getTempLocation('test-function', 'us-east-1'), 'index.js') + const functionUri = vscode.Uri.parse('lambda:us-east-1/test-function') + const fileUri = vscode.Uri.file(filepath) + + beforeEach(function () { + provider = LambdaFunctionNodeDecorationProvider.getInstance() + getFunctionInfoStub = sinon.stub(utils, 'getFunctionInfo') + fsStatStub = sinon.stub(fs, 'stat') + fsReaddirStub = sinon.stub(fs, 'readdir') + }) + + afterEach(function () { + sinon.restore() + }) + + describe('provideFileDecoration', function () { + it('returns decoration for lambda URI with undeployed changes', async function () { + getFunctionInfoStub.resolves(true) + + const decoration = await provider.provideFileDecoration(functionUri) + + assert.ok(decoration) + assert.strictEqual(decoration.badge, 'M') + assert.strictEqual(decoration.tooltip, 'This function has undeployed changes') + assert.strictEqual(decoration.propagate, false) + }) + + it('returns undefined for lambda URI without undeployed changes', async function () { + getFunctionInfoStub.resolves(false) + + const decoration = await provider.provideFileDecoration(functionUri) + + assert.strictEqual(decoration, undefined) + }) + + it('returns decoration for file URI with modifications after deployment', async function () { + const lastDeployed = 1 + const fileModified = 2 + + getFunctionInfoStub.resolves({ lastDeployed, undeployed: true }) + fsStatStub.resolves({ mtime: fileModified }) + + const decoration = await provider.provideFileDecoration(fileUri) + + assert.ok(decoration) + assert.strictEqual(decoration.badge, 'M') + assert.strictEqual(decoration.tooltip, 'This function has undeployed changes') + assert.strictEqual(decoration.propagate, true) + }) + + it('returns undefined for file URI without modifications after deployment', async function () { + const lastDeployed = 2 + const fileModified = 1 + + getFunctionInfoStub.resolves({ lastDeployed, undeployed: true }) + fsStatStub.resolves({ mtime: fileModified }) + + const decoration = await provider.provideFileDecoration(fileUri) + + assert.strictEqual(decoration, undefined) + }) + + it('returns undefined for file URI when no deployment info exists', async function () { + getFunctionInfoStub.resolves(undefined) + + const decoration = await provider.provideFileDecoration(fileUri) + + assert.strictEqual(decoration, undefined) + }) + + it('returns undefined for file URI that does not match lambda pattern', async function () { + const uri = vscode.Uri.file(path.join('not', 'in', 'tmp')) + + const decoration = await provider.provideFileDecoration(uri) + + assert.strictEqual(decoration, undefined) + }) + + it('handles errors gracefully when checking file modification', async function () { + getFunctionInfoStub.resolves(0) + fsStatStub.rejects(new Error('File not found')) + + const decoration = await provider.provideFileDecoration(fileUri) + + assert.strictEqual(decoration, undefined) + }) + }) + + describe('addBadge', function () { + it('fires decoration change events for both URIs', async function () { + const fileUri = vscode.Uri.file(path.join('test', 'file.js')) + const functionUri = vscode.Uri.parse('lambda:us-east-1/test-function') + + let eventCount = 0 + const disposable = provider.onDidChangeFileDecorations(() => { + eventCount++ + }) + + await provider.addBadge(fileUri, functionUri) + + assert.strictEqual(eventCount, 2) + disposable.dispose() + }) + }) + + describe('getFilePaths', function () { + it('returns all file paths recursively', async function () { + const basePath = path.join('test', 'dir') + + // Mock first readdir call + fsReaddirStub.onFirstCall().resolves([ + ['file1.js', vscode.FileType.File], + ['subdir', vscode.FileType.Directory], + ]) + + // Mock second readdir call for subdirectory + fsReaddirStub.onSecondCall().resolves([['file2.js', vscode.FileType.File]]) + + // Access private method through any cast for testing + const paths = await (provider as any).getFilePaths(basePath) + + assert.ok(paths.includes(basePath)) + assert.ok(paths.includes(path.join('test', 'dir', 'file1.js'))) + assert.ok(paths.includes(path.join('test', 'dir', 'subdir'))) + assert.ok(paths.includes(path.join('test', 'dir', 'subdir', 'file2.js'))) + }) + }) +}) diff --git a/packages/core/src/test/lambda/explorer/lambdaNodes.test.ts b/packages/core/src/test/lambda/explorer/lambdaNodes.test.ts index ba494680911..86ed7bbe44c 100644 --- a/packages/core/src/test/lambda/explorer/lambdaNodes.test.ts +++ b/packages/core/src/test/lambda/explorer/lambdaNodes.test.ts @@ -4,8 +4,8 @@ */ import assert from 'assert' -import { LambdaFunctionNode } from '../../../lambda/explorer/lambdaFunctionNode' -import { contextValueLambdaFunction, LambdaNode } from '../../../lambda/explorer/lambdaNodes' +import { contextValueLambdaFunction, LambdaFunctionNode } from '../../../lambda/explorer/lambdaFunctionNode' +import { LambdaNode } from '../../../lambda/explorer/lambdaNodes' import { asyncGenerator } from '../../../shared/utilities/collectionUtils' import { assertNodeListOnlyHasErrorNode, @@ -17,7 +17,7 @@ import { DefaultLambdaClient } from '../../../shared/clients/lambdaClient' const regionCode = 'someregioncode' function createLambdaClient(...functionNames: string[]) { - const client = stub(DefaultLambdaClient, { regionCode }) + const client = stub(DefaultLambdaClient, { regionCode, userAgent: undefined }) client.listFunctions.returns(asyncGenerator(functionNames.map((name) => ({ FunctionName: name })))) return client 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..566c465a0dd 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) { diff --git a/packages/core/src/test/lambda/models/samTemplates.test.ts b/packages/core/src/test/lambda/models/samTemplates.test.ts index 4abb3f87315..7fe78e630c8 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,7 @@ describe('getSamTemplateWizardOption', function () { case 'python3.10': case 'python3.11': case 'python3.12': - case 'python3.13': + case 'python3.13' 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 new file mode 100644 index 00000000000..98734e51833 --- /dev/null +++ b/packages/core/src/test/lambda/remoteDebugging/ldkClient.test.ts @@ -0,0 +1,565 @@ +/*! + * 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 { FunctionConfiguration } from '@aws-sdk/client-lambda' +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' + +describe('LdkClient', () => { + let sandbox: sinon.SinonSandbox + let ldkClient: LdkClient + let mockLambdaClient: any + let mockIoTSTClient: AwsStub + let mockLocalProxy: any + + beforeEach(() => { + sandbox = sinon.createSandbox() + + // Mock Lambda client + mockLambdaClient = { + getFunction: sandbox.stub(), + updateFunctionConfiguration: sandbox.stub(), + publishVersion: sandbox.stub(), + deleteFunction: sandbox.stub(), + } + sandbox.stub(utils, 'getLambdaClientWithAgent').returns(mockLambdaClient) + + mockIoTSTClient = mockClient(IoTSecureTunnelingClient) + sandbox.stub(utils, 'getIoTSTClientWithAgent').returns(mockIoTSTClient as any) + + // 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) + + // Mock global state + const stateStorage = new Map() + const mockGlobalState = { + get: (key: string) => stateStorage.get(key), + update: async (key: string, value: any) => { + stateStorage.set(key, value) + return Promise.resolve() + }, + } + sandbox.stub(globals, 'globalState').value(mockGlobalState) + + // Mock telemetry util + sandbox.stub(telemetryUtil, 'getClientId').returns('test-client-id') + ldkClient = LdkClient.instance + ldkClient.dispose() + }) + + afterEach(() => { + sandbox.restore() + }) + + describe('Singleton Pattern', () => { + it('should return the same instance', () => { + const instance1 = LdkClient.instance + const instance2 = LdkClient.instance + assert.strictEqual(instance1, instance2, 'Should return the same singleton instance') + }) + }) + + describe('dispose()', () => { + it('should dispose resources properly', () => { + // Set up a mock local proxy + ;(ldkClient as any).localProxy = mockLocalProxy + + ldkClient.dispose() + + assert(mockLocalProxy.stop.calledOnce, 'Should stop local proxy') + assert.strictEqual((ldkClient as any).localProxy, undefined, 'Should clear local proxy reference') + }) + + it('should clear client caches', () => { + // Add some clients to cache + ;(ldkClient as any).lambdaClientCache.set('us-east-1', mockLambdaClient) + ;(ldkClient as any).lambdaClientCache.set('us-west-2', mockLambdaClient) + + assert.strictEqual((ldkClient as any).lambdaClientCache.size, 2, 'Should have cached clients') + + ldkClient.dispose() + + assert.strictEqual((ldkClient as any).lambdaClientCache.size, 0, 'Should clear Lambda client cache') + }) + }) + + describe('createOrReuseTunnel()', () => { + it('should create new tunnel when none exists', async () => { + mockIoTSTClient.on(ListTunnelsCommand).resolves({ tunnelSummaries: [] }) + mockIoTSTClient.on(OpenTunnelCommand).resolves({ + tunnelId: 'tunnel-123', + sourceAccessToken: 'source-token', + destinationAccessToken: 'dest-token', + }) + + const result = await ldkClient.createOrReuseTunnel('us-east-1') + + assert(result, 'Should return tunnel info') + assert.strictEqual(result?.tunnelID, 'tunnel-123') + assert.strictEqual(result?.sourceToken, 'source-token') + assert.strictEqual(result?.destinationToken, 'dest-token') + 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: TunnelStatus.OPEN, + createdAt: new Date(Date.now() - 60 * 60 * 1000), // 1 hour ago + } + + mockIoTSTClient.on(ListTunnelsCommand).resolves({ tunnelSummaries: [existingTunnel] }) + mockIoTSTClient.on(RotateTunnelAccessTokenCommand).resolves({ + sourceAccessToken: 'rotated-source-token', + destinationAccessToken: 'rotated-dest-token', + }) + + const result = await ldkClient.createOrReuseTunnel('us-east-1') + + assert(result, 'Should return tunnel info') + assert.strictEqual(result?.tunnelID, 'existing-tunnel') + assert.strictEqual(result?.sourceToken, 'rotated-source-token') + assert.strictEqual(result?.destinationToken, 'rotated-dest-token') + }) + + it('should handle tunnel creation errors', async () => { + mockIoTSTClient.on(ListTunnelsCommand).resolves({ tunnelSummaries: [] }) + mockIoTSTClient.on(OpenTunnelCommand).rejects(new Error('Tunnel creation failed')) + + await assert.rejects( + async () => await ldkClient.createOrReuseTunnel('us-east-1'), + /Error creating\/reusing tunnel/, + 'Should throw error on tunnel creation failure' + ) + }) + }) + + describe('refreshTunnelTokens()', () => { + it('should refresh tunnel tokens successfully', async () => { + mockIoTSTClient.on(RotateTunnelAccessTokenCommand).resolves({ + sourceAccessToken: 'new-source-token', + destinationAccessToken: 'new-dest-token', + }) + + const result = await ldkClient.refreshTunnelTokens('tunnel-123', 'us-east-1') + + assert(result, 'Should return tunnel info') + assert.strictEqual(result?.tunnelID, 'tunnel-123') + assert.strictEqual(result?.sourceToken, 'new-source-token') + assert.strictEqual(result?.destinationToken, 'new-dest-token') + }) + + it('should handle token refresh errors', async () => { + mockIoTSTClient.on(RotateTunnelAccessTokenCommand).rejects(new Error('Token refresh failed')) + + await assert.rejects( + async () => await ldkClient.refreshTunnelTokens('tunnel-123', 'us-east-1'), + /Error refreshing tunnel tokens/, + 'Should throw error on token refresh failure' + ) + }) + }) + + describe('getFunctionDetail()', () => { + const mockFunctionConfig: FunctionConfiguration = createMockFunctionConfig({ + FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:testFunction', + }) + + it('should get function details successfully', async () => { + mockLambdaClient.getFunction.resolves({ Configuration: mockFunctionConfig }) + + const result = await ldkClient.getFunctionDetail(mockFunctionConfig.FunctionArn!) + + assert.deepStrictEqual(result, mockFunctionConfig, 'Should return function configuration') + }) + + it('should handle function details retrieval errors', async () => { + mockLambdaClient.getFunction.reset() + mockLambdaClient.getFunction.rejects(new Error('Function not found')) + + const result = await ldkClient.getFunctionDetail(mockFunctionConfig.FunctionArn!) + + assert.strictEqual(result, undefined, 'Should return undefined on error') + }) + + it('should handle invalid ARN', async () => { + const result = await ldkClient.getFunctionDetail('invalid-arn') + + assert.strictEqual(result, undefined, 'Should return undefined for invalid ARN') + }) + }) + + describe('createDebugDeployment()', () => { + const mockFunctionConfig: FunctionConfiguration = createMockFunctionConfig({ + FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:testFunction', + }) + + const mockProgress = createMockProgress() + + beforeEach(() => { + mockLambdaClient.updateFunctionConfiguration.resolves({}) + mockLambdaClient.publishVersion.resolves({ Version: 'v1' }) + }) + + it('should create debug deployment successfully without version publishing', async () => { + const result = await ldkClient.createDebugDeployment( + mockFunctionConfig, + 'dest-token', + 900, + false, + 'layer-arn', + mockProgress as any + ) + + assert.strictEqual(result, '$Latest', 'Should return $Latest for non-version deployment') + assert(mockLambdaClient.updateFunctionConfiguration.calledOnce, 'Should update function configuration') + assert(mockLambdaClient.publishVersion.notCalled, 'Should not publish version') + }) + + it('should create debug deployment with version publishing', async () => { + const result = await ldkClient.createDebugDeployment( + mockFunctionConfig, + 'dest-token', + 900, + true, + 'layer-arn', + mockProgress as any + ) + + assert.strictEqual(result, 'v1', 'Should return version number') + assert(mockLambdaClient.publishVersion.calledOnce, 'Should publish version') + }) + + it('should handle deployment errors', async () => { + mockLambdaClient.updateFunctionConfiguration.reset() + mockLambdaClient.updateFunctionConfiguration.rejects(new Error('Update failed')) + + await assert.rejects( + async () => + await ldkClient.createDebugDeployment( + mockFunctionConfig, + 'dest-token', + 900, + false, + 'layer-arn', + mockProgress as any + ), + /Failed to create debug deployment/, + 'Should throw error on deployment failure' + ) + }) + + it('should handle missing function ARN', async () => { + const configWithoutArn = { ...mockFunctionConfig, FunctionArn: undefined } + + await assert.rejects( + async () => + await ldkClient.createDebugDeployment( + configWithoutArn, + 'dest-token', + 900, + false, + 'layer-arn', + mockProgress as any + ), + /Function ARN is missing/, + 'Should throw error for missing ARN' + ) + }) + }) + + describe('removeDebugDeployment()', () => { + const mockFunctionConfig: FunctionConfiguration = createMockFunctionConfig({ + FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:testFunction', + }) + + beforeEach(() => { + mockLambdaClient.updateFunctionConfiguration.resolves({}) + }) + + it('should remove debug deployment successfully', async () => { + const result = await ldkClient.removeDebugDeployment(mockFunctionConfig, false) + + assert.strictEqual(result, true, 'Should return true on successful removal') + assert(mockLambdaClient.updateFunctionConfiguration.calledOnce, 'Should update function configuration') + }) + + it('should handle removal errors', async () => { + mockLambdaClient.updateFunctionConfiguration.rejects(new Error('Update failed')) + + await assert.rejects( + async () => await ldkClient.removeDebugDeployment(mockFunctionConfig, false), + /Error removing debug deployment/, + 'Should throw error on removal failure' + ) + }) + + it('should handle missing function ARN', async () => { + const configWithoutArn = { ...mockFunctionConfig, FunctionArn: undefined, FunctionName: undefined } + + await assert.rejects( + async () => await ldkClient.removeDebugDeployment(configWithoutArn, false), + /Error removing debug deployment/, + 'Should throw error for missing ARN' + ) + }) + }) + + describe('deleteDebugVersion()', () => { + it('should delete debug version successfully', async () => { + mockLambdaClient.deleteFunction.resolves({}) + + const result = await ldkClient.deleteDebugVersion( + 'arn:aws:lambda:us-east-1:123456789012:function:testFunction', + 'v1' + ) + + assert.strictEqual(result, true, 'Should return true on successful deletion') + assert(mockLambdaClient.deleteFunction.calledOnce, 'Should call deleteFunction') + }) + + it('should handle version deletion errors', async () => { + mockLambdaClient.deleteFunction.rejects(new Error('Delete failed')) + + const result = await ldkClient.deleteDebugVersion( + 'arn:aws:lambda:us-east-1:123456789012:function:testFunction', + 'v1' + ) + + assert.strictEqual(result, false, 'Should return false on deletion error') + }) + + it('should handle invalid ARN for version deletion', async () => { + const result = await ldkClient.deleteDebugVersion('invalid-arn', 'v1') + + assert.strictEqual(result, false, 'Should return false for invalid ARN') + }) + }) + + describe('startProxy()', () => { + beforeEach(() => { + mockLocalProxy.start.resolves(9229) + mockLocalProxy.stop.returns() + }) + + it('should start proxy successfully', async () => { + const result = await ldkClient.startProxy('us-east-1', 'source-token', 9229) + + assert.strictEqual(result, true, 'Should return true on successful start') + assert( + mockLocalProxy.start.calledWith('us-east-1', 'source-token', 9229), + 'Should start proxy with correct parameters' + ) + }) + + it('should stop existing proxy before starting new one', async () => { + // Create a spy for the stop method + const stopSpy = sandbox.spy() + + // Set up existing proxy with the spy + ;(ldkClient as any).localProxy = { stop: stopSpy } + + await ldkClient.startProxy('us-east-1', 'source-token', 9229) + + assert(stopSpy.called, 'Should stop existing proxy') + }) + + it('should handle proxy start errors', async () => { + mockLocalProxy.start.rejects(new Error('Proxy start failed')) + + await assert.rejects( + async () => await ldkClient.startProxy('us-east-1', 'source-token', 9229), + /Failed to start proxy/, + 'Should throw error on proxy start failure' + ) + }) + }) + + describe('stopProxy()', () => { + it('should stop proxy successfully', async () => { + // Set up existing proxy + ;(ldkClient as any).localProxy = { stop: mockLocalProxy.stop } + + const result = await ldkClient.stopProxy() + + assert.strictEqual(result, true, 'Should return true on successful stop') + assert(mockLocalProxy.stop.calledOnce, 'Should stop proxy') + assert.strictEqual((ldkClient as any).localProxy, undefined, 'Should clear proxy reference') + }) + + it('should handle stopping when no proxy exists', async () => { + const result = await ldkClient.stopProxy() + + assert.strictEqual(result, true, 'Should return true when no proxy to stop') + }) + }) + + describe('Client User-Agent', () => { + 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 getUserAgent at the telemetryUtil level to return a known value + const getUserAgentStub = sandbox.stub(telemetryUtil, 'getUserAgent') + getUserAgentStub.returns('test-user-agent') + + // Stub the sdkClientBuilderV3 to capture the client options + let capturedClientOptions: any + const createAwsServiceStub = sandbox.stub(globals.sdkClientBuilderV3, 'createAwsService') + createAwsServiceStub.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(createAwsServiceStub.called, 'Should call createAwsService') + assert.strictEqual(capturedClientOptions.clientOptions.region, 'us-east-1', 'Should use correct region') + assert.deepStrictEqual( + capturedClientOptions.clientOptions.userAgent, + [['LAMBDA-DEBUG/1.0.0 test-user-agent']], + 'Should include correct user-agent 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 getUserAgent to return a known value + const getUserAgentStub = sandbox.stub(telemetryUtil, 'getUserAgent') + getUserAgentStub.returns('test-user-agent') + + // Stub the sdkClientBuilderV3 to capture the client options + let capturedClientOptions: any + const createAwsServiceStub = sandbox.stub(globals.sdkClientBuilderV3, 'createAwsService') + createAwsServiceStub.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(createAwsServiceStub.calledOnce, 'Should call createAwsService once') + assert.strictEqual(capturedClientOptions.clientOptions.region, 'us-east-1', 'Should use correct region') + assert.deepStrictEqual( + capturedClientOptions.clientOptions.userAgent, + [['LAMBDA-DEBUG/1.0.0 test-user-agent']], + 'Should include correct user-agent with LAMBDA-DEBUG prefix' + ) + }) + }) +}) + +describe('Helper Functions', () => { + describe('getRegionFromArn', () => { + it('should extract region from valid ARN', () => { + const arn = 'arn:aws:lambda:us-east-1:123456789012:function:testFunction' + const result = getRegionFromArn(arn) + assert.strictEqual(result, 'us-east-1', 'Should extract region correctly') + }) + + it('should handle undefined ARN', () => { + const result = getRegionFromArn(undefined) + assert.strictEqual(result, undefined, 'Should return undefined for undefined ARN') + }) + + it('should handle invalid ARN format', () => { + const result = getRegionFromArn('invalid-arn') + assert.strictEqual(result, undefined, 'Should return undefined for invalid ARN') + }) + + it('should handle ARN with insufficient parts', () => { + const result = getRegionFromArn('arn:aws:lambda') + assert.strictEqual(result, undefined, 'Should return undefined for ARN with insufficient parts') + }) + }) + + describe('isTunnelInfo', () => { + it('should validate correct tunnel info', () => { + const tunnelInfo = { + tunnelID: 'tunnel-123', + sourceToken: 'source-token', + destinationToken: 'dest-token', + } + const result = isTunnelInfo(tunnelInfo) + assert.strictEqual(result, true, 'Should validate correct tunnel info') + }) + + it('should reject invalid tunnel info', () => { + const invalidTunnelInfo = { + tunnelID: 'tunnel-123', + sourceToken: 'source-token', + // missing destinationToken + } + const result = isTunnelInfo(invalidTunnelInfo as any) + assert.strictEqual(result, false, 'Should reject invalid tunnel info') + }) + + it('should reject non-object types', () => { + assert.strictEqual(isTunnelInfo('string' as any), false, 'Should reject string') + assert.strictEqual(isTunnelInfo(123 as any), false, 'Should reject number') + assert.strictEqual(isTunnelInfo(undefined as any), false, 'Should reject undefined') + }) + }) +}) diff --git a/packages/core/src/test/lambda/remoteDebugging/ldkController.test.ts b/packages/core/src/test/lambda/remoteDebugging/ldkController.test.ts new file mode 100644 index 00000000000..040264a9ff8 --- /dev/null +++ b/packages/core/src/test/lambda/remoteDebugging/ldkController.test.ts @@ -0,0 +1,805 @@ +/*! + * 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 sinon, { SinonStubbedInstance, createStubInstance } from 'sinon' +import { FunctionConfiguration } from '@aws-sdk/client-lambda' +import { + RemoteDebugController, + activateRemoteDebugging, + revertExistingConfig, + tryAutoDetectOutFile, +} 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' +import { getOpenExternalStub } from '../../globalSetup.test' +import { assertTelemetry } from '../../testUtil' +import { + createMockFunctionConfig, + createMockDebugConfig, + createMockGlobalState, + setupMockLdkClientOperations, + setupMockVSCodeDebugAPIs, + setupMockRevertExistingConfig, + setupDebuggingState, + setupMockCleanupOperations, +} from './testUtils' +import { getRemoteDebugLayer } from '../../../lambda/remoteDebugging/remoteLambdaDebugger' +import { fs } from '../../../shared/fs/fs' +import * as detectCdkProjects from '../../../awsService/cdk/explorer/detectCdkProjects' + +describe('RemoteDebugController', () => { + let sandbox: sinon.SinonSandbox + let mockLdkClient: SinonStubbedInstance + let controller: RemoteDebugController + let mockGlobalState: any + + beforeEach(() => { + sandbox = sinon.createSandbox() + + // 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) + + // Get controller instance + controller = RemoteDebugController.instance + + // Ensure clean state + controller.ensureCleanState() + }) + + afterEach(() => { + sandbox.restore() + }) + + describe('Singleton Pattern', () => { + it('should return the same instance', () => { + const instance1 = RemoteDebugController.instance + const instance2 = RemoteDebugController.instance + assert.strictEqual(instance1, instance2, 'Should return the same singleton instance') + }) + }) + + describe('State Management', () => { + it('should initialize with clean state', () => { + controller.ensureCleanState() + + assert.strictEqual(controller.isDebugging, false, 'Should not be debugging initially') + assert.strictEqual(controller.qualifier, undefined, 'Qualifier should be undefined initially') + }) + + it('should clean up disposables on ensureCleanState', () => { + // Set up some mock disposables + const mockDisposable = { dispose: sandbox.stub() } + ;(controller as any).debugSessionDisposables.set('test-arn', mockDisposable) + + controller.ensureCleanState() + + assert(mockDisposable.dispose.calledOnce, 'Should dispose existing disposables') + assert.strictEqual((controller as any).debugSessionDisposables.size, 0, 'Should clear disposables map') + }) + }) + + describe('Runtime Support Checks', () => { + it('should support code download for node and python runtimes', () => { + assert.strictEqual(controller.supportCodeDownload('nodejs18.x'), true, 'Should support Node.js') + assert.strictEqual(controller.supportCodeDownload('python3.9'), true, 'Should support Python') + assert.strictEqual( + controller.supportCodeDownload('java11'), + false, + 'Should not support Java for code download' + ) + 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') + assert.strictEqual(controller.supportRuntimeRemoteDebug('java11'), true, 'Should support Java') + assert.strictEqual(controller.supportRuntimeRemoteDebug('dotnet6'), false, 'Should not support .NET') + assert.strictEqual( + controller.supportRuntimeRemoteDebug(undefined), + false, + 'Should not support undefined runtime' + ) + }) + + it('should get remote debug layer for supported regions and architectures', () => { + 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') + assert(result?.includes('LDKLayerX86'), 'Should contain the x86 layer name') + }) + + it('should return undefined for unsupported regions', () => { + 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(getRemoteDebugLayer(undefined, ['x86_64']), undefined) + assert.strictEqual(getRemoteDebugLayer('us-west-2', undefined), undefined) + }) + }) + + describe('Extension Installation', () => { + it('should return true when extension is already installed', async () => { + // Mock VSCode extensions API - return extension as already installed + const mockExtension = { id: 'ms-vscode.js-debug', isActive: true } + sandbox.stub(vscode.extensions, 'getExtension').returns(mockExtension as any) + + const result = await controller.installDebugExtension('nodejs18.x') + + assert.strictEqual(result, true, 'Should return true when extension is already installed') + }) + + it('should return true when extension installation succeeds', async () => { + // Mock extension as not installed initially, then installed after command + const getExtensionStub = sandbox.stub(vscode.extensions, 'getExtension') + getExtensionStub.onFirstCall().returns(undefined) // Not installed initially + getExtensionStub.onSecondCall().returns({ isActive: true } as any) // Installed after command + + sandbox.stub(vscode.commands, 'executeCommand').resolves() + sandbox.stub(messages, 'showConfirmationMessage').resolves(true) + + const result = await controller.installDebugExtension('python3.9') + + assert.strictEqual(result, true, 'Should return true when installation succeeds') + }) + + it('should return false when user cancels extension installation', async () => { + // Mock extension as not installed + sandbox.stub(vscode.extensions, 'getExtension').returns(undefined) + sandbox.stub(messages, 'showConfirmationMessage').resolves(false) + + const result = await controller.installDebugExtension('python3.9') + + assert.strictEqual(result, false, 'Should return false when user cancels') + }) + + it('should handle Java runtime workflow', async () => { + // Mock extension as already installed to skip extension installation + const mockExtension = { id: 'redhat.java', isActive: true } + sandbox.stub(vscode.extensions, 'getExtension').returns(mockExtension as any) + + // Mock no Java path found + sandbox.stub(require('../../../shared/utilities/pathFind'), 'findJavaPath').resolves(undefined) + + // Mock user choosing to install JVM + const showConfirmationStub = sandbox.stub(messages, 'showConfirmationMessage').resolves(true) + + // Mock openExternal to prevent actual URL opening + // sandbox.stub(vscode.env, 'openExternal').resolves(true) + getOpenExternalStub().resolves(true) + const result = await controller.installDebugExtension('java11') + + assert.strictEqual(result, false, 'Should return false to allow user to install JVM') + assert(showConfirmationStub.calledOnce, 'Should show JVM installation dialog') + }) + + it('should throw error for undefined runtime', async () => { + await assert.rejects( + async () => await controller.installDebugExtension(undefined), + /Runtime is undefined/, + 'Should throw error for undefined runtime' + ) + }) + }) + + describe('Debug Session Management', () => { + let mockConfig: DebugConfig + let mockFunctionConfig: FunctionConfiguration + + beforeEach(() => { + mockConfig = createMockDebugConfig({ + layerArn: 'arn:aws:lambda:us-west-2:123456789012:layer:LDKLayerX86:6', + }) + + mockFunctionConfig = createMockFunctionConfig() + }) + + it('should start debugging successfully', async () => { + // Mock VSCode APIs + setupMockVSCodeDebugAPIs(sandbox) + + // Mock runtime support + sandbox.stub(controller, 'supportRuntimeRemoteDebug').returns(true) + + // Mock successful LdkClient operations + setupMockLdkClientOperations(mockLdkClient, mockFunctionConfig) + + // Mock revertExistingConfig + setupMockRevertExistingConfig(sandbox) + + await controller.startDebugging(mockConfig.functionArn, 'nodejs18.x', mockConfig) + + // Assert state changes + assert.strictEqual(controller.isDebugging, true, 'Should be in debugging state') + // Qualifier is only set for version publishing, not for $LATEST + assert.strictEqual(controller.qualifier, undefined, 'Should not set qualifier for $LATEST') + + // Verify LdkClient calls + assert(mockLdkClient.getFunctionDetail.calledWith(mockConfig.functionArn), 'Should get function details') + assert(mockLdkClient.createOrReuseTunnel.calledOnce, 'Should create tunnel') + assert(mockLdkClient.createDebugDeployment.calledOnce, 'Should create debug deployment') + assert(mockLdkClient.startProxy.calledOnce, 'Should start proxy') + + 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","isLambdaRemote":true}', + runtimeString: 'nodejs18.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 success but tunnel creation failure + setupMockLdkClientOperations(mockLdkClient, mockFunctionConfig) + mockLdkClient.createOrReuseTunnel.rejects(new Error('Tunnel creation failed')) + + // Mock revertExistingConfig + setupMockRevertExistingConfig(sandbox) + + let errorThrown = false + try { + await controller.startDebugging(mockConfig.functionArn, 'nodejs18.x', mockConfig) + } catch (error) { + errorThrown = true + assert(error instanceof Error, 'Should throw an error') + assert( + error.message.includes('Error StartDebugging') || error.message.includes('Tunnel creation failed'), + 'Should throw relevant error' + ) + } + + assert(errorThrown, 'Should have thrown an error') + + // Assert state is cleaned up + assert.strictEqual(controller.isDebugging, false, 'Should not be in debugging state after failure') + assert(mockLdkClient.stopProxy.calledOnce, 'Should attempt cleanup') + }) + + it('should handle version publishing workflow', async () => { + // Mock VSCode APIs + setupMockVSCodeDebugAPIs(sandbox) + + // Mock runtime support + sandbox.stub(controller, 'supportRuntimeRemoteDebug').returns(true) + + const versionConfig = { ...mockConfig, shouldPublishVersion: true } + + // Mock successful LdkClient operations with version publishing + setupMockLdkClientOperations(mockLdkClient, mockFunctionConfig) + mockLdkClient.createDebugDeployment.resolves('v1') + + // Mock revertExistingConfig + setupMockRevertExistingConfig(sandbox) + + await controller.startDebugging(versionConfig.functionArn, 'nodejs18.x', versionConfig) + + assert.strictEqual(controller.isDebugging, true, 'Should be in debugging state') + assert.strictEqual(controller.qualifier, 'v1', 'Should set version qualifier') + // Verify telemetry was emitted with version action + 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","isLambdaRemote":true}', + runtimeString: 'nodejs18.x', + }) + }) + + it('should prevent multiple debugging sessions', async () => { + // Set controller to already debugging + controller.isDebugging = true + + await controller.startDebugging(mockConfig.functionArn, 'nodejs18.x', mockConfig) + + // Should not call LdkClient methods + assert(mockLdkClient.getFunctionDetail.notCalled, 'Should not start new session') + }) + }) + + describe('Stop Debugging', () => { + it('should stop debugging successfully', async () => { + // Mock VSCode APIs + sandbox.stub(vscode.commands, 'executeCommand').resolves() + + // Set up debugging state + await setupDebuggingState(controller, mockGlobalState) + + // Mock successful cleanup operations + setupMockCleanupOperations(mockLdkClient) + + await controller.stopDebugging() + + // Assert state is cleaned up + assert.strictEqual(controller.isDebugging, false, 'Should not be in debugging state') + + // Verify cleanup operations + assert(mockLdkClient.stopProxy.calledOnce, 'Should stop proxy') + assert(mockLdkClient.removeDebugDeployment.calledOnce, 'Should remove debug deployment') + assert(mockLdkClient.deleteDebugVersion.calledOnce, 'Should delete debug version') + assertTelemetry('lambda_remoteDebugStop', { + result: 'Succeeded', + }) + }) + + it('should handle stop debugging when not debugging', async () => { + controller.isDebugging = false + + await controller.stopDebugging() + + // Should complete without error when not debugging + assert.strictEqual(controller.isDebugging, false, 'Should remain not debugging') + }) + + it('should handle cleanup errors gracefully', async () => { + // Mock VSCode APIs + sandbox.stub(vscode.commands, 'executeCommand').resolves() + + controller.isDebugging = true + + const mockFunctionConfig = { + FunctionName: 'testFunction', + FunctionArn: 'arn:aws:lambda:us-west-2:123456789012:function:testFunction', + } + // Set up the snapshot in mock state + await mockGlobalState.update('aws.lambda.remoteDebugSnapshot', mockFunctionConfig) + + // Mock cleanup failure + mockLdkClient.stopProxy.rejects(new Error('Cleanup failed')) + mockLdkClient.removeDebugDeployment.resolves(true) + + await assert.rejects( + async () => await controller.stopDebugging(), + /error when stopping remote debug/, + 'Should throw error on cleanup failure' + ) + + // State should still be cleaned up + assert.strictEqual(controller.isDebugging, false, 'Should clean up state even on error') + // Verify telemetry was emitted for failure + assertTelemetry('lambda_remoteDebugStop', { + result: 'Failed', + }) + }) + }) + + describe('Snapshot Management', () => { + it('should get lambda snapshot from global state', async () => { + const mockSnapshot = { + FunctionName: 'testFunction', + FunctionArn: 'arn:aws:lambda:us-west-2:123456789012:function:testFunction', + } + // Set up the snapshot in mock state + await mockGlobalState.update('aws.lambda.remoteDebugSnapshot', mockSnapshot) + + const result = getLambdaSnapshot() + + assert.deepStrictEqual(result, mockSnapshot, 'Should return snapshot from global state') + }) + + it('should return undefined when no snapshot exists', () => { + const result = getLambdaSnapshot() + + assert.strictEqual(result, undefined, 'Should return undefined when no snapshot') + }) + }) + + describe('Telemetry Verification', () => { + let mockConfig: DebugConfig + let mockFunctionConfig: FunctionConfiguration + + beforeEach(() => { + mockConfig = createMockDebugConfig({ + layerArn: 'arn:aws:lambda:us-west-2:123456789012:layer:LDKLayerX86:6', + }) + + mockFunctionConfig = createMockFunctionConfig() + }) + + it('should emit lambda_remoteDebugStart telemetry for failed debugging start', async () => { + // Mock VSCode APIs + setupMockVSCodeDebugAPIs(sandbox) + + // Mock runtime support + sandbox.stub(controller, 'supportRuntimeRemoteDebug').returns(true) + + // Mock function config retrieval success but tunnel creation failure + setupMockLdkClientOperations(mockLdkClient, mockFunctionConfig) + mockLdkClient.createOrReuseTunnel.rejects(new Error('Tunnel creation failed')) + + // Mock revertExistingConfig + setupMockRevertExistingConfig(sandbox) + + try { + await controller.startDebugging(mockConfig.functionArn, 'nodejs18.x', mockConfig) + } catch (error) { + // Expected to throw + } + + // Verify telemetry was emitted for failure + 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","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('Module Functions', () => { + let sandbox: sinon.SinonSandbox + let mockGlobalState: any + + beforeEach(() => { + sandbox = sinon.createSandbox() + + // Mock global state with actual storage + mockGlobalState = createMockGlobalState() + sandbox.stub(globals, 'globalState').value(mockGlobalState) + }) + + afterEach(() => { + sandbox.restore() + }) + + describe('activateRemoteDebugging', () => { + it('should activate remote debugging and ensure clean state', async () => { + // Mock revertExistingConfig + sandbox + .stub(require('../../../lambda/remoteDebugging/ldkController'), 'revertExistingConfig') + .resolves(true) + + // Mock controller + const mockController = { + ensureCleanState: sandbox.stub(), + } + sandbox.stub(RemoteDebugController, 'instance').get(() => mockController) + + await activateRemoteDebugging() + + assert(mockController.ensureCleanState.calledOnce, 'Should ensure clean state') + }) + + it('should handle activation errors gracefully', async () => { + // Mock revertExistingConfig to throw error + sandbox + .stub(require('../../../lambda/remoteDebugging/ldkController'), 'revertExistingConfig') + .rejects(new Error('Revert failed')) + + // Should not throw error, just handle gracefully + await activateRemoteDebugging() + + // Test passes if no error is thrown + assert(true, 'Should handle activation errors gracefully') + }) + }) + + describe('revertExistingConfig', () => { + let mockLdkClient: SinonStubbedInstance + + beforeEach(() => { + mockLdkClient = createStubInstance(LdkClient) + sandbox.stub(LdkClient, 'instance').get(() => mockLdkClient) + }) + + it('should return true when no existing config', async () => { + // mockGlobalState.get.returns(undefined) + + const result = await revertExistingConfig() + + assert.strictEqual(result, true, 'Should return true when no config to revert') + }) + + it('should revert existing config successfully', async () => { + const mockSnapshot = { + FunctionName: 'testFunction', + FunctionArn: 'arn:aws:lambda:us-west-2:123456789012:function:testFunction', + Timeout: 30, + } + const mockCurrentConfig = { + FunctionName: 'testFunction', + FunctionArn: 'arn:aws:lambda:us-west-2:123456789012:function:testFunction', + Timeout: 900, // Different from snapshot + } + + // Set up the snapshot in mock state + await mockGlobalState.update('aws.lambda.remoteDebugSnapshot', mockSnapshot) + mockLdkClient.getFunctionDetail.resolves(mockCurrentConfig) + mockLdkClient.removeDebugDeployment.resolves(true) + + const showConfirmationStub = sandbox.stub(messages, 'showConfirmationMessage').resolves(true) + const result = await revertExistingConfig() + + assert.strictEqual(result, true, 'Should return true on successful revert') + assert(showConfirmationStub.calledOnce, 'Should show confirmation dialog') + assert(mockLdkClient.removeDebugDeployment.calledWith(mockSnapshot, false), 'Should revert config') + }) + + it('should handle user cancellation of revert', async () => { + const mockSnapshot = { + FunctionName: 'testFunction', + FunctionArn: 'arn:aws:lambda:us-west-2:123456789012:function:testFunction', + } + const mockCurrentConfig = { + FunctionName: 'testFunction', + FunctionArn: 'arn:aws:lambda:us-west-2:123456789012:function:testFunction', + Timeout: 900, + } + + // Set up the snapshot in mock state + await mockGlobalState.update('aws.lambda.remoteDebugSnapshot', mockSnapshot) + mockLdkClient.getFunctionDetail.resolves(mockCurrentConfig) + + sandbox.stub(messages, 'showConfirmationMessage').resolves(false) + + const result = await revertExistingConfig() + + assert.strictEqual(result, true, 'Should return true when user cancels') + // Verify snapshot was cleared + assert.strictEqual( + mockGlobalState.get('aws.lambda.remoteDebugSnapshot'), + undefined, + 'Should clear snapshot' + ) + }) + + it('should handle corrupted snapshot gracefully', async () => { + const corruptedSnapshot = { + // Missing FunctionArn and FunctionName + Timeout: 30, + } + + // Set up corrupted snapshot in mock state + await mockGlobalState.update('aws.lambda.remoteDebugSnapshot', corruptedSnapshot) + + const result = await revertExistingConfig() + + assert.strictEqual(result, true, 'Should return true for corrupted snapshot') + // Verify snapshot was cleared + assert.strictEqual( + mockGlobalState.get('aws.lambda.remoteDebugSnapshot'), + undefined, + 'Should clear corrupted snapshot' + ) + }) + + it('should handle revert errors', async () => { + const mockSnapshot = { + FunctionName: 'testFunction', + FunctionArn: 'arn:aws:lambda:us-west-2:123456789012:function:testFunction', + } + + // Set up the snapshot in mock state + await mockGlobalState.update('aws.lambda.remoteDebugSnapshot', mockSnapshot) + mockLdkClient.getFunctionDetail.rejects(new Error('Failed to get function')) + + await assert.rejects( + async () => await revertExistingConfig(), + /Error in revertExistingConfig/, + 'Should throw error on revert failure' + ) + }) + }) +}) diff --git a/packages/core/src/test/lambda/remoteDebugging/localProxy.test.ts b/packages/core/src/test/lambda/remoteDebugging/localProxy.test.ts new file mode 100644 index 00000000000..7c1bd0479b4 --- /dev/null +++ b/packages/core/src/test/lambda/remoteDebugging/localProxy.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 sinon from 'sinon' +import WebSocket from 'ws' +import { LocalProxy } from '../../../lambda/remoteDebugging/localProxy' + +describe('LocalProxy', () => { + let sandbox: sinon.SinonSandbox + let localProxy: LocalProxy + + beforeEach(() => { + sandbox = sinon.createSandbox() + localProxy = new LocalProxy() + }) + + afterEach(() => { + localProxy.stop() + sandbox.restore() + }) + + describe('Constructor', () => { + it('should initialize with default values', () => { + const proxy = new LocalProxy() + assert.strictEqual((proxy as any).isConnected, false, 'Should not be connected initially') + assert.strictEqual((proxy as any).reconnectAttempts, 0, 'Should have zero reconnect attempts') + assert.strictEqual((proxy as any).currentStreamId, 1, 'Should start with stream ID 1') + assert.strictEqual((proxy as any).nextConnectionId, 1, 'Should start with connection ID 1') + }) + }) + + describe('Protobuf Loading', () => { + it('should load protobuf definition successfully', async () => { + const proxy = new LocalProxy() + await (proxy as any).loadProtobufDefinition() + + assert((proxy as any).Message, 'Should load Message type') + assert.strictEqual(typeof (proxy as any).Message, 'object', 'Message should be a protobuf Type object') + assert.strictEqual((proxy as any).Message.constructor.name, 'Type', 'Message should be a protobuf Type') + }) + + it('should not reload protobuf definition if already loaded', async () => { + const proxy = new LocalProxy() + await (proxy as any).loadProtobufDefinition() + const firstMessage = (proxy as any).Message + + await (proxy as any).loadProtobufDefinition() + const secondMessage = (proxy as any).Message + + assert.strictEqual(firstMessage, secondMessage, 'Should not reload protobuf definition') + }) + }) + + describe('TCP Server Management', () => { + it('should close TCP server and connections properly', () => { + const mockSocket = { + removeAllListeners: sandbox.stub(), + destroy: sandbox.stub(), + } + + const mockServer = { + removeAllListeners: sandbox.stub(), + close: sandbox.stub().callsArg(0), + } + + // Set up mock state + ;(localProxy as any).tcpServer = mockServer + ;(localProxy as any).tcpConnections = new Map([[1, { socket: mockSocket }]]) + ;(localProxy as any).closeTcpServer() + + assert(mockSocket.removeAllListeners.called, 'Should remove socket listeners') + assert(mockSocket.destroy.calledOnce, 'Should destroy socket') + assert(mockServer.removeAllListeners.called, 'Should remove server listeners') + assert(mockServer.close.calledOnce, 'Should close server') + }) + }) + + describe('WebSocket Connection Management', () => { + it('should create WebSocket with correct URL and headers', async () => { + const mockWs = { + on: sandbox.stub(), + once: sandbox.stub(), + readyState: WebSocket.OPEN, + removeAllListeners: sandbox.stub(), + close: sandbox.stub(), + terminate: sandbox.stub(), + } + + // Set up LocalProxy with required properties + ;(localProxy as any).region = 'us-east-1' + ;(localProxy as any).accessToken = 'test-access-token' + + // Mock the WebSocket constructor + const WebSocketStub = sandbox.stub().returns(mockWs) + sandbox.stub(WebSocket, 'WebSocket').callsFake(WebSocketStub) + + // Mock the open event to resolve the promise + mockWs.on.withArgs('open').callsArg(1) + + await (localProxy as any).connectWebSocket() + + assert(WebSocketStub.calledOnce, 'Should create WebSocket') + const [url, protocols, options] = WebSocketStub.getCall(0).args + + assert(url.includes('wss://data.tunneling.iot.'), 'Should use correct WebSocket URL') + assert(url.includes('.amazonaws.com:443/tunnel'), 'Should use correct WebSocket URL') + assert(url.includes('local-proxy-mode=source'), 'Should set local proxy mode') + assert.deepStrictEqual(protocols, ['aws.iot.securetunneling-3.0'], 'Should use correct protocol') + assert(options && options.headers && options.headers['access-token'], 'Should include access token header') + assert(options && options.headers && options.headers['client-token'], 'Should include client token header') + }) + + it('should handle WebSocket connection errors', async () => { + const mockWs = { + on: sandbox.stub(), + once: sandbox.stub(), + readyState: WebSocket.CONNECTING, + removeAllListeners: sandbox.stub(), + close: sandbox.stub(), + terminate: sandbox.stub(), + } + + sandbox.stub(WebSocket, 'WebSocket').returns(mockWs) + + // Mock the error event + mockWs.on.withArgs('error').callsArgWith(1, new Error('Connection failed')) + + await assert.rejects( + async () => await (localProxy as any).connectWebSocket(), + /Connection failed/, + 'Should throw error on WebSocket connection failure' + ) + }) + + it('should close WebSocket connection properly', () => { + const mockWs = { + readyState: WebSocket.OPEN, + removeAllListeners: sandbox.stub(), + close: sandbox.stub(), + terminate: sandbox.stub(), + once: sandbox.stub(), + } + + ;(localProxy as any).ws = mockWs + ;(localProxy as any).closeWebSocket() + + assert(mockWs.removeAllListeners.called, 'Should remove all listeners') + assert(mockWs.close.calledWith(1000, 'Normal Closure'), 'Should close with normal closure code') + }) + + it('should terminate WebSocket if not open', () => { + const mockWs = { + readyState: WebSocket.CONNECTING, + removeAllListeners: sandbox.stub(), + close: sandbox.stub(), + terminate: sandbox.stub(), + } + + ;(localProxy as any).ws = mockWs + ;(localProxy as any).closeWebSocket() + + assert(mockWs.terminate.calledOnce, 'Should terminate WebSocket if not open') + }) + }) + + describe('Ping/Pong Management', () => { + it('should start ping interval', () => { + const setIntervalStub = sandbox.stub(global, 'setInterval').returns({} as any) + + ;(localProxy as any).startPingInterval() + + assert(setIntervalStub.calledOnce, 'Should start ping interval') + assert.strictEqual(setIntervalStub.getCall(0).args[1], 30000, 'Should ping every 30 seconds') + }) + + it('should stop ping interval', () => { + const clearIntervalStub = sandbox.stub(global, 'clearInterval') + const mockInterval = {} as any + ;(localProxy as any).pingInterval = mockInterval + ;(localProxy as any).stopPingInterval() + + assert(clearIntervalStub.calledWith(mockInterval), 'Should clear ping interval') + assert.strictEqual((localProxy as any).pingInterval, undefined, 'Should clear interval reference') + }) + + it('should send ping when WebSocket is open', () => { + const mockWs = { + readyState: WebSocket.OPEN, + ping: sandbox.stub(), + } + + ;(localProxy as any).ws = mockWs + + // Simulate ping interval callback + const setIntervalStub = sandbox.stub(global, 'setInterval') + ;(localProxy as any).startPingInterval() + + const pingCallback = setIntervalStub.getCall(0).args[0] + pingCallback() + + assert(mockWs.ping.calledOnce, 'Should send ping') + }) + }) + + describe('Message Processing', () => { + beforeEach(async () => { + // Load protobuf definition + await (localProxy as any).loadProtobufDefinition() + }) + + it('should process binary WebSocket messages', () => { + const processMessageStub = sandbox.stub(localProxy as any, 'processMessage') + + // Create a mock message buffer with length prefix + const messageData = Buffer.from('test message') + const buffer = Buffer.alloc(2 + messageData.length) + buffer.writeUInt16BE(messageData.length, 0) + messageData.copy(buffer, 2) + ;(localProxy as any).handleWebSocketMessage(buffer) + + assert(processMessageStub.calledOnce, 'Should process message') + assert(processMessageStub.calledWith(messageData), 'Should pass correct message data') + }) + + it('should handle incomplete message data', () => { + const processMessageStub = sandbox.stub(localProxy as any, 'processMessage') + + // Create incomplete buffer (only length prefix) + const buffer = Buffer.alloc(2) + buffer.writeUInt16BE(100, 0) // Claims 100 bytes but buffer is only 2 + ;(localProxy as any).handleWebSocketMessage(buffer) + + assert(processMessageStub.notCalled, 'Should not process incomplete message') + }) + + it('should handle non-buffer WebSocket messages', () => { + const processMessageStub = sandbox.stub(localProxy as any, 'processMessage') + + ;(localProxy as any).handleWebSocketMessage('string message') + + assert(processMessageStub.notCalled, 'Should not process non-buffer messages') + }) + }) + + describe('TCP Connection Handling', () => { + beforeEach(() => { + ;(localProxy as any).isConnected = true + ;(localProxy as any).isDisposed = false + }) + + it('should handle new TCP connections when connected', () => { + const mockSocket = { + on: sandbox.stub(), + destroy: sandbox.stub(), + once: sandbox.stub(), + } + + const sendStreamStartStub = sandbox.stub(localProxy as any, 'sendStreamStart') + + ;(localProxy as any).handleNewTcpConnection(mockSocket) + + assert(mockSocket.on.calledWith('data'), 'Should listen for data events') + assert(mockSocket.on.calledWith('error'), 'Should listen for error events') + assert(mockSocket.on.calledWith('close'), 'Should listen for close events') + assert(sendStreamStartStub.calledOnce, 'Should send stream start for first connection') + }) + + it('should reject TCP connections when not connected', () => { + ;(localProxy as any).isConnected = false + + const mockSocket = { + destroy: sandbox.stub(), + } + + ;(localProxy as any).handleNewTcpConnection(mockSocket) + + assert(mockSocket.destroy.calledOnce, 'Should destroy socket when not connected') + }) + + it('should reject TCP connections when disposed', () => { + ;(localProxy as any).isDisposed = true + + const mockSocket = { + destroy: sandbox.stub(), + } + + ;(localProxy as any).handleNewTcpConnection(mockSocket) + + assert(mockSocket.destroy.calledOnce, 'Should destroy socket when disposed') + }) + + it('should send connection start for subsequent connections', () => { + ;(localProxy as any).nextConnectionId = 2 // Second connection + + const mockSocket = { + on: sandbox.stub(), + destroy: sandbox.stub(), + once: sandbox.stub(), + } + + const sendConnectionStartStub = sandbox.stub(localProxy as any, 'sendConnectionStart') + + ;(localProxy as any).handleNewTcpConnection(mockSocket) + + assert(sendConnectionStartStub.calledOnce, 'Should send connection start for subsequent connections') + }) + }) + + describe('Lifecycle Management', () => { + it('should start proxy successfully', async () => { + const startTcpServerStub = sandbox.stub(localProxy as any, 'startTcpServer').resolves(9229) + const connectWebSocketStub = sandbox.stub(localProxy as any, 'connectWebSocket').resolves() + + const port = await localProxy.start('us-east-1', 'source-token', 9229) + + assert.strictEqual(port, 9229, 'Should return assigned port') + assert(startTcpServerStub.calledWith(9229), 'Should start TCP server') + assert(connectWebSocketStub.calledOnce, 'Should connect WebSocket') + assert.strictEqual((localProxy as any).region, 'us-east-1', 'Should store region') + assert.strictEqual((localProxy as any).accessToken, 'source-token', 'Should store access token') + }) + + it('should handle start errors and cleanup', async () => { + sandbox.stub(localProxy as any, 'startTcpServer').resolves(9229) + sandbox.stub(localProxy as any, 'connectWebSocket').rejects(new Error('WebSocket failed')) + const stopStub = sandbox.stub(localProxy, 'stop') + + await assert.rejects( + async () => await localProxy.start('us-east-1', 'source-token', 9229), + /WebSocket failed/, + 'Should throw error on start failure' + ) + + assert(stopStub.calledOnce, 'Should cleanup on start failure') + }) + + it('should stop proxy and cleanup resources', () => { + const stopPingIntervalStub = sandbox.stub(localProxy as any, 'stopPingInterval') + const closeWebSocketStub = sandbox.stub(localProxy as any, 'closeWebSocket') + const closeTcpServerStub = sandbox.stub(localProxy as any, 'closeTcpServer') + + // Set up some state + ;(localProxy as any).isConnected = true + ;(localProxy as any).reconnectAttempts = 5 + ;(localProxy as any).clientToken = 'test-token' + + localProxy.stop() + + assert(stopPingIntervalStub.calledOnce, 'Should stop ping interval') + assert(closeWebSocketStub.calledOnce, 'Should close WebSocket') + assert(closeTcpServerStub.calledOnce, 'Should close TCP server') + assert.strictEqual((localProxy as any).isConnected, false, 'Should reset connection state') + assert.strictEqual((localProxy as any).reconnectAttempts, 0, 'Should reset reconnect attempts') + assert.strictEqual((localProxy as any).clientToken, '', 'Should clear client token') + assert.strictEqual((localProxy as any).isDisposed, true, 'Should mark as disposed') + }) + + it('should handle duplicate stop calls gracefully', () => { + const stopPingIntervalStub = sandbox.stub(localProxy as any, 'stopPingInterval') + + localProxy.stop() + localProxy.stop() // Second call + + // Should not throw error and should handle gracefully + assert(stopPingIntervalStub.calledOnce, 'Should only stop once') + }) + }) + + describe('Message Sending', () => { + beforeEach(async () => { + await (localProxy as any).loadProtobufDefinition() + }) + + it('should send messages when WebSocket is open', () => { + const mockWs = { + readyState: WebSocket.OPEN, + send: sandbox.stub(), + } + + ;(localProxy as any).ws = mockWs + ;(localProxy as any).serviceId = 'WSS' + ;(localProxy as any).sendMessage(1, 1, 1, Buffer.from('test')) + + assert(mockWs.send.calledOnce, 'Should send message') + const sentData = mockWs.send.getCall(0).args[0] + assert(Buffer.isBuffer(sentData), 'Should send buffer data') + assert(sentData.length > 2, 'Should include length prefix') + }) + + it('should not send messages when WebSocket is not open', () => { + const mockWs = { + readyState: WebSocket.CONNECTING, + send: sandbox.stub(), + } + + ;(localProxy as any).ws = mockWs + ;(localProxy as any).sendMessage(1, 1, 1, Buffer.from('test')) + + assert(mockWs.send.notCalled, 'Should not send when WebSocket is not open') + }) + + it('should split large data into chunks', () => { + const mockWs = { + readyState: WebSocket.OPEN, + send: sandbox.stub(), + } + + ;(localProxy as any).ws = mockWs + + // Create data larger than max chunk size (63KB) + const largeData = Buffer.alloc(70 * 1024, 'a') + + ;(localProxy as any).sendData(1, 1, largeData) + + assert(mockWs.send.calledTwice, 'Should split large data into chunks') + }) + }) +}) 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 new file mode 100644 index 00000000000..42deaa4ec96 --- /dev/null +++ b/packages/core/src/test/lambda/remoteDebugging/testUtils.ts @@ -0,0 +1,176 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import sinon from 'sinon' +import { Architecture, FunctionConfiguration, Runtime, SnapStartApplyOn } from '@aws-sdk/client-lambda' +import { LambdaFunctionNode } from '../../../lambda/explorer/lambdaFunctionNode' +import { InitialData } from '../../../lambda/vue/remoteInvoke/invokeLambda' +import type { DebugConfig } from '../../../lambda/remoteDebugging/lambdaDebugger' + +/** + * Creates a mock Lambda function configuration for testing + */ +export function createMockFunctionConfig(overrides: Partial = {}): FunctionConfiguration { + return { + FunctionName: 'testFunction', + FunctionArn: 'arn:aws:lambda:us-west-2:123456789012:function:testFunction', + Runtime: Runtime.nodejs18x, + Handler: 'index.handler', + Timeout: 30, + Layers: [], + Environment: { Variables: {} }, + Architectures: [Architecture.x86_64], + SnapStart: { ApplyOn: SnapStartApplyOn.None }, + ...overrides, + } +} + +/** + * Creates a mock Lambda function node for testing + */ +export function createMockFunctionNode(overrides: Partial = {}): LambdaFunctionNode { + const config = createMockFunctionConfig() + return { + configuration: config, + regionCode: 'us-west-2', + localDir: '/local/path', + ...overrides, + } as LambdaFunctionNode +} + +/** + * Creates mock initial data for RemoteInvokeWebview testing + */ +export function createMockInitialData(overrides: Partial = {}): InitialData { + const mockFunctionNode = createMockFunctionNode() + return { + FunctionName: 'testFunction', + FunctionArn: 'arn:aws:lambda:us-west-2:123456789012:function:testFunction', + FunctionRegion: 'us-west-2', + InputSamples: [], + Runtime: 'nodejs18.x', + LocalRootPath: '/local/path', + LambdaFunctionNode: mockFunctionNode, + supportCodeDownload: true, + runtimeSupportsRemoteDebug: true, + regionSupportsRemoteDebug: true, + ...overrides, + } as InitialData +} + +/** + * Creates a mock debug configuration for testing + */ +export function createMockDebugConfig(overrides: Partial = {}): DebugConfig { + return { + functionArn: 'arn:aws:lambda:us-west-2:123456789012:function:testFunction', + functionName: 'testFunction', + port: 9229, + localRoot: '/local/path', + remoteRoot: '/var/task', + skipFiles: [], + shouldPublishVersion: false, + lambdaTimeout: 900, + layerArn: 'arn:aws:lambda:us-west-2:123456789012:layer:LDKLayerX86:6', + isLambdaRemote: true, + ...overrides, + } +} + +/** + * Creates a mock global state for testing + */ +export function createMockGlobalState(): any { + const stateStorage = new Map() + return { + get: (key: string) => stateStorage.get(key), + tryGet: (key: string, type?: any, defaultValue?: any) => { + const value = stateStorage.get(key) + return value !== undefined ? value : defaultValue + }, + update: async (key: string, value: any) => { + stateStorage.set(key, value) + return Promise.resolve() + }, + } +} + +/** + * Sets up common mocks for VSCode APIs + */ +export function setupVSCodeMocks(sandbox: sinon.SinonSandbox) { + return { + startDebugging: sandbox.stub(), + executeCommand: sandbox.stub(), + onDidTerminateDebugSession: sandbox.stub().returns({ dispose: sandbox.stub() }), + } +} + +/** + * Creates a mock progress reporter for testing + */ +export function createMockProgress(): any { + return { + report: sinon.stub(), + } +} + +/** + * Sets up common debugging state for stop debugging tests + */ +export function setupDebuggingState(controller: any, mockGlobalState: any, qualifier: string = 'v1') { + controller.isDebugging = true + controller.qualifier = qualifier + ;(controller as any).lastDebugStartTime = Date.now() - 5000 // 5 seconds ago + + const mockFunctionConfig = { + FunctionName: 'testFunction', + FunctionArn: 'arn:aws:lambda:us-west-2:123456789012:function:testFunction', + } + + return mockGlobalState.update('aws.lambda.remoteDebugSnapshot', mockFunctionConfig) +} + +/** + * Sets up common mock operations for successful cleanup + */ +export function setupMockCleanupOperations(mockLdkClient: any) { + mockLdkClient.stopProxy.resolves(true) + mockLdkClient.removeDebugDeployment.resolves(true) + mockLdkClient.deleteDebugVersion.resolves(true) +} + +/** + * Sets up common mock operations for LdkClient testing + */ +export function setupMockLdkClientOperations(mockLdkClient: any, mockFunctionConfig: any) { + mockLdkClient.getFunctionDetail.resolves(mockFunctionConfig) + mockLdkClient.createOrReuseTunnel.resolves({ + tunnelID: 'tunnel-123', + sourceToken: 'source-token', + destinationToken: 'dest-token', + }) + mockLdkClient.createDebugDeployment.resolves('$LATEST') + mockLdkClient.startProxy.resolves(true) + mockLdkClient.stopProxy.resolves(true) + mockLdkClient.removeDebugDeployment.resolves(true) + mockLdkClient.deleteDebugVersion.resolves(true) +} + +/** + * Sets up common VSCode debug API mocks + */ +export function setupMockVSCodeDebugAPIs(sandbox: sinon.SinonSandbox) { + sandbox.stub(require('vscode').debug, 'startDebugging').resolves(true) + sandbox.stub(require('vscode').commands, 'executeCommand').resolves() + sandbox.stub(require('vscode').debug, 'onDidTerminateDebugSession').returns({ dispose: sandbox.stub() }) +} + +/** + * Sets up mock for revertExistingConfig function + */ +export function setupMockRevertExistingConfig(sandbox: sinon.SinonSandbox) { + return sandbox.stub(require('../../../lambda/remoteDebugging/ldkController'), 'revertExistingConfig').resolves(true) +} diff --git a/packages/core/src/test/lambda/uriHandlers.test.ts b/packages/core/src/test/lambda/uriHandlers.test.ts new file mode 100644 index 00000000000..f3e8ae7c368 --- /dev/null +++ b/packages/core/src/test/lambda/uriHandlers.test.ts @@ -0,0 +1,36 @@ +/*! + * 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 { parseOpenParams } from '../../lambda/uriHandlers' +import { globals } from '../../shared' + +describe('Lambda URI Handler', function () { + describe('load-function', function () { + it('registers for "/lambda/load-function"', function () { + assert.throws(() => globals.uriHandler.onPath('/lambda/load-function', () => {})) + }) + + it('parses parameters', function () { + let query = new SearchParams({ + functionName: 'example', + }) + assert.throws(() => parseOpenParams(query), /A region must be provided/) + query = new SearchParams({ + region: 'example', + }) + assert.throws(() => parseOpenParams(query), /A function name must be provided/) + + const valid = { + functionName: 'example', + region: 'example', + isCfn: 'false', + } + query = new SearchParams(valid) + assert.deepEqual(parseOpenParams(query), valid) + }) + }) +}) diff --git a/packages/core/src/test/lambda/utils.test.ts b/packages/core/src/test/lambda/utils.test.ts index b7c1edb0aa4..7a8e82043cf 100644 --- a/packages/core/src/test/lambda/utils.test.ts +++ b/packages/core/src/test/lambda/utils.test.ts @@ -4,9 +4,28 @@ */ import assert from 'assert' -import { getLambdaDetails } from '../../lambda/utils' +import * as sinon from 'sinon' +import { + getLambdaDetails, + getTempLocation, + getTempRegionLocation, + getFunctionInfo, + setFunctionInfo, + compareCodeSha, +} from '../../lambda/utils' +import { LambdaFunction } from '../../lambda/commands/uploadLambda' +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', async function () { +describe('lambda utils', function () { + const mockLambda = { + name: 'test-function', + region: 'us-east-1', + configuration: { FunctionName: 'test-function' }, + } describe('getLambdaDetails', function () { it('returns valid filenames and function names', function () { const jsNonNestedParsedName = getLambdaDetails({ @@ -49,7 +68,126 @@ describe('lambda utils', async 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' })) + }) + }) + + describe('getTempLocation', function () { + it('returns correct temp location path', function () { + const result = getTempLocation('test-function', 'us-east-1') + const expected = path.join(tempDirPath, 'lambda', 'us-east-1', 'test-function') + assert.strictEqual(result, expected) + }) + }) + + describe('getTempRegionLocation', function () { + it('returns correct temp region path', function () { + const result = getTempRegionLocation('us-west-2') + const expected = path.join(tempDirPath, 'lambda', 'us-west-2') + assert.strictEqual(result, expected) + }) + }) + + describe('getFunctionInfo', function () { + afterEach(function () { + sinon.restore() + }) + + it('returns parsed data when file exists', async function () { + const mockData = { lastDeployed: 123456, undeployed: false, sha: 'test-sha' } + sinon.stub(fs, 'readFileText').resolves(JSON.stringify(mockData)) + + const result = await getFunctionInfo(mockLambda) + assert.deepStrictEqual(result, mockData) + }) + + it('returns specific field when requested', async function () { + const mockData = { lastDeployed: 123456, undeployed: false, sha: 'test-sha' } + sinon.stub(fs, 'readFileText').resolves(JSON.stringify(mockData)) + + const result = await getFunctionInfo(mockLambda, 'sha') + assert.strictEqual(result, 'test-sha') + }) + + it('returns empty object when file does not exist', async function () { + sinon.stub(fs, 'readFileText').rejects(new Error('File not found')) + + const result = await getFunctionInfo(mockLambda) + assert.deepStrictEqual(result, {}) + }) + }) + + describe('setFunctionInfo', function () { + let mockLambda: LambdaFunction + + // jscpd:ignore-start + beforeEach(function () { + mockLambda = { + name: 'test-function', + region: 'us-east-1', + configuration: { FunctionName: 'test-function' }, + } + }) + + afterEach(function () { + sinon.restore() + }) + // jscpd:ignore-end + + it('merges with existing data', async function () { + const existingData = { lastDeployed: 123456, undeployed: true, sha: 'old-sha', handlerFile: 'index.js' } + sinon.stub(fs, 'readFileText').resolves(JSON.stringify(existingData)) + const writeStub = sinon.stub(fs, 'writeFile').resolves() + sinon.stub(DefaultLambdaClient.prototype, 'getFunction').resolves({ + Configuration: { CodeSha256: 'new-sha' }, + } as any) + + await setFunctionInfo(mockLambda, { undeployed: false }) + + assert(writeStub.calledOnce) + const writtenData = JSON.parse(writeStub.firstCall.args[1] as string) + assert.strictEqual(writtenData.lastDeployed, 123456) + assert.strictEqual(writtenData.undeployed, false) + assert.strictEqual(writtenData.sha, 'new-sha') + assert.strictEqual(writtenData.handlerFile, 'index.js') + }) + }) + + describe('compareCodeSha', function () { + let mockLambda: LambdaFunction + + // jscpd:ignore-start + beforeEach(function () { + mockLambda = { + name: 'test-function', + region: 'us-east-1', + configuration: { FunctionName: 'test-function' }, + } + }) + + afterEach(function () { + sinon.restore() + }) + // jscpd:ignore-end + + it('returns true when local and remote SHA match', async function () { + sinon.stub(fs, 'readFileText').resolves(JSON.stringify({ sha: 'same-sha' })) + sinon.stub(DefaultLambdaClient.prototype, 'getFunction').resolves({ + Configuration: { CodeSha256: 'same-sha' }, + } as any) + + const result = await compareCodeSha(mockLambda) + assert.strictEqual(result, true) + }) + + it('returns false when local and remote SHA differ', async function () { + sinon.stub(fs, 'readFileText').resolves(JSON.stringify({ sha: 'local-sha' })) + sinon.stub(DefaultLambdaClient.prototype, 'getFunction').resolves({ + Configuration: { CodeSha256: 'remote-sha' }, + } as any) + + const result = await compareCodeSha(mockLambda) + assert.strictEqual(result, false) }) }) }) 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 2a0fcaa0e0d..4d2aaa6c507 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) @@ -42,7 +44,7 @@ describe('RemoteInvokeWebview', () => { InputSamples: [], } as InitialData - remoteInvokeWebview = new RemoteInvokeWebview(outputChannel, client, data) + remoteInvokeWebview = new RemoteInvokeWebview(outputChannel, client, client, data) }) describe('init', () => { it('should return the data property', () => { @@ -61,8 +63,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[] = [] @@ -87,8 +89,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 +113,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 +152,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 +242,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 +673,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 +694,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 +817,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 @@ -471,7 +915,8 @@ describe('RemoteInvokeWebview', () => { createWebviewPanelArgs[1], `Invoke Lambda ${mockFunctionNode.configuration.FunctionName}` ) - assert.deepStrictEqual(createWebviewPanelArgs[2], { viewColumn: -1 }) + // opens in side panel + assert.deepStrictEqual(createWebviewPanelArgs[2], { viewColumn: vscode.ViewColumn.Beside }) }) }) }) diff --git a/packages/core/src/test/lambda/vue/remoteInvoke/invokeLambdaDebugging.test.ts b/packages/core/src/test/lambda/vue/remoteInvoke/invokeLambdaDebugging.test.ts new file mode 100644 index 00000000000..79f7863cd09 --- /dev/null +++ b/packages/core/src/test/lambda/vue/remoteInvoke/invokeLambdaDebugging.test.ts @@ -0,0 +1,581 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { RemoteInvokeWebview, InitialData } from '../../../../lambda/vue/remoteInvoke/invokeLambda' +import { LambdaClient, DefaultLambdaClient } from '../../../../shared/clients/lambdaClient' +import * as vscode from 'vscode' +import sinon, { SinonStubbedInstance, createStubInstance } from 'sinon' +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' +import * as uploadLambda from '../../../../lambda/commands/uploadLambda' +import * as appBuilderUtils from '../../../../awsService/appBuilder/utils' +import * as messages from '../../../../shared/utilities/messages' +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 + let client: SinonStubbedInstance + let remoteInvokeWebview: RemoteInvokeWebview + let data: InitialData + let sandbox: sinon.SinonSandbox + let mockDebugController: SinonStubbedInstance + let mockFunctionNode: LambdaFunctionNode + + beforeEach(() => { + sandbox = sinon.createSandbox() + client = createStubInstance(DefaultLambdaClient) + outputChannel = { + appendLine: sandbox.stub(), + show: sandbox.stub(), + } as unknown as vscode.OutputChannel + + mockFunctionNode = { + configuration: { + FunctionName: 'testFunction', + FunctionArn: 'arn:aws:lambda:us-west-2:123456789012:function:testFunction', + Handler: 'index.handler', + Runtime: 'nodejs18.x', + SnapStart: { ApplyOn: 'None' }, + }, + regionCode: 'us-west-2', + localDir: '/local/path', + } as LambdaFunctionNode + + data = { + FunctionName: 'testFunction', + FunctionArn: 'arn:aws:lambda:us-west-2:123456789012:function:testFunction', + FunctionRegion: 'us-west-2', + InputSamples: [], + Runtime: 'nodejs18.x', + LocalRootPath: '/local/path', + LambdaFunctionNode: mockFunctionNode, + supportCodeDownload: true, + runtimeSupportsRemoteDebug: true, + regionSupportsRemoteDebug: true, + } as InitialData + + remoteInvokeWebview = new RemoteInvokeWebview(outputChannel, client, client, data) + + // Mock RemoteDebugController + mockDebugController = createStubInstance(RemoteDebugController) + sandbox.stub(RemoteDebugController, 'instance').get(() => mockDebugController) + + // Set handler file as available by default to avoid timeout issues + ;(remoteInvokeWebview as any).handlerFileAvailable = true + }) + + afterEach(() => { + sandbox.restore() + }) + + describe('Debug Timer Management', () => { + it('should start debug timer and count down', async () => { + remoteInvokeWebview.startDebugTimer() + + // Check initial state + assert.strictEqual(remoteInvokeWebview.getDebugTimeRemaining(), 60) + + // Wait a bit and check if timer is counting down + await new Promise((resolve) => { + setTimeout(() => { + const timeRemaining = remoteInvokeWebview.getDebugTimeRemaining() + assert(timeRemaining < 60 && timeRemaining > 0, 'Timer should be counting down') + remoteInvokeWebview.stopDebugTimer() + resolve() + }, 1100) // Wait slightly more than 1 second + }) + }) + + it('should stop debug timer', () => { + remoteInvokeWebview.startDebugTimer() + assert(remoteInvokeWebview.getDebugTimeRemaining() > 0) + + remoteInvokeWebview.stopDebugTimer() + assert.strictEqual(remoteInvokeWebview.getDebugTimeRemaining(), 0) + }) + }) + + describe('Debug State Management', () => { + it('should reset server state correctly', () => { + // Set up some state + remoteInvokeWebview.startDebugTimer() + + // Mock the debugging state + mockDebugController.isDebugging = true + + remoteInvokeWebview.resetServerState() + + assert.strictEqual(remoteInvokeWebview.getDebugTimeRemaining(), 0) + assert.strictEqual(remoteInvokeWebview.isWebViewDebugging(), false) + }) + + it('should check if ready to invoke when not invoking', () => { + const result = remoteInvokeWebview.checkReadyToInvoke() + assert.strictEqual(result, true) + }) + + it('should show warning when invoke is in progress', () => { + // Mock the window.showWarningMessage through getTestWindow + getTestWindow().onDidShowMessage(() => { + // Message handler for warning + }) + + // Set invoking state + ;(remoteInvokeWebview as any).isInvoking = true + + const result = remoteInvokeWebview.checkReadyToInvoke() + + assert.strictEqual(result, false) + // The warning should be shown but we can't easily verify it in this test setup + }) + + it('should return correct debugging states', () => { + mockDebugController.isDebugging = true + assert.strictEqual(remoteInvokeWebview.isLDKDebugging(), true) + + mockDebugController.isDebugging = false + assert.strictEqual(remoteInvokeWebview.isLDKDebugging(), false) + }) + }) + + describe('Debug Configuration and Validation', () => { + let mockConfig: DebugConfig + + beforeEach(() => { + mockConfig = createMockDebugConfig({ + functionArn: data.FunctionArn, + functionName: data.FunctionName, + }) + }) + + it('should check ready to debug with valid config', async () => { + // Ensure handler file is available to avoid confirmation dialog + ;(remoteInvokeWebview as any).handlerFileAvailable = true + + const result = await remoteInvokeWebview.checkReadyToDebug(mockConfig) + assert.strictEqual(result, true) + }) + + it('should return false when LambdaFunctionNode is undefined', async () => { + remoteInvokeWebview = new RemoteInvokeWebview(outputChannel, client, client, { + ...data, + LambdaFunctionNode: undefined, + }) + + const result = await remoteInvokeWebview.checkReadyToDebug(mockConfig) + assert.strictEqual(result, false) + }) + + it('should show warning when handler file is not available', async () => { + const showConfirmationStub = sandbox.stub(messages, 'showConfirmationMessage').resolves(false) + + // Set handler file as not available + ;(remoteInvokeWebview as any).handlerFileAvailable = false + + const result = await remoteInvokeWebview.checkReadyToDebug(mockConfig) + + assert.strictEqual(result, false) + assert(showConfirmationStub.calledOnce) + }) + + it('should show snapstart warning when publishing version with snapstart enabled', async () => { + const showConfirmationStub = sandbox.stub(messages, 'showConfirmationMessage').resolves(false) + + mockConfig.shouldPublishVersion = true + data.LambdaFunctionNode!.configuration.SnapStart = { ApplyOn: 'PublishedVersions' } + + const result = await remoteInvokeWebview.checkReadyToDebug(mockConfig) + + assert.strictEqual(result, false) + assert(showConfirmationStub.calledOnce) + }) + }) + + describe('Debug Session Management', () => { + let mockConfig: DebugConfig + + beforeEach(() => { + mockConfig = createMockDebugConfig({ + functionArn: data.FunctionArn, + functionName: data.FunctionName, + }) + }) + + it('should start debugging successfully', async () => { + // Ensure handler file is available to avoid confirmation dialog + ;(remoteInvokeWebview as any).handlerFileAvailable = true + + mockDebugController.startDebugging.resolves() + mockDebugController.isDebugging = true + + const result = await remoteInvokeWebview.startDebugging(mockConfig) + + assert.strictEqual(result, true) + assert(mockDebugController.startDebugging.calledOnce) + }) + + it('should call stop debugging', async () => { + mockDebugController.isDebugging = true + mockDebugController.stopDebugging.resolves() + + await remoteInvokeWebview.stopDebugging() + + // The method doesn't return a boolean, it returns void + assert(mockDebugController.stopDebugging.calledOnce) + }) + + it('should handle debug pre-check with existing session', async () => { + const showConfirmationStub = sandbox.stub(messages, 'showConfirmationMessage').resolves(true) + const stopDebuggingStub = sandbox.stub(remoteInvokeWebview, 'stopDebugging').resolves(false) + mockDebugController.isDebugging = true + mockDebugController.installDebugExtension.resolves(true) + + // Mock revertExistingConfig - need to import it properly + const ldkController = require('../../../../lambda/remoteDebugging/ldkController') + const revertStub = sandbox.stub(ldkController, 'revertExistingConfig').resolves(true) + + await remoteInvokeWebview.debugPreCheck() + + assert(showConfirmationStub.calledOnce) + assert(stopDebuggingStub.calledOnce) + assert(mockDebugController.installDebugExtension.calledOnce) + assert(revertStub.calledOnce) + }) + }) + + describe('File Operations and Code Management', () => { + it('should prompt for folder selection', async () => { + const mockUri = vscode.Uri.file('/selected/folder') + getTestWindow().onDidShowDialog((d) => d.selectItem(mockUri)) + + const result = await remoteInvokeWebview.promptFolder() + + assert.strictEqual(result, mockUri.fsPath) + assert.strictEqual(remoteInvokeWebview.getLocalPath(), mockUri.fsPath) + }) + + it('should return undefined when no folder is selected', async () => { + getTestWindow().onDidShowDialog((d) => d.close()) + + const result = await remoteInvokeWebview.promptFolder() + + assert.strictEqual(result, undefined) + }) + + it('should try to open handler file successfully', async () => { + const mockHandlerUri = vscode.Uri.file('/local/path/index.js') + sandbox.stub(appBuilderUtils, 'getLambdaHandlerFile').resolves(mockHandlerUri) + sandbox.stub(fs, 'exists').resolves(true) + sandbox.stub(downloadLambda, 'openLambdaFile').resolves() + + const result = await remoteInvokeWebview.tryOpenHandlerFile('/local/path') + + assert.strictEqual(result, true) + assert.strictEqual(remoteInvokeWebview.getHandlerAvailable(), true) + }) + + it('should handle handler file not found', async () => { + sandbox.stub(appBuilderUtils, 'getLambdaHandlerFile').resolves(undefined) + + // Mock the warning message through getTestWindow + getTestWindow().onDidShowMessage(() => { + // Message handler for warning + }) + + const result = await remoteInvokeWebview.tryOpenHandlerFile('/local/path') + + assert.strictEqual(result, false) + assert.strictEqual(remoteInvokeWebview.getHandlerAvailable(), false) + }) + + it('should download remote code successfully', async () => { + const mockUri = vscode.Uri.file('/downloaded/path') + sandbox.stub(downloadLambda, 'runDownloadLambda').resolves(mockUri) + + // Mock workspace state operations + const mockWorkspaceState = { + get: sandbox.stub().returns({}), + update: sandbox.stub().resolves(), + } + sandbox.stub(globals, 'context').value({ + workspaceState: mockWorkspaceState, + }) + + const result = await remoteInvokeWebview.downloadRemoteCode() + + assert.strictEqual(result, mockUri.fsPath) + assert.strictEqual(data.LocalRootPath, mockUri.fsPath) + }) + + it('should handle download failure', async () => { + sandbox.stub(downloadLambda, 'runDownloadLambda').rejects(new Error('Download failed')) + + await assert.rejects( + async () => await remoteInvokeWebview.downloadRemoteCode(), + /Failed to download remote code/ + ) + }) + }) + + describe('File Watching and Code Synchronization', () => { + it('should setup file watcher when local root path exists', () => { + const createFileSystemWatcherStub = sandbox.stub(vscode.workspace, 'createFileSystemWatcher') + const mockWatcher = { + onDidChange: sandbox.stub(), + onDidCreate: sandbox.stub(), + onDidDelete: sandbox.stub(), + } + createFileSystemWatcherStub.returns(mockWatcher as any) + + // Call the private method through reflection + ;(remoteInvokeWebview as any).setupFileWatcher() + + assert(createFileSystemWatcherStub.calledOnce) + assert(mockWatcher.onDidChange.calledOnce) + assert(mockWatcher.onDidCreate.calledOnce) + assert(mockWatcher.onDidDelete.calledOnce) + }) + + it('should handle file changes and prompt for upload', async () => { + const showConfirmationStub = sandbox.stub(messages, 'showMessage').resolves('Yes') + const runUploadDirectoryStub = sandbox.stub(uploadLambda, 'runUploadDirectory').resolves() + + // Mock file watcher setup + let changeHandler: () => Promise + const mockWatcher = { + onDidChange: (handler: () => Promise) => { + changeHandler = handler + }, + onDidCreate: sandbox.stub(), + onDidDelete: sandbox.stub(), + } + sandbox.stub(vscode.workspace, 'createFileSystemWatcher').returns(mockWatcher as any) + + // Setup file watcher + ;(remoteInvokeWebview as any).setupFileWatcher() + + // Trigger file change + await changeHandler!() + + assert(showConfirmationStub.calledOnce) + assert(runUploadDirectoryStub.calledOnce) + }) + }) + + describe('Lambda Invocation with Debugging', () => { + it('should invoke lambda with remote debugging enabled', async () => { + const mockResponse = { + LogResult: Buffer.from('Debug log').toString('base64'), + Payload: new TextEncoder().encode('{"result": "debug success"}'), + } satisfies InvocationResponse + client.invoke.resolves(mockResponse) + mockDebugController.isDebugging = true + mockDebugController.qualifier = 'v1' + + const focusStub = sandbox.stub(vscode.commands, 'executeCommand').resolves() + + await remoteInvokeWebview.invokeLambda('{"test": "input"}', 'test', true) + + assert(client.invoke.calledWith(data.FunctionArn, '{"test": "input"}', 'v1')) + assert(focusStub.calledWith('workbench.action.focusFirstEditorGroup')) + }) + + it('should handle timer management during debugging invocation', async () => { + const mockResponse = { + LogResult: Buffer.from('Debug log').toString('base64'), + Payload: new TextEncoder().encode('{"result": "debug success"}'), + } satisfies InvocationResponse + client.invoke.resolves(mockResponse) + mockDebugController.isDebugging = true + + const stopTimerStub = sandbox.stub(remoteInvokeWebview, 'stopDebugTimer') + const startTimerStub = sandbox.stub(remoteInvokeWebview, 'startDebugTimer') + + await remoteInvokeWebview.invokeLambda('{"test": "input"}', 'test', true) + + // Timer should be stopped at least once during invoke + assert(stopTimerStub.calledOnce) + assert(startTimerStub.calledOnce) // Called after invoke + }) + }) + + describe('Dispose and Cleanup', () => { + it('should dispose server and clean up resources', async () => { + // Set up debugging state and disposables + ;(remoteInvokeWebview as any).debugging = true + mockDebugController.isDebugging = true + + // Mock disposables + const mockDisposable = { dispose: sandbox.stub() } + ;(remoteInvokeWebview as any).watcherDisposable = mockDisposable + ;(remoteInvokeWebview as any).fileWatcherDisposable = mockDisposable + + await remoteInvokeWebview.disposeServer() + + assert(mockDisposable.dispose.calledTwice) + assert(mockDebugController.stopDebugging.calledOnce) + }) + + it('should handle dispose when not debugging', async () => { + mockDebugController.isDebugging = false + + const mockDisposable = { dispose: sandbox.stub() } + ;(remoteInvokeWebview as any).watcherDisposable = mockDisposable + + await remoteInvokeWebview.disposeServer() + + assert(mockDisposable.dispose.calledOnce) + }) + }) + + describe('Debug Session Event Handling', () => { + it('should handle debug session termination', async () => { + const resetStateStub = sandbox.stub(remoteInvokeWebview, 'resetServerState') + + // Mock debug session termination event + let terminationHandler: (session: vscode.DebugSession) => Promise + sandbox.stub(vscode.debug, 'onDidTerminateDebugSession').callsFake((handler) => { + terminationHandler = handler + return { dispose: sandbox.stub() } + }) + + // Initialize the webview to set up event handlers + remoteInvokeWebview.init() + + // Simulate debug session termination + const mockSession = { name: 'test-session' } as vscode.DebugSession + await terminationHandler!(mockSession) + + assert(resetStateStub.calledOnce) + }) + }) + + describe('Debugging Flow', () => { + let mockConfig: DebugConfig + + beforeEach(() => { + mockConfig = createMockDebugConfig({ + 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: { + run: mockRun, + }, + }) + }) + + it('should handle complete debugging workflow', async () => { + // Setup mocks for successful debugging + mockDebugController.startDebugging.resolves() + mockDebugController.stopDebugging.resolves() + mockDebugController.isDebugging = false + + // Mock the debugging state change after startDebugging is called + mockDebugController.startDebugging.callsFake(async () => { + mockDebugController.isDebugging = true + return Promise.resolve() + }) + + // 1. Start debugging + const startResult = await remoteInvokeWebview.startDebugging(mockConfig) + assert.strictEqual(startResult, true, 'Debug session should start successfully') + + // Set qualifier for invocation + mockDebugController.qualifier = '$LATEST' + + // 2. Test lambda invocation during debugging + const mockResponse = { + LogResult: Buffer.from('Debug invocation log').toString('base64'), + Payload: new TextEncoder().encode('{"debugResult": "success"}'), + } satisfies InvocationResponse + client.invoke.resolves(mockResponse) + + await remoteInvokeWebview.invokeLambda('{"debugInput": "test"}', 'integration-test', true) + + // Verify invocation was called with correct parameters + assert(client.invoke.calledWith(data.FunctionArn, '{"debugInput": "test"}', '$LATEST')) + + // 3. Stop debugging + await remoteInvokeWebview.stopDebugging() + + // Verify cleanup operations were called + assert(mockDebugController.stopDebugging.calledOnce, 'Should stop debugging') + }) + + it('should handle debugging failure gracefully', async () => { + // Setup mock for debugging failure + mockDebugController.startDebugging.rejects(new Error('Debug start failed')) + mockDebugController.isDebugging = false + + // Attempt to start debugging - should throw error + try { + await remoteInvokeWebview.startDebugging(mockConfig) + assert.fail('Expected error to be thrown') + } catch (error) { + assert(error instanceof ToolkitError) + assert(error.message.includes('Failed to start debugging')) + assert(error.cause?.message.includes('Debug start failed')) + } + + assert.strictEqual( + remoteInvokeWebview.isWebViewDebugging(), + false, + 'Webview should not be in debugging state' + ) + }) + + it('should handle version publishing workflow', async () => { + // Setup config for version publishing + const versionConfig = { ...mockConfig, shouldPublishVersion: true } + + // Setup mocks for version publishing + mockDebugController.startDebugging.resolves() + mockDebugController.stopDebugging.resolves() + mockDebugController.isDebugging = false + + // Mock the debugging state change after startDebugging is called + mockDebugController.startDebugging.callsFake(async () => { + mockDebugController.isDebugging = true + mockDebugController.qualifier = 'v1' + return Promise.resolve() + }) + + // Start debugging with version publishing + const startResult = await remoteInvokeWebview.startDebugging(versionConfig) + assert.strictEqual(startResult, true, 'Debug session should start successfully') + + // Test invocation with version qualifier + const mockResponse = { + LogResult: Buffer.from('Version debug log').toString('base64'), + Payload: new TextEncoder().encode('{"versionResult": "success"}'), + } satisfies InvocationResponse + client.invoke.resolves(mockResponse) + + await remoteInvokeWebview.invokeLambda('{"versionInput": "test"}', 'version-test', true) + + // Should invoke with version qualifier + assert(client.invoke.calledWith(data.FunctionArn, '{"versionInput": "test"}', 'v1')) + + // Stop debugging + await remoteInvokeWebview.stopDebugging() + + assert(mockDebugController.stopDebugging.calledOnce, 'Should stop debugging') + }) + }) +}) 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..852ba43d5bd 100644 --- a/packages/core/src/test/lambda/vue/remoteInvoke/remoteInvoke.test.ts +++ b/packages/core/src/test/lambda/vue/remoteInvoke/remoteInvoke.test.ts @@ -8,7 +8,7 @@ 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 @@ -26,15 +26,15 @@ describe('RemoteInvokeWebview', function () { mockData = { FunctionArn: 'arn:aws:lambda:us-west-2:123456789012:function:my-function', } - remoteInvokeWebview = new RemoteInvokeWebview(outputChannel, client, mockData) + remoteInvokeWebview = new RemoteInvokeWebview(outputChannel, client, 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 = { + const mockResponse = { LogResult: Buffer.from('Test log').toString('base64'), - Payload: '{"result": "success"}', - } + Payload: new TextEncoder().encode('{"result": "success"}'), + } satisfies InvocationResponse client.invoke.resolves(mockResponse) await remoteInvokeWebview.invokeLambda(input) sinon.assert.calledOnce(client.invoke) 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..0756cdcbe88 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/activation.test.ts @@ -0,0 +1,273 @@ +/*! + * 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' + +describe('SageMaker Unified Studio Main Activation', function () { + 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 + + beforeEach(function () { + mockExtensionContext = { + subscriptions: [], + extensionPath: '/test/path', + globalState: { + get: sinon.stub(), + update: sinon.stub(), + }, + workspaceState: { + get: sinon.stub(), + update: sinon.stub(), + }, + } 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') + + // Set default return values + isSageMakerStub.returns(false) + initializeResourceMetadataStub.resolves() + setContextStub.resolves() + isInSmusSpaceEnvironmentStub.returns(false) + activateConnectionMagicsSelectorStub.resolves() + activateExplorerStub.resolves() + }) + + afterEach(function () { + sinon.restore() + }) + + describe('activate function', function () { + it('should always activate explorer regardless of environment', async function () { + isSageMakerStub.returns(false) + + await activate(mockExtensionContext) + + assert.ok(activateExplorerStub.calledOnceWith(mockExtensionContext)) + }) + + it('should not initialize SMUS components when not in SageMaker environment', async function () { + isSageMakerStub.returns(false) + + await activate(mockExtensionContext) + + 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(mockExtensionContext) + + 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(mockExtensionContext) + + 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(mockExtensionContext) + + // 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(mockExtensionContext), /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(mockExtensionContext), /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(mockExtensionContext), /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(mockExtensionContext), /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(mockExtensionContext) + + assert.ok(activateConnectionMagicsSelectorStub.calledWith(mockExtensionContext)) + assert.ok(activateExplorerStub.calledWith(mockExtensionContext)) + }) + }) + + 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(mockExtensionContext) + + 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(mockExtensionContext) + + 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(mockExtensionContext) + + 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(mockExtensionContext) + assert.ok(setContextStub.calledWith('aws.smus.inSmusSpaceEnvironment', true)) + + // Reset and test with false + setContextStub.resetHistory() + isInSmusSpaceEnvironmentStub.returns(false) + await activate(mockExtensionContext) + 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(mockExtensionContext), /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(mockExtensionContext) + + // Verify all expected functions were called + assert.ok(initializeResourceMetadataStub.calledOnce) + assert.ok(setContextStub.calledOnce) + assert.ok(activateConnectionMagicsSelectorStub.calledOnce) + assert.ok(activateExplorerStub.calledOnce) + }) + + it('should handle undefined extension context gracefully', async function () { + const undefinedContext = undefined as any + + // Should not throw for undefined context, but let the individual activation functions handle it + await activate(undefinedContext) + + assert.ok(activateExplorerStub.calledWith(undefinedContext)) + }) + }) +}) 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..951e391d181 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/auth/connectionCredentialsProvider.test.ts @@ -0,0 +1,215 @@ +/*! + * 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 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), + activeConnection: { + ssoRegion: testRegion, + }, + } as any + + // Mock DataZone client + mockDataZoneClient = { + getConnection: sinon.stub().resolves(mockGetConnectionResponse), + } as any + + // Stub DataZoneClient.getInstance + dataZoneClientStub = sinon.stub(DataZoneClient, 'getInstance').resolves(mockDataZoneClient as any) + + connectionProvider = new ConnectionCredentialsProvider(mockAuthProvider as any, testConnectionId) + }) + + 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..a6ca72736e9 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/auth/model.test.ts @@ -0,0 +1,232 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { + createSmusProfile, + isValidSmusConnection, + scopeSmus, + SmusConnection, +} from '../../../sagemakerunifiedstudio/auth/model' +import { SsoConnection } from '../../../auth/connection' + +describe('SMUS Auth Model', function () { + const testDomainUrl = 'https://dzd_domainId.sagemaker.us-east-2.on.aws' + const testDomainId = 'dzd_domainId' + const testStartUrl = 'https://identitycenter.amazonaws.com/ssoins-testInstanceId' + const testRegion = 'us-east-2' + + describe('scopeSmus', function () { + it('should have correct scope value', function () { + assert.strictEqual(scopeSmus, 'datazone:domain:access') + }) + }) + + describe('createSmusProfile', function () { + it('should create profile with default scopes', function () { + const profile = createSmusProfile(testDomainUrl, testDomainId, testStartUrl, testRegion) + + assert.strictEqual(profile.domainUrl, testDomainUrl) + assert.strictEqual(profile.domainId, testDomainId) + assert.strictEqual(profile.startUrl, testStartUrl) + assert.strictEqual(profile.ssoRegion, testRegion) + assert.strictEqual(profile.type, 'sso') + assert.deepStrictEqual(profile.scopes, [scopeSmus]) + }) + + it('should create profile with custom scopes', function () { + const customScopes = ['custom:scope', 'another:scope'] + const profile = createSmusProfile(testDomainUrl, testDomainId, testStartUrl, testRegion, customScopes) + + assert.strictEqual(profile.domainUrl, testDomainUrl) + assert.strictEqual(profile.domainId, testDomainId) + assert.strictEqual(profile.startUrl, testStartUrl) + assert.strictEqual(profile.ssoRegion, testRegion) + assert.strictEqual(profile.type, 'sso') + assert.deepStrictEqual(profile.scopes, customScopes) + }) + + it('should create profile with all required properties', function () { + const profile = createSmusProfile(testDomainUrl, testDomainId, testStartUrl, testRegion) + + // Check SsoProfile properties + assert.strictEqual(profile.type, 'sso') + assert.strictEqual(profile.startUrl, testStartUrl) + assert.strictEqual(profile.ssoRegion, testRegion) + assert.ok(Array.isArray(profile.scopes)) + + // Check SmusProfile properties + assert.strictEqual(profile.domainUrl, testDomainUrl) + assert.strictEqual(profile.domainId, testDomainId) + }) + }) + + describe('isValidSmusConnection', function () { + it('should return true for valid SMUS connection', function () { + const validConnection = { + id: 'test-connection-id', + type: 'sso', + startUrl: testStartUrl, + ssoRegion: testRegion, + scopes: [scopeSmus], + label: 'Test SMUS Connection', + domainUrl: testDomainUrl, + domainId: testDomainId, + } as SmusConnection + + assert.strictEqual(isValidSmusConnection(validConnection), true) + }) + + it('should return false for connection without SMUS scope', function () { + const connectionWithoutScope = { + id: 'test-connection-id', + type: 'sso', + startUrl: testStartUrl, + ssoRegion: testRegion, + scopes: ['sso:account:access'], + label: 'Test Connection', + domainUrl: testDomainUrl, + domainId: testDomainId, + } as any + + assert.strictEqual(isValidSmusConnection(connectionWithoutScope), false) + }) + + it('should return false for connection without SMUS properties', function () { + const connectionWithoutSmusProps = { + id: 'test-connection-id', + type: 'sso', + startUrl: testStartUrl, + ssoRegion: testRegion, + scopes: [scopeSmus], + label: 'Test Connection', + } as SsoConnection + + assert.strictEqual(isValidSmusConnection(connectionWithoutSmusProps), false) + }) + + it('should return false for non-SSO connection', function () { + const nonSsoConnection = { + id: 'test-connection-id', + type: 'iam', + label: 'Test IAM Connection', + domainUrl: testDomainUrl, + domainId: testDomainId, + scopes: [scopeSmus], + } + + assert.strictEqual(isValidSmusConnection(nonSsoConnection), false) + }) + + it('should return false for undefined connection', function () { + assert.strictEqual(isValidSmusConnection(undefined), false) + }) + + it('should return false for null connection', function () { + assert.strictEqual(isValidSmusConnection(undefined), false) + }) + + it('should return false for connection without scopes', function () { + const connectionWithoutScopes = { + id: 'test-connection-id', + type: 'sso', + startUrl: testStartUrl, + ssoRegion: testRegion, + label: 'Test Connection', + domainUrl: testDomainUrl, + domainId: testDomainId, + } + + assert.strictEqual(isValidSmusConnection(connectionWithoutScopes), false) + }) + + it('should return false for connection with empty scopes array', function () { + const connectionWithEmptyScopes = { + id: 'test-connection-id', + type: 'sso', + startUrl: testStartUrl, + ssoRegion: testRegion, + scopes: [], + label: 'Test Connection', + domainUrl: testDomainUrl, + domainId: testDomainId, + } + + assert.strictEqual(isValidSmusConnection(connectionWithEmptyScopes), false) + }) + + it('should return true for connection with SMUS scope among other scopes', function () { + const connectionWithMultipleScopes = { + id: 'test-connection-id', + type: 'sso', + startUrl: testStartUrl, + ssoRegion: testRegion, + scopes: ['sso:account:access', scopeSmus, 'other:scope'], + label: 'Test SMUS Connection', + domainUrl: testDomainUrl, + domainId: testDomainId, + } as SmusConnection + + assert.strictEqual(isValidSmusConnection(connectionWithMultipleScopes), true) + }) + + it('should return false for connection missing domainUrl', function () { + const connectionMissingDomainUrl = { + id: 'test-connection-id', + type: 'sso', + startUrl: testStartUrl, + ssoRegion: testRegion, + scopes: [scopeSmus], + label: 'Test Connection', + domainId: testDomainId, + } + + assert.strictEqual(isValidSmusConnection(connectionMissingDomainUrl), false) + }) + + it('should return false for connection missing domainId', function () { + const connectionMissingDomainId = { + id: 'test-connection-id', + type: 'sso', + startUrl: testStartUrl, + ssoRegion: testRegion, + scopes: [scopeSmus], + label: 'Test Connection', + domainUrl: testDomainUrl, + } + + assert.strictEqual(isValidSmusConnection(connectionMissingDomainId), false) + }) + }) + + describe('SmusConnection interface', function () { + it('should extend both SmusProfile and SsoConnection', function () { + const connection = { + id: 'test-connection-id', + type: 'sso', + startUrl: testStartUrl, + ssoRegion: testRegion, + scopes: [scopeSmus], + label: 'Test SMUS Connection', + domainUrl: testDomainUrl, + domainId: testDomainId, + } as SmusConnection + + // Should have Connection properties + assert.strictEqual(connection.id, 'test-connection-id') + assert.strictEqual(connection.label, 'Test SMUS Connection') + + // Should have SsoConnection properties + assert.strictEqual(connection.type, 'sso') + assert.strictEqual(connection.startUrl, testStartUrl) + assert.strictEqual(connection.ssoRegion, testRegion) + assert.ok(Array.isArray(connection.scopes)) + + // Should have SmusProfile properties + assert.strictEqual(connection.domainUrl, testDomainUrl) + assert.strictEqual(connection.domainId, testDomainId) + }) + }) +}) 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..6dd206593f8 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/auth/projectRoleCredentialsProvider.test.ts @@ -0,0 +1,241 @@ +/*! + * 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), + } as any + + // Mock DataZone client + mockDataZoneClient = { + getProjectDefaultEnvironmentCreds: sinon.stub().resolves(mockGetEnvironmentCredentialsResponse), + } as any + + // Stub DataZoneClient.getInstance + dataZoneClientStub = sinon.stub(DataZoneClient, 'getInstance').resolves(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 getInstance was called + assert.ok(dataZoneClientStub.calledWith(mockSmusAuthProvider)) + + // 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..7cd2662f467 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/auth/smusAuthenticationProvider.test.ts @@ -0,0 +1,760 @@ +/*! + * 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' + +// Mock the setContext function BEFORE importing modules that use it +const setContextModule = require('../../../shared/vscode/setContext') + +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 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 + }, + 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, 'getInstance').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(require('../../../auth/secondaryAuth'), 'getSecondaryAuth').returns(mockSecondaryAuth) + + smusAuthProvider = new SmusAuthenticationProvider(mockAuth, mockSecondaryAuth) + + // 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 () { + it('should call secondary auth restoreConnection', async function () { + await smusAuthProvider.restore() + assert.ok(mockSecondaryAuth.restoreConnection.called) + }) + }) + + describe('connectToSmus', function () { + it('should create new connection when none exists', async function () { + mockAuth.listConnections.resolves([]) + + const result = await smusAuthProvider.connectToSmus(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.connectToSmus(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.connectToSmus(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.connectToSmus('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.connectToSmus(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.connectToSmus(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.connectToSmus(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.connectToSmus(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.connectToSmus(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', async function () { + const result = await smusAuthProvider.reauthenticate(mockSmusConnection) + + assert.strictEqual(result, mockSmusConnection) + 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 + const stsClientModule = require('../../../shared/clients/stsClient') + 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.getInstance 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)) + }) + }) + }) +}) 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..982aa481bd3 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/activation.test.ts @@ -0,0 +1,449 @@ +/*! + * 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 { DataZoneClient } from '../../../sagemakerunifiedstudio/shared/client/datazoneClient' +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' + +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 createTreeViewStub: sinon.SinonStub + let registerCommandStub: sinon.SinonStub + 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() })), + 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 + + mockSmusRootNode = { + getChildren: sinon.stub().resolves([]), + getProjectSelectNode: sinon.stub().returns({ refreshNode: sinon.stub().resolves() }), + } 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) + + // Stub SmusAuthenticationProvider + sinon.stub(SmusAuthenticationProvider, 'fromContext').returns(mockSmusAuthProvider as any) + + // Stub DataZoneClient + 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 - it should be the last one added + 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(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('Failed to reauthenticate'))) + }) + + 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/lakehouseStrategy.test.ts b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/lakehouseStrategy.test.ts new file mode 100644 index 00000000000..63e87c25f23 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/lakehouseStrategy.test.ts @@ -0,0 +1,463 @@ +/*! + * 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 { GlueCatalogClient } from '../../../../sagemakerunifiedstudio/shared/client/glueCatalogClient' +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 + + sandbox.stub(GlueCatalogClient, 'createWithCredentials').returns(mockGlueCatalogClient as any) + 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 () { + mockGlueCatalogClient.getCatalogs.resolves({ catalogs: [], nextToken: undefined }) + + const node = createLakehouseConnectionNode( + mockConnection as any, + mockCredentialsProvider as ConnectionCredentialsProvider, + 'us-east-1' + ) + const children = await node.getChildren() + + assert.ok(children.some((child) => child.resource === '[No data found]')) + }) + }) + + describe('Catalog nodes', function () { + it('should create catalog nodes from API', async function () { + mockGlueCatalogClient.getCatalogs.resolves({ + catalogs: [{ CatalogId: 'test-catalog', CatalogType: 'HIVE' }], + }) + 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() + + assert.ok(children.length > 0) + assert.ok(mockGlueCatalogClient.getCatalogs.called) + }) + + 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) + }) + }) + + 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..ebf2eae2cb0 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioAuthInfoNode.test.ts @@ -0,0 +1,291 @@ +/*! + * 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 } from '../../../../sagemakerunifiedstudio/auth/model' + +describe('SageMakerUnifiedStudioAuthInfoNode', function () { + let authInfoNode: SageMakerUnifiedStudioAuthInfoNode + let mockAuthProvider: any + let mockConnection: SmusConnection + 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() })), + get activeConnection() { + return currentActiveConnection + }, + set activeConnection(value: SmusConnection | undefined) { + currentActiveConnection = value + }, + } + + // 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', function () { + const treeItem = 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', function () { + const treeItem = 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', function () { + const treeItem = 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', function () { + const treeItem = 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', function () { + mockAuthProvider.isConnected.returns(true) + mockAuthProvider.isConnectionValid.returns(true) + + const treeItem = 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', function () { + mockAuthProvider.isConnected.returns(true) + mockAuthProvider.isConnectionValid.returns(false) + + const treeItem = 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', function () { + mockAuthProvider.isConnected.returns(false) + + const treeItem = 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', function () { + mockAuthProvider.isConnected.returns(true) + mockAuthProvider.isConnectionValid.returns(true) + + const treeItem = 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', function () { + mockAuthProvider.isConnected.returns(true) + mockAuthProvider.isConnectionValid.returns(false) + + const treeItem = 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', function () { + mockAuthProvider.isConnected.returns(false) + + const treeItem = 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')) + }) + }) +}) 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..fc74eeab435 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioComputeNode.test.ts @@ -0,0 +1,93 @@ +/*! + * 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' + +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', async function () { + const mockProject = { id: 'project-123', name: 'Test Project' } + ;(mockParent.getProject as sinon.SinonStub).returns(mockProject) + + 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) + }) + }) + + 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..686c85a0055 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioConnectionParentNode.test.ts @@ -0,0 +1,234 @@ +/*! + * 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: {} as any, + parent: { + project: mockProject, + } as any, + } as any + + // Stub static methods + sinon.stub(DataZoneClient, 'getInstance').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: {} 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, 'getInstance').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..991e5955989 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioDataNode.test.ts @@ -0,0 +1,235 @@ +/*! + * 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' + +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'), + } 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, 'getInstance').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' }, + ] + + 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) + mockDataZoneClient.getConnection.rejects(new Error('Connection error')) + + 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') + + // 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..2fd8317fe06 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioProjectNode.test.ts @@ -0,0 +1,335 @@ +/*! + * 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'), + } 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, 'getInstance').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('returns false when getCredentials fails', async function () { + const mockCredProvider = { + getCredentials: sinon.stub().rejects(new Error('Credentials error')), + } + projectNode['authProvider'].getProjectCredentialProvider = sinon.stub().resolves(mockCredProvider) + + const hasAccess = await projectNode['checkProjectCredsAccess']('project-123') + assert.strictEqual(hasAccess, false) + }) + + it('returns false when access check throws non-AccessDeniedException error', async function () { + projectNode['authProvider'].getProjectCredentialProvider = sinon.stub().rejects(new Error('Other error')) + + const hasAccess = await projectNode['checkProjectCredsAccess']('project-123') + assert.strictEqual(hasAccess, false) + }) + }) + + 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..64b866c7704 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioRootNode.test.ts @@ -0,0 +1,527 @@ +/*! + * 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' + +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, 'getInstance').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: { domainId: testDomainId, ssoRegion: 'us-west-2' }, + 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 + + 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 DataZoneClient static methods + sinon.stub(DataZoneClient, 'getInstance').returns(mockDataZoneClient as any) + + // 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'), + } as any) + + // 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 + + 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 + + sinon.stub(DataZoneClient, 'getInstance').returns(mockDataZoneClient as any) + 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) + + 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')) + assert.ok( + createQuickPickStub.calledWith([ + { + label: 'No projects found', + detail: '', + description: '', + data: {}, + }, + ]) + ) + }) + + 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) + }) +}) 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..31481e70953 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpacesParentNode.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 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 } from '../../../../sagemakerunifiedstudio/shared/smusUtils' + +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' }, + } 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, 'getInstance').resolves(mockDataZoneClient as any) + sinon.stub(getLogger(), 'debug') + sinon.stub(getLogger(), 'error') + sinon.stub(SmusUtils, 'extractSSOIdFromUserId').returns('user-12345') + + 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.getInstance 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/) + }) + }) +}) 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..cd92aa42981 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/explorer/nodes/utils.test.ts @@ -0,0 +1,252 @@ +/*! + * 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, +} 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) + }) + }) +}) 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..38dbd5e33f5 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/shared/client/datazoneClient.test.ts @@ -0,0 +1,483 @@ +/*! + * 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' + +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 = { + 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(), + }), + } as any + + // Set up the DataZoneClient using getInstance since constructor is private + DataZoneClient.dispose() + dataZoneClient = await DataZoneClient.getInstance(mockAuthProvider) + }) + + afterEach(() => { + sinon.restore() + }) + + describe('getInstance', () => { + it('should return singleton instance', async () => { + const instance1 = await DataZoneClient.getInstance(mockAuthProvider) + const instance2 = await DataZoneClient.getInstance(mockAuthProvider) + + assert.strictEqual(instance1, instance2) + }) + + it('should create new instance after dispose', async () => { + const instance1 = await DataZoneClient.getInstance(mockAuthProvider) + DataZoneClient.dispose() + const instance2 = await DataZoneClient.getInstance(mockAuthProvider) + + assert.notStrictEqual(instance1, instance2) + }) + }) + + describe('dispose', () => { + it('should clear singleton instance', async () => { + const instance = await DataZoneClient.getInstance(mockAuthProvider) + DataZoneClient.dispose() + + // Should create new instance after dispose + const newInstance = await DataZoneClient.getInstance(mockAuthProvider) + assert.notStrictEqual(instance, newInstance) + }) + }) + + 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), + } + + 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 () { + const client = await DataZoneClient.getInstance(mockAuthProvider) + + // Create a stub for listProjects that returns paginated results + 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 + client.listProjects = listProjectsStub + + // Call fetchAllProjects + const result = await client.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 () { + const client = await DataZoneClient.getInstance(mockAuthProvider) + + // Create a stub for listProjects that returns empty results + const listProjectsStub = sinon.stub().resolves({ + projects: [], + nextToken: undefined, + }) + + // Replace the listProjects method with our stub + client.listProjects = listProjectsStub + + // Call fetchAllProjects + const result = await client.fetchAllProjects() + + // Verify results + assert.strictEqual(result.length, 0) + assert.strictEqual(listProjectsStub.callCount, 1) + }) + + it('handles errors gracefully', async function () { + const client = await DataZoneClient.getInstance(mockAuthProvider) + + // 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 + client.listProjects = listProjectsStub + + // Call fetchAllProjects and expect it to throw + await assert.rejects(() => client.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' }], + }), + } + + 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), + } + + 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'), + /Failed to get tooling environment ID: 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) + }) + }) +}) 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..22a97d82caf --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/shared/client/gluePrivateClient.test.ts @@ -0,0 +1,143 @@ +/*! + * 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 globals from '../../../../shared/extensionGlobals' +import { GlueCatalogClient } from '../../../../sagemakerunifiedstudio/shared/client/glueCatalogClient' +import { ConnectionCredentialsProvider } from '../../../../sagemakerunifiedstudio/auth/providers/connectionCredentialsProvider' + +describe('GlueCatalogClient', function () { + let sandbox: sinon.SinonSandbox + let mockGlueClient: any + let mockSdkClientBuilder: any + + beforeEach(function () { + sandbox = sinon.createSandbox() + + mockGlueClient = { + getCatalogs: sandbox.stub().returns({ + promise: sandbox.stub().resolves({ + CatalogList: [ + { + Name: 'test-catalog', + CatalogType: 'HIVE', + Parameters: { key1: 'value1' }, + }, + ], + }), + }), + } + + mockSdkClientBuilder = { + createAwsService: sandbox.stub().resolves(mockGlueClient), + } + + sandbox.stub(globals, 'sdkClientBuilder').value(mockSdkClientBuilder) + }) + + 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 () { + mockGlueClient.getCatalogs.returns({ + promise: sandbox.stub().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') + mockGlueClient.getCatalogs.returns({ + promise: sandbox.stub().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: async () => ({ + accessKeyId: 'test-key', + secretAccessKey: 'test-secret', + sessionToken: 'test-token', + }), + } + + const client = GlueCatalogClient.createWithCredentials( + 'us-east-1', + credentialsProvider as ConnectionCredentialsProvider + ) + await client.getCatalogs() + + assert.ok(mockSdkClientBuilder.createAwsService.calledOnce) + const callArgs = mockSdkClientBuilder.createAwsService.getCall(0).args[1] + assert.ok(callArgs.credentialProvider) + assert.strictEqual(callArgs.region, 'us-east-1') + }) + + it('should create client without credentials when not provided', async function () { + const client = GlueCatalogClient.getInstance('us-east-1') + await client.getCatalogs() + + assert.ok(mockSdkClientBuilder.createAwsService.calledOnce) + const callArgs = mockSdkClientBuilder.createAwsService.getCall(0).args[1] + assert.strictEqual(callArgs.region, 'us-east-1') + assert.ok(!callArgs.credentials) + }) + }) +}) 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/smusUtils.test.ts b/packages/core/src/test/sagemakerunifiedstudio/shared/smusUtils.test.ts new file mode 100644 index 00000000000..c03b55c64c6 --- /dev/null +++ b/packages/core/src/test/sagemakerunifiedstudio/shared/smusUtils.test.ts @@ -0,0 +1,579 @@ +/*! + * 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, +} 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('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' + ) + } + ) + }) +}) 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/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' @@ -137,6 +138,7 @@ describe('generateDeployedNode', () => { label: 'iam', getCredentials: sinon.stub(), state: 'valid', + endpointUrl: undefined, } const lambdaDeployedNodeInput = { @@ -147,6 +149,7 @@ describe('generateDeployedNode', () => { regionCode: expectedRegionCode, stackName: expectedStackName, resourceTreeEntity: { + Id: 'MyLambdaFunction', Type: 'AWS::Serverless::Function', }, } @@ -169,7 +172,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) @@ -177,12 +180,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, @@ -237,6 +240,7 @@ describe('generateDeployedNode', () => { regionCode: expectedRegionCode, stackName: expectedStackName, resourceTreeEntity: { + Id: 'my-project-source-bucket-physical-id', Type: 'AWS::S3::Bucket', }, } @@ -256,7 +260,7 @@ describe('generateDeployedNode', () => { const expectedS3BucketName = 'my-project-source-bucket-physical-id' const deployedResourceNodeExplorerNode: S3BucketNode = validateBasicProperties( - deployedResourceNodes, + deployedResourceNodes as DeployedResourceNode[], expectedS3BucketArn, 'awsS3BucketNode', expectedRegionCode, @@ -284,6 +288,7 @@ describe('generateDeployedNode', () => { regionCode: expectedRegionCode, stackName: expectedStackName, resourceTreeEntity: { + Id: 'my-project-apigw-physical-id', Type: 'AWS::Serverless::Api', }, } @@ -330,7 +335,7 @@ describe('generateDeployedNode', () => { ) const deployedResourceNodeExplorerNode: RestApiNode = validateBasicProperties( - deployedResourceNodes, + deployedResourceNodes as DeployedResourceNode[], expectedApiGatewayArn, 'awsApiGatewayNode', expectedRegionCode, @@ -356,6 +361,7 @@ describe('generateDeployedNode', () => { regionCode: expectedRegionCode, stackName: expectedStackName, resourceTreeEntity: { + Id: 'my-unsupported-resource-physical-id', Type: 'AWS::Serverless::UnsupportType', }, } 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..ca01168cd9a 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 @@ -11,7 +11,11 @@ import { } 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 +23,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 +147,7 @@ describe('ResourceNode', () => { Method: undefined, }, ], - } + } satisfies ResourceTreeEntity const workspaceFolder = { uri: vscode.Uri.parse('myworkspace'), name: 'my-workspace', @@ -175,7 +288,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 +297,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 +314,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..650e72bca9f 100644 --- a/packages/core/src/test/shared/awsClientBuilderV3.test.ts +++ b/packages/core/src/test/shared/awsClientBuilderV3.test.ts @@ -60,6 +60,54 @@ 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())) @@ -194,6 +242,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 () { 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 new file mode 100644 index 00000000000..379cce02d3a --- /dev/null +++ b/packages/core/src/test/shared/clients/sagemakerClient.test.ts @@ -0,0 +1,389 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as sinon from 'sinon' +import * as assert from 'assert' +import { SagemakerClient } from '../../../shared/clients/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' + +describe('SagemakerClient.fetchSpaceAppsAndDomains', function () { + const region = 'test-region' + let client: SagemakerClient + let listAppsStub: sinon.SinonStub + + const appDetails: AppDetails[] = [ + { AppName: 'app1', DomainId: 'domain1', SpaceName: 'space1', AppType: 'CodeEditor' }, + { AppName: 'app2', DomainId: 'domain2', SpaceName: 'space2', AppType: 'CodeEditor' }, + { AppName: 'app3', DomainId: 'domain2', SpaceName: 'space3', AppType: 'JupyterLab' }, + ] + + const spaceDetails: SpaceDetails[] = [ + { SpaceName: 'space1', DomainId: 'domain1' }, + { SpaceName: 'space2', DomainId: 'domain2' }, + { SpaceName: 'space3', DomainId: 'domain2' }, + { SpaceName: 'space4', DomainId: 'domain3' }, + ] + + const domain1: DescribeDomainResponse = { DomainId: 'domain1', DomainName: 'domainName1' } + const domain2: DescribeDomainResponse = { DomainId: 'domain2', DomainName: 'domainName2' } + const domain3: DescribeDomainResponse = { + DomainId: 'domain3', + DomainName: 'domainName3', + DomainSettings: { UnifiedStudioSettings: { DomainId: 'unifiedStudioDomain1' } }, + } + + beforeEach(function () { + client = new SagemakerClient(region) + + listAppsStub = sinon.stub(client, 'listApps').returns(intoCollection([appDetails])) + sinon.stub(client, 'listSpaces').returns(intoCollection([spaceDetails])) + sinon.stub(client, 'describeDomain').callsFake(async ({ DomainId }) => { + switch (DomainId) { + case 'domain1': + return domain1 as DescribeDomainCommandOutput + case 'domain2': + return domain2 as DescribeDomainCommandOutput + case 'domain3': + return domain3 as DescribeDomainCommandOutput + default: + return {} as DescribeDomainCommandOutput + } + }) + }) + + afterEach(function () { + sinon.restore() + }) + + it('returns a map of space details with corresponding app details', async function () { + const [spaceApps, domains] = await client.fetchSpaceAppsAndDomains() + + assert.strictEqual(spaceApps.size, 3) + assert.strictEqual(domains.size, 3) + + const spaceAppKey1 = 'domain1__space1' + const spaceAppKey2 = 'domain2__space2' + const spaceAppKey3 = 'domain2__space3' + + assert.ok(spaceApps.has(spaceAppKey1), 'Expected spaceApps to have key for domain1__space1') + assert.ok(spaceApps.has(spaceAppKey2), 'Expected spaceApps to have key for domain2__space2') + assert.ok(spaceApps.has(spaceAppKey3), 'Expected spaceApps to have key for domain2__space3') + + assert.deepStrictEqual(spaceApps.get(spaceAppKey1)?.App?.AppName, 'app1') + assert.deepStrictEqual(spaceApps.get(spaceAppKey2)?.App?.AppName, 'app2') + assert.deepStrictEqual(spaceApps.get(spaceAppKey3)?.App?.AppName, 'app3') + + const domainKey1 = 'domain1' + const domainKey2 = 'domain2' + + assert.ok(domains.has(domainKey1), 'Expected domains to have key for domain1') + assert.ok(domains.has(domainKey2), 'Expected domains to have key for domain2') + + assert.deepStrictEqual(domains.get(domainKey1)?.DomainName, 'domainName1') + assert.deepStrictEqual(domains.get(domainKey2)?.DomainName, 'domainName2') + }) + + it('returns map even if some spaces have no matching apps', async function () { + listAppsStub.returns(intoCollection([{ AppName: 'app1', DomainId: 'domain1', SpaceName: 'space1' }])) + + const [spaceApps] = await client.fetchSpaceAppsAndDomains() + + const spaceAppKey2 = 'domain2__space2' + const spaceAppKey3 = 'domain2__space3' + + assert.strictEqual(spaceApps.size, 3) + assert.strictEqual(spaceApps.get(spaceAppKey2)?.App, undefined) + assert.strictEqual(spaceApps.get(spaceAppKey3)?.App, undefined) + }) + + it('filters out unified studio domains when filterSmusDomains is true', async function () { + const [spaceApps] = await client.fetchSpaceAppsAndDomains(undefined, true) + + 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('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.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' }) + + await assert.rejects( + client.waitForAppInService('domain1', 'space1', 'CodeEditor', 2, 10), + /Timed out waiting for app/ + ) + }) +}) + +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({}) + + await client.startSpace('my-space', 'my-domain') + + 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', + }, + }, + }, + }) + + createAppStub.resolves({}) + + await client.startSpace('my-space', 'my-domain') + + 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', + }, + }) + + 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', + }) + ) + }) + + 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('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({}) + + const promise = client.startSpace('my-space', 'my-domain') + + // Wait for the error message to appear and select "Yes" + await getTestWindow().waitForMessage(/not supported for remote access/) + getTestWindow().getFirstMessage().selectItem('Yes') + + await promise + sinon.assert.calledOnce(updateSpaceStub) + sinon.assert.calledOnce(createAppStub) + }) + + 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 "No" + await getTestWindow().waitForMessage(/not supported for remote access/) + getTestWindow().getFirstMessage().selectItem('No') + + 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..2fdf6b5d14e 100644 --- a/packages/core/src/test/shared/credentials/loginManager.test.ts +++ b/packages/core/src/test/shared/credentials/loginManager.test.ts @@ -12,7 +12,9 @@ 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 } 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 +106,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 +128,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 +136,142 @@ 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: GetCallerIdentityResponse = { + Account: 'AccountId1234', + Arn: 'arn:aws:iam::AccountId1234:user/test-user', + UserId: 'AIDACKCEXAMPLEEXAMPLE', + // @ts-ignore - Adding the $response property for testing + $response: { + httpResponse: { + headers: { + [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: GetCallerIdentityResponse = { + Account: 'AccountId1234', + Arn: 'arn:aws:iam::AccountId1234:user/test-user', + UserId: 'AIDACKCEXAMPLEEXAMPLE', + // @ts-ignore - Adding the $response property for testing + $response: { + httpResponse: { + headers: { + '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 $response property', async function () { + const mockCallerIdentityWithoutResponse: GetCallerIdentityResponse = { + Account: 'AccountId1234', + Arn: 'arn:aws:iam::AccountId1234:user/test-user', + UserId: 'AIDACKCEXAMPLEEXAMPLE', + } + getAccountIdStub.reset() + getAccountIdStub.resolves(mockCallerIdentityWithoutResponse) + + 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/extensionUtilities.test.ts b/packages/core/src/test/shared/extensionUtilities.test.ts index 0fc846f54ab..ce4c764d3d5 100644 --- a/packages/core/src/test/shared/extensionUtilities.test.ts +++ b/packages/core/src/test/shared/extensionUtilities.test.ts @@ -5,11 +5,11 @@ 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' -import { UserActivity, getComputeRegion, initializeComputeRegion } from '../../shared/extensionUtilities' +import { UserActivity, getComputeRegion, initializeComputeRegion, isCn } from '../../shared/extensionUtilities' import { isDifferentVersion, setMostRecentVersion } from '../../shared/extensionUtilities' import { InstanceIdentity } from '../../shared/clients/ec2MetadataClient' import { extensionVersion } from '../../shared/vscode/env' @@ -18,6 +18,8 @@ import globals from '../../shared/extensionGlobals' import { maybeShowMinVscodeWarning } from '../../shared/extensionStartup' import { getTestWindow } from './vscode/window' import { assertTelemetry } from '../testUtil' +import { isSageMaker } from '../../shared/extensionUtilities' +import { hasSageMakerEnvVars } from '../../shared/vscode/env' describe('extensionUtilities', function () { it('maybeShowMinVscodeWarning', async () => { @@ -96,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') @@ -135,6 +137,73 @@ describe('initializeComputeRegion, getComputeRegion', async function () { }) }) +describe('isCn', function () { + let sandbox: sinon.SinonSandbox + const metadataService = new DefaultEc2MetadataClient() + + beforeEach(function () { + sandbox = sinon.createSandbox() + }) + + afterEach(function () { + sandbox.restore() + }) + + it('returns false when compute region is not defined', async function () { + // Reset the compute region to undefined first + const utils = require('../../shared/extensionUtilities') + Object.defineProperty(utils, 'computeRegion', { + value: undefined, + configurable: true, + }) + + const result = isCn() + + assert.strictEqual(result, false, 'isCn() should return false when compute region is undefined') + }) + + it('returns false when compute region is not initialized', async function () { + // Set the compute region to "notInitialized" + const utils = require('../../shared/extensionUtilities') + Object.defineProperty(utils, 'computeRegion', { + value: 'notInitialized', + configurable: true, + }) + + const result = isCn() + + assert.strictEqual(result, false, 'isCn() should return false when compute region is notInitialized') + }) + + it('returns true for CN regions', async function () { + sandbox.stub(metadataService, 'getInstanceIdentity').resolves({ region: 'cn-north-1' }) + await initializeComputeRegion(metadataService, false, true) + + const result = isCn() + + assert.strictEqual(result, true, 'isCn() should return true for China regions') + }) + + it('returns false for non-CN regions', async function () { + sandbox.stub(metadataService, 'getInstanceIdentity').resolves({ region: 'us-east-1' }) + await initializeComputeRegion(metadataService, false, true) + + const result = isCn() + + assert.strictEqual(result, false, 'isCn() should return false for non-China regions') + }) + + it('returns false when an error occurs', async function () { + const utils = require('../../shared/extensionUtilities') + + sandbox.stub(utils, 'getComputeRegion').throws(new Error('Test error')) + + const result = isCn() + + assert.strictEqual(result, false, 'isCn() should return false when an error occurs') + }) +}) + describe('UserActivity', function () { let count: number let sandbox: sinon.SinonSandbox @@ -294,3 +363,143 @@ describe('UserActivity', function () { return event.event } }) + +describe('isSageMaker', function () { + let sandbox: sinon.SinonSandbox + const env = require('../../shared/vscode/env') + const utils = require('../../shared/extensionUtilities') + + beforeEach(function () { + sandbox = sinon.createSandbox() + utils.resetSageMakerState() + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('SMAI detection', function () { + it('returns true when both app name and env vars match', function () { + sandbox.stub(vscode.env, 'appName').value('SageMaker Code Editor') + sandbox.stub(env, 'hasSageMakerEnvVars').returns(true) + + assert.strictEqual(isSageMaker('SMAI'), true) + }) + + it('returns false when app name is different', function () { + sandbox.stub(vscode.env, 'appName').value('Visual Studio Code') + sandbox.stub(env, 'hasSageMakerEnvVars').returns(true) + + assert.strictEqual(isSageMaker('SMAI'), false) + }) + + it('returns false when env vars are missing', function () { + sandbox.stub(vscode.env, 'appName').value('SageMaker Code Editor') + sandbox.stub(env, 'hasSageMakerEnvVars').returns(false) + + assert.strictEqual(isSageMaker('SMAI'), false) + }) + + it('defaults to SMAI when no parameter provided', function () { + sandbox.stub(vscode.env, 'appName').value('SageMaker Code Editor') + sandbox.stub(env, 'hasSageMakerEnvVars').returns(true) + + assert.strictEqual(isSageMaker(), true) + }) + }) + + describe('SMUS detection', function () { + it('returns true when all conditions are met', function () { + sandbox.stub(vscode.env, 'appName').value('SageMaker Code Editor') + sandbox.stub(env, 'hasSageMakerEnvVars').returns(true) + sandbox.stub(process, 'env').value({ SERVICE_NAME: 'SageMakerUnifiedStudio' }) + utils.resetSageMakerState() + + assert.strictEqual(isSageMaker('SMUS'), true) + }) + + it('returns false when unified studio is missing', function () { + sandbox.stub(vscode.env, 'appName').value('SageMaker Code Editor') + sandbox.stub(env, 'hasSageMakerEnvVars').returns(true) + sandbox.stub(process, 'env').value({ SERVICE_NAME: 'SomeOtherService' }) + utils.resetSageMakerState() + + assert.strictEqual(isSageMaker('SMUS'), false) + }) + + it('returns false when env vars are missing', function () { + sandbox.stub(vscode.env, 'appName').value('SageMaker Code Editor') + sandbox.stub(env, 'hasSageMakerEnvVars').returns(false) + sandbox.stub(process, 'env').value({ SERVICE_NAME: 'SageMakerUnifiedStudio' }) + utils.resetSageMakerState() + + assert.strictEqual(isSageMaker('SMUS'), false) + }) + + it('returns false when app name is different', function () { + sandbox.stub(vscode.env, 'appName').value('Visual Studio Code') + sandbox.stub(env, 'hasSageMakerEnvVars').returns(true) + sandbox.stub(process, 'env').value({ SERVICE_NAME: 'SageMakerUnifiedStudio' }) + utils.resetSageMakerState() + + assert.strictEqual(isSageMaker('SMUS'), false) + }) + }) + + it('returns false for invalid appName parameter', function () { + sandbox.stub(vscode.env, 'appName').value('SageMaker Code Editor') + sandbox.stub(env, 'hasSageMakerEnvVars').returns(true) + + // @ts-ignore - Testing invalid input + assert.strictEqual(isSageMaker('INVALID'), false) + }) +}) + +describe('hasSageMakerEnvVars', function () { + let sandbox: sinon.SinonSandbox + + beforeEach(function () { + sandbox = sinon.createSandbox() + }) + + afterEach(function () { + sandbox.restore() + }) + + it('detects SageMaker environment variables', function () { + // Test SAGEMAKER_ prefix + sandbox.stub(process, 'env').value({ SAGEMAKER_APP_TYPE: 'JupyterServer' }) + assert.strictEqual(hasSageMakerEnvVars(), true) + + // Test SM_ prefix + sandbox.stub(process, 'env').value({ SM_APP_TYPE: 'CodeEditor' }) + assert.strictEqual(hasSageMakerEnvVars(), true) + + // Test SERVICE_NAME with correct value + sandbox.stub(process, 'env').value({ SERVICE_NAME: 'SageMakerUnifiedStudio' }) + assert.strictEqual(hasSageMakerEnvVars(), true) + + // Test STUDIO_LOGGING_DIR with correct path + sandbox.stub(process, 'env').value({ STUDIO_LOGGING_DIR: '/var/log/studio/app.log' }) + assert.strictEqual(hasSageMakerEnvVars(), true) + + // Test invalid SERVICE_NAME + sandbox.stub(process, 'env').value({ SERVICE_NAME: 'SomeOtherService' }) + assert.strictEqual(hasSageMakerEnvVars(), false) + + // Test invalid STUDIO_LOGGING_DIR + sandbox.stub(process, 'env').value({ STUDIO_LOGGING_DIR: '/var/log/other/app.log' }) + assert.strictEqual(hasSageMakerEnvVars(), false) + + // Test multiple env vars + sandbox.stub(process, 'env').value({ + SAGEMAKER_APP_TYPE: 'JupyterServer', + SM_APP_TYPE: 'CodeEditor', + }) + assert.strictEqual(hasSageMakerEnvVars(), true) + + // Test no env vars + sandbox.stub(process, 'env').value({}) + assert.strictEqual(hasSageMakerEnvVars(), false) + }) +}) 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/lsp/utils/platform.test.ts b/packages/core/src/test/shared/lsp/utils/platform.test.ts new file mode 100644 index 00000000000..862bd06f990 --- /dev/null +++ b/packages/core/src/test/shared/lsp/utils/platform.test.ts @@ -0,0 +1,209 @@ +/*! + * 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 { createServerOptions } from '../../../../shared/lsp/utils/platform' +import * as extensionUtilities from '../../../../shared/extensionUtilities' +import * as env from '../../../../shared/vscode/env' +import { ChildProcess } from '../../../../shared/utilities/processUtils' + +describe('createServerOptions - SageMaker Authentication', function () { + let sandbox: sinon.SinonSandbox + let isSageMakerStub: sinon.SinonStub + let isRemoteWorkspaceStub: sinon.SinonStub + let executeCommandStub: sinon.SinonStub + + beforeEach(function () { + sandbox = sinon.createSandbox() + + isSageMakerStub = sandbox.stub(extensionUtilities, 'isSageMaker') + isRemoteWorkspaceStub = sandbox.stub(env, 'isRemoteWorkspace') + sandbox.stub(env, 'isDebugInstance').returns(false) + executeCommandStub = sandbox.stub(vscode.commands, 'executeCommand') + + sandbox.stub(ChildProcess.prototype, 'run').resolves() + sandbox.stub(ChildProcess.prototype, 'send').resolves() + sandbox.stub(ChildProcess.prototype, 'proc').returns({} as any) + }) + + afterEach(function () { + sandbox.restore() + }) + + // jscpd:ignore-start + it('sets USE_IAM_AUTH=true when authMode is Iam', async function () { + isSageMakerStub.returns(true) + executeCommandStub.withArgs('sagemaker.parseCookies').resolves({ authMode: 'Iam' }) + + // Capture constructor arguments using sinon stub + let capturedOptions: any = undefined + const childProcessConstructorSpy = sandbox.stub().callsFake((command: string, args: string[], options: any) => { + capturedOptions = options + // Create a fake instance with the methods we need + const fakeInstance = { + run: sandbox.stub().resolves(), + send: sandbox.stub().resolves(), + proc: sandbox.stub().returns({}), + pid: sandbox.stub().returns(12345), + stop: sandbox.stub(), + stopped: false, + } + return fakeInstance + }) + + // Replace ChildProcess constructor + sandbox.replace( + require('../../../../shared/utilities/processUtils'), + 'ChildProcess', + childProcessConstructorSpy + ) + + const serverOptions = createServerOptions({ + encryptionKey: Buffer.from('test-key'), + executable: ['node'], + serverModule: 'test-module.js', + execArgv: ['--stdio'], + }) + + await serverOptions() + + assert(capturedOptions, 'ChildProcess constructor should have been called') + assert(capturedOptions.spawnOptions, 'spawnOptions should be defined') + assert(capturedOptions.spawnOptions.env, 'spawnOptions.env should be defined') + assert.equal(capturedOptions.spawnOptions.env.USE_IAM_AUTH, 'true') + }) + + it('does not set USE_IAM_AUTH when authMode is Sso', async function () { + isSageMakerStub.returns(true) + executeCommandStub.withArgs('sagemaker.parseCookies').resolves({ authMode: 'Sso' }) + + // Capture constructor arguments using sinon stub + let capturedOptions: any = undefined + const childProcessConstructorSpy = sandbox.stub().callsFake((command: string, args: string[], options: any) => { + capturedOptions = options + // Create a fake instance with the methods we need + const fakeInstance = { + run: sandbox.stub().resolves(), + send: sandbox.stub().resolves(), + proc: sandbox.stub().returns({}), + pid: sandbox.stub().returns(12345), + stop: sandbox.stub(), + stopped: false, + } + return fakeInstance + }) + + // Replace ChildProcess constructor + sandbox.replace( + require('../../../../shared/utilities/processUtils'), + 'ChildProcess', + childProcessConstructorSpy + ) + + const serverOptions = createServerOptions({ + encryptionKey: Buffer.from('test-key'), + executable: ['node'], + serverModule: 'test-module.js', + execArgv: ['--stdio'], + }) + + await serverOptions() + + assert(capturedOptions, 'ChildProcess constructor should have been called') + assert(capturedOptions.spawnOptions, 'spawnOptions should be defined') + assert(capturedOptions.spawnOptions.env, 'spawnOptions.env should be defined') + assert.equal(capturedOptions.spawnOptions.env.USE_IAM_AUTH, undefined) + }) + + it('defaults to IAM auth when parseCookies fails', async function () { + isSageMakerStub.returns(true) + isRemoteWorkspaceStub.returns(false) + executeCommandStub.withArgs('sagemaker.parseCookies').rejects(new Error('Command failed')) + + // Capture constructor arguments using sinon stub + let capturedOptions: any = undefined + const childProcessConstructorSpy = sandbox.stub().callsFake((command: string, args: string[], options: any) => { + capturedOptions = options + // Create a fake instance with the methods we need + const fakeInstance = { + run: sandbox.stub().resolves(), + send: sandbox.stub().resolves(), + proc: sandbox.stub().returns({}), + pid: sandbox.stub().returns(12345), + stop: sandbox.stub(), + stopped: false, + } + return fakeInstance + }) + + // Replace ChildProcess constructor + sandbox.replace( + require('../../../../shared/utilities/processUtils'), + 'ChildProcess', + childProcessConstructorSpy + ) + + const serverOptions = createServerOptions({ + encryptionKey: Buffer.from('test-key'), + executable: ['node'], + serverModule: 'test-module.js', + execArgv: ['--stdio'], + }) + + await serverOptions() + + assert(capturedOptions, 'ChildProcess constructor should have been called') + assert(capturedOptions.spawnOptions, 'spawnOptions should be defined') + assert(capturedOptions.spawnOptions.env, 'spawnOptions.env should be defined') + assert.equal(capturedOptions.spawnOptions.env.USE_IAM_AUTH, 'true') + }) + + it('does not default to IAM in remote workspace without SMUS', async function () { + isSageMakerStub.returns(true) + isRemoteWorkspaceStub.returns(true) + process.env.SERVICE_NAME = 'OtherService' + executeCommandStub.withArgs('sagemaker.parseCookies').rejects(new Error('Command failed')) + + // Capture constructor arguments using sinon stub + let capturedOptions: any = undefined + const childProcessConstructorSpy = sandbox.stub().callsFake((command: string, args: string[], options: any) => { + capturedOptions = options + // Create a fake instance with the methods we need + const fakeInstance = { + run: sandbox.stub().resolves(), + send: sandbox.stub().resolves(), + proc: sandbox.stub().returns({}), + pid: sandbox.stub().returns(12345), + stop: sandbox.stub(), + stopped: false, + } + return fakeInstance + }) + + // Replace ChildProcess constructor + sandbox.replace( + require('../../../../shared/utilities/processUtils'), + 'ChildProcess', + childProcessConstructorSpy + ) + + const serverOptions = createServerOptions({ + encryptionKey: Buffer.from('test-key'), + executable: ['node'], + serverModule: 'test-module.js', + execArgv: ['--stdio'], + }) + + await serverOptions() + + assert(capturedOptions, 'ChildProcess constructor should have been called') + assert(capturedOptions.spawnOptions, 'spawnOptions should be defined') + assert(capturedOptions.spawnOptions.env, 'spawnOptions.env should be defined') + assert.equal(capturedOptions.spawnOptions.env.USE_IAM_AUTH, undefined) + }) + // jscpd:ignore-end +}) 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/debugger/samDebugConfigProvider.test.ts b/packages/core/src/test/shared/sam/debugger/samDebugConfigProvider.test.ts index 9fe7c76e842..ccdf113b580 100644 --- a/packages/core/src/test/shared/sam/debugger/samDebugConfigProvider.test.ts +++ b/packages/core/src/test/shared/sam/debugger/samDebugConfigProvider.test.ts @@ -317,7 +317,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: '' })) @@ -2906,7 +2911,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/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 96ca450ae14..b828859586b 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' @@ -82,6 +82,16 @@ describe('VscodeRemoteSshConfig', async function () { const command = result.unwrap() assert.strictEqual(command, testProxyCommand) }) + + it('uses %n token for sagemaker_connect to preserve hostname case', async function () { + const sagemakerConfig = new MockSshConfig('sshPath', 'testHostNamePrefix', 'sagemaker_connect') + sagemakerConfig.testIsWin = false + + const result = await sagemakerConfig.getProxyCommandWrapper('sagemaker_connect') + assert.ok(result.isOk()) + const command = result.unwrap() + assert.strictEqual(command, `'sagemaker_connect' '%n'`) + }) }) describe('matchSshSection', async function () { diff --git a/packages/core/src/test/shared/telemetry/util.test.ts b/packages/core/src/test/shared/telemetry/util.test.ts index 8d6f3ddc53f..059d86a891e 100644 --- a/packages/core/src/test/shared/telemetry/util.test.ts +++ b/packages/core/src/test/shared/telemetry/util.test.ts @@ -24,6 +24,10 @@ import { randomUUID } from 'crypto' import { isUuid } from '../../../shared/crypto' import { MetricDatum } from '../../../shared/telemetry/clienttelemetry' import { assertLogsContain } from '../../globalSetup.test' +import { getClientName } from '../../../shared/telemetry/util' +import * as extensionUtilities from '../../../shared/extensionUtilities' +import * as sinon from 'sinon' +import * as vscode from 'vscode' describe('TelemetryConfig', function () { const settingKey = 'aws.telemetry' @@ -391,3 +395,70 @@ describe('validateMetricEvent', function () { assertLogsContain('invalid Metric', false, 'warn') }) }) + +describe('getClientName', function () { + let sandbox: sinon.SinonSandbox + let isSageMakerStub: sinon.SinonStub + + beforeEach(function () { + sandbox = sinon.createSandbox() + isSageMakerStub = sandbox.stub(extensionUtilities, 'isSageMaker') + }) + + afterEach(function () { + sandbox.restore() + }) + + it('returns "AmazonQ-For-SMUS-CE" when in SMUS environment', function () { + isSageMakerStub.withArgs('SMUS').returns(true) + sandbox.stub(vscode.env, 'appName').value('SageMaker Code Editor') + + const result = getClientName() + + assert.strictEqual(result, 'AmazonQ-For-SMUS-CE') + assert.ok(isSageMakerStub.calledOnceWith('SMUS')) + }) + + it('returns "AmazonQ-For-SMAI-CE" when in SMAI environment', function () { + isSageMakerStub.withArgs('SMUS').returns(false) + isSageMakerStub.withArgs('SMAI').returns(true) + sandbox.stub(vscode.env, 'appName').value('SageMaker Code Editor') + + const result = getClientName() + + assert.strictEqual(result, 'AmazonQ-For-SMAI-CE') + assert.ok(isSageMakerStub.calledWith('SMUS')) + assert.ok(isSageMakerStub.calledWith('SMAI')) + }) + + it('returns vscode app name when not in SMUS environment', function () { + const mockAppName = 'Visual Studio Code' + isSageMakerStub.withArgs('SMUS').returns(false) + isSageMakerStub.withArgs('SMAI').returns(false) + sandbox.stub(vscode.env, 'appName').value(mockAppName) + + const result = getClientName() + + assert.strictEqual(result, mockAppName) + assert.ok(isSageMakerStub.calledWith('SMUS')) + assert.ok(isSageMakerStub.calledWith('SMAI')) + }) + + it('handles undefined app name gracefully', function () { + isSageMakerStub.withArgs('SMUS').returns(false) + sandbox.stub(vscode.env, 'appName').value(undefined) + + const result = getClientName() + + assert.strictEqual(result, undefined) + }) + + it('prioritizes SMUS detection over app name', function () { + isSageMakerStub.withArgs('SMUS').returns(true) + sandbox.stub(vscode.env, 'appName').value('SageMaker Code Editor') + + const result = getClientName() + + assert.strictEqual(result, 'AmazonQ-For-SMUS-CE') + }) +}) diff --git a/packages/core/src/test/shared/utilities/functionUtils.test.ts b/packages/core/src/test/shared/utilities/functionUtils.test.ts index 7880d11ff63..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) => { @@ -152,6 +188,33 @@ describe('debounce', function () { assert.strictEqual(counter, 2) }) + describe('useLastCall option', function () { + let args: number[] + let clock: ReturnType + let addToArgs: (i: number) => void + + before(function () { + args = [] + clock = installFakeClock() + addToArgs = (n: number) => args.push(n) + }) + + afterEach(function () { + clock.uninstall() + args.length = 0 + }) + + it('only calls with the last args', async function () { + const debounced = debounce(addToArgs, 10, true) + const p1 = debounced(1) + const p2 = debounced(2) + const p3 = debounced(3) + await clock.tickAsync(100) + await Promise.all([p1, p2, p3]) + assert.deepStrictEqual(args, [3]) + }) + }) + describe('window rolling', function () { let clock: ReturnType const calls: ReturnType[] = [] 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/stepFunctions/explorer/stepFunctionNodes.test.ts b/packages/core/src/test/stepFunctions/explorer/stepFunctionNodes.test.ts index 0f6df30e3e7..61b4f2b1813 100644 --- a/packages/core/src/test/stepFunctions/explorer/stepFunctionNodes.test.ts +++ b/packages/core/src/test/stepFunctions/explorer/stepFunctionNodes.test.ts @@ -15,14 +15,14 @@ import { } from '../../utilities/explorerNodeAssertions' import { asyncGenerator } from '../../../shared/utilities/collectionUtils' import globals from '../../../shared/extensionGlobals' -import { DefaultStepFunctionsClient } from '../../../shared/clients/stepFunctionsClient' +import { StepFunctionsClient } from '../../../shared/clients/stepFunctions' import { stub } from '../../utilities/stubber' const regionCode = 'someregioncode' describe('StepFunctionsNode', function () { function createStatesClient(...stateMachineNames: string[]) { - const client = stub(DefaultStepFunctionsClient, { regionCode }) + const client = stub(StepFunctionsClient, { regionCode }) client.listStateMachines.returns( asyncGenerator( stateMachineNames.map((name) => { diff --git a/packages/core/src/test/stepFunctions/workflowStudio/workflowStudioApiHandler.test.ts b/packages/core/src/test/stepFunctions/workflowStudio/workflowStudioApiHandler.test.ts index 32c9160c1c1..c16534abc4d 100644 --- a/packages/core/src/test/stepFunctions/workflowStudio/workflowStudioApiHandler.test.ts +++ b/packages/core/src/test/stepFunctions/workflowStudio/workflowStudioApiHandler.test.ts @@ -16,7 +16,7 @@ import { } from '../../../stepFunctions/workflowStudio/types' import * as vscode from 'vscode' import { assertTelemetry } from '../../testUtil' -import { DefaultStepFunctionsClient } from '../../../shared/clients/stepFunctionsClient' +import { StepFunctionsClient } from '../../../shared/clients/stepFunctions' import { IamClient } from '../../../shared/clients/iam' describe('WorkflowStudioApiHandler', function () { @@ -64,7 +64,7 @@ describe('WorkflowStudioApiHandler', function () { fileId: '', } - const sfnClient = new DefaultStepFunctionsClient('us-east-1') + const sfnClient = new StepFunctionsClient('us-east-1') apiHandler = new WorkflowStudioApiHandler('us-east-1', context, { sfn: sfnClient, iam: new IamClient('us-east-1'), 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/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 index 0038795ad89..d173500c608 100644 --- a/packages/core/src/testE2E/codewhisperer/referenceTracker.test.ts +++ b/packages/core/src/testE2E/codewhisperer/referenceTracker.test.ts @@ -14,7 +14,6 @@ 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. diff --git a/packages/core/src/testE2E/codewhisperer/serviceInvocations.test.ts b/packages/core/src/testE2E/codewhisperer/serviceInvocations.test.ts index d4265d13982..37f32b130dd 100644 --- a/packages/core/src/testE2E/codewhisperer/serviceInvocations.test.ts +++ b/packages/core/src/testE2E/codewhisperer/serviceInvocations.test.ts @@ -98,7 +98,7 @@ describe('CodeWhisperer service invocation', async function () { 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', 'main.go') + const appCodePath = path.join(appRoot, 'hello-world', 'go.mod') // check that handler is empty before invocation const requestIdBefore = RecommendationHandler.instance.requestId 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/perf/buildIndex.test.ts b/packages/core/src/testInteg/perf/buildIndex.test.ts deleted file mode 100644 index d60de3bdc3a..00000000000 --- a/packages/core/src/testInteg/perf/buildIndex.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { performanceTest } from '../../shared/performance/performance' -import * as sinon from 'sinon' -import * as vscode from 'vscode' -import assert from 'assert' -import { LspClient, LspController } from '../../amazonq' -import { LanguageClient, ServerOptions } from 'vscode-languageclient' -import { createTestWorkspace } from '../../test/testUtil' -import { BuildIndexRequestType, GetUsageRequestType } from '../../amazonq/lsp/types' -import { fs, getRandomString } from '../../shared' -import { FileSystem } from '../../shared/fs/fs' -import { getFsCallsUpperBound } from './utilities' - -interface SetupResult { - clientReqStub: sinon.SinonStub - fsSpy: sinon.SinonSpiedInstance - findFilesSpy: sinon.SinonSpy -} - -async function verifyResult(setup: SetupResult) { - // A correct run makes 2 requests, but don't want to make it exact to avoid over-sensitivity to implementation. If we make 10+ something is likely wrong. - assert.ok(setup.clientReqStub.callCount >= 2 && setup.clientReqStub.callCount <= 10) - assert.ok(setup.clientReqStub.calledWith(BuildIndexRequestType)) - assert.ok(setup.clientReqStub.calledWith(GetUsageRequestType)) - - assert.strictEqual(getFsCallsUpperBound(setup.fsSpy), 0, 'should not make any fs calls') - assert.ok(setup.findFilesSpy.callCount <= 2, 'findFiles should not be called more than twice') -} - -async function setupWithWorkspace(numFiles: number, options: { fileContent: string }): Promise { - // Force VSCode to find my test workspace only to keep test contained and controlled. - const testWorksapce = await createTestWorkspace(numFiles, options) - sinon.stub(vscode.workspace, 'workspaceFolders').value([testWorksapce]) - - // Avoid sending real request to lsp. - const clientReqStub = sinon.stub(LanguageClient.prototype, 'sendRequest').resolves(true) - const fsSpy = sinon.spy(fs) - const findFilesSpy = sinon.spy(vscode.workspace, 'findFiles') - LspClient.instance.client = new LanguageClient('amazonq', 'test-client', {} as ServerOptions, {}) - return { clientReqStub, fsSpy, findFilesSpy } -} - -describe('buildIndex', function () { - describe('performanceTests', function () { - afterEach(function () { - sinon.restore() - }) - performanceTest({}, 'indexing many small files', function () { - return { - setup: async () => setupWithWorkspace(250, { fileContent: '0123456789' }), - execute: async () => { - await LspController.instance.buildIndex({ - startUrl: '', - maxIndexSize: 30, - isVectorIndexEnabled: true, - }) - }, - verify: verifyResult, - } - }) - performanceTest({}, 'indexing few large files', function () { - return { - setup: async () => setupWithWorkspace(10, { fileContent: getRandomString(1000) }), - execute: async () => { - await LspController.instance.buildIndex({ - startUrl: '', - maxIndexSize: 30, - isVectorIndexEnabled: true, - }) - }, - verify: verifyResult, - } - }) - }) -}) diff --git a/packages/core/src/testInteg/perf/prepareRepoData.test.ts b/packages/core/src/testInteg/perf/prepareRepoData.test.ts deleted file mode 100644 index c1ba1df1223..00000000000 --- a/packages/core/src/testInteg/perf/prepareRepoData.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -/*! - * 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 { WorkspaceFolder } from 'vscode' -import { getEqualOSTestOptions, performanceTest } from '../../shared/performance/performance' -import { createTestWorkspace } from '../../test/testUtil' -import { prepareRepoData, TelemetryHelper } from '../../amazonqFeatureDev' -import { AmazonqCreateUpload, fs, getRandomString } from '../../shared' -import { Span } from '../../shared/telemetry' -import { FileSystem } from '../../shared/fs/fs' -import { getFsCallsUpperBound } from './utilities' - -type resultType = { - zipFileBuffer: Buffer - zipFileChecksum: string -} - -type setupResult = { - workspace: WorkspaceFolder - fsSpy: sinon.SinonSpiedInstance - numFiles: number - fileSize: number -} - -function performanceTestWrapper(numFiles: number, fileSize: number) { - return performanceTest( - getEqualOSTestOptions({ - userCpuUsage: 200, - systemCpuUsage: 35, - heapTotal: 20, - }), - `handles ${numFiles} files of size ${fileSize} bytes`, - function () { - const telemetry = new TelemetryHelper() - return { - setup: async () => { - const fsSpy = sinon.spy(fs) - const workspace = await createTestWorkspace(numFiles, { - fileNamePrefix: 'file', - fileContent: getRandomString(fileSize), - fileNameSuffix: '.md', - }) - return { workspace, fsSpy, numFiles, fileSize } - }, - execute: async (setup: setupResult) => { - return await prepareRepoData( - [setup.workspace.uri.fsPath], - [setup.workspace], - { - record: () => {}, - } as unknown as Span, - { telemetry } - ) - }, - verify: async (setup: setupResult, result: resultType) => { - verifyResult(setup, result, telemetry, numFiles * fileSize) - }, - } - } - ) -} - -function verifyResult(setup: setupResult, result: resultType, telemetry: TelemetryHelper, expectedSize: number): void { - assert.ok(result) - assert.strictEqual(Buffer.isBuffer(result.zipFileBuffer), true) - assert.strictEqual(telemetry.repositorySize, expectedSize) - assert.strictEqual(result.zipFileChecksum.length, 44) - assert.ok(getFsCallsUpperBound(setup.fsSpy) <= setup.numFiles * 8, 'total system calls should be under 8 per file') -} - -describe('prepareRepoData', function () { - describe('Performance Tests', function () { - afterEach(function () { - sinon.restore() - }) - performanceTestWrapper(10, 1000) - performanceTestWrapper(50, 500) - performanceTestWrapper(100, 100) - performanceTestWrapper(250, 10) - }) -}) diff --git a/packages/core/src/testInteg/perf/registerNewFiles.test.ts b/packages/core/src/testInteg/perf/registerNewFiles.test.ts deleted file mode 100644 index 716e79d4e48..00000000000 --- a/packages/core/src/testInteg/perf/registerNewFiles.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -/*! - * 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 { featureDevScheme } from '../../amazonqFeatureDev' -import { getEqualOSTestOptions, performanceTest } from '../../shared/performance/performance' -import { getTestWorkspaceFolder } from '../integrationTestsUtilities' -import { VirtualFileSystem } from '../../shared' -import { registerNewFiles } from '../../amazonq/util/files' -import { NewFileInfo, NewFileZipContents } from '../../amazonq' - -interface SetupResult { - workspace: vscode.WorkspaceFolder - fileContents: NewFileZipContents[] - vfsSpy: sinon.SinonSpiedInstance - vfs: VirtualFileSystem -} - -function getFileContents(numFiles: number, fileSize: number): NewFileZipContents[] { - return Array.from({ length: numFiles }, (_, i) => { - return { - zipFilePath: `test-path-${i}`, - fileContent: 'x'.repeat(fileSize), - } - }) -} - -function performanceTestWrapper(label: string, numFiles: number, fileSize: number) { - const conversationId = 'test-conversation' - return performanceTest( - getEqualOSTestOptions({ - userCpuUsage: 300, - systemCpuUsage: 35, - heapTotal: 20, - }), - label, - function () { - return { - setup: async () => { - const testWorkspaceUri = vscode.Uri.file(getTestWorkspaceFolder()) - const fileContents = getFileContents(numFiles, fileSize) - const vfs = new VirtualFileSystem() - const vfsSpy = sinon.spy(vfs) - - return { - workspace: { - uri: testWorkspaceUri, - name: 'test-workspace', - index: 0, - }, - fileContents: fileContents, - vfsSpy: vfsSpy, - vfs: vfs, - } - }, - execute: async (setup: SetupResult) => { - return registerNewFiles( - setup.vfs, - setup.fileContents, - 'test-upload-id', - [setup.workspace], - conversationId, - featureDevScheme - ) - }, - verify: async (setup: SetupResult, result: NewFileInfo[]) => { - assert.strictEqual(result.length, numFiles) - assert.ok( - setup.vfsSpy.registerProvider.callCount <= numFiles, - 'only register each file once in vfs' - ) - }, - } - } - ) -} - -describe('registerNewFiles', function () { - describe('performance tests', function () { - performanceTestWrapper('1x10MB', 1, 10000) - performanceTestWrapper('10x1000B', 10, 1000) - performanceTestWrapper('100x100B', 100, 100) - performanceTestWrapper('1000x10B', 1000, 10) - performanceTestWrapper('10000x1B', 10000, 1) - }) -}) diff --git a/packages/core/src/testInteg/perf/zipcode.test.ts b/packages/core/src/testInteg/perf/zipcode.test.ts index f5e81086152..71303e493c9 100644 --- a/packages/core/src/testInteg/perf/zipcode.test.ts +++ b/packages/core/src/testInteg/perf/zipcode.test.ts @@ -54,7 +54,6 @@ function performanceTestWrapper(numberOfFiles: number, fileSize: number) { path: setup.tempDir, name: setup.tempFileName, }, - humanInTheLoopFlag: false, projectPath: setup.tempDir, zipManifest: setup.transformQManifest, }) 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/testLint/eslint.test.ts b/packages/core/src/testLint/eslint.test.ts index ccf670a1cee..fc3607008bb 100644 --- a/packages/core/src/testLint/eslint.test.ts +++ b/packages/core/src/testLint/eslint.test.ts @@ -12,6 +12,8 @@ describe('eslint', function () { it('passes eslint', function () { const result = runCmd( [ + 'node', + '--max-old-space-size=8192', '../../node_modules/.bin/eslint', '-c', '../../.eslintrc.js', diff --git a/packages/core/src/testLint/gitSecrets.test.ts b/packages/core/src/testLint/gitSecrets.test.ts index fce29585d1c..49091665ea1 100644 --- a/packages/core/src/testLint/gitSecrets.test.ts +++ b/packages/core/src/testLint/gitSecrets.test.ts @@ -23,7 +23,12 @@ describe('git-secrets', function () { /** git-secrets patterns that will not cause a failure during the scan */ function setAllowListPatterns(gitSecrets: string) { - const allowListPatterns: string[] = ['"accountId": "123456789012"'] + const allowListPatterns: string[] = [ + '"accountId": "123456789012"', + "'accountId': '123456789012'", + "Account: '123456789012'", + "accountId: '123456789012'", + ] for (const pattern of allowListPatterns) { // Returns non-zero exit code if pattern already exists diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 3baba69a575..702a86ee8e6 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -7,5 +7,6 @@ "declaration": true, "declarationMap": true }, - "exclude": ["node_modules", ".vscode-test", "src/testFixtures", "dist"] + "exclude": ["node_modules", ".vscode-test", "src/testFixtures", "dist"], + "noEmitOnError": false // allow emitting even with type errors } diff --git a/packages/core/webpack.config.js b/packages/core/webpack.config.js index fba19d133b2..b58c990704a 100644 --- a/packages/core/webpack.config.js +++ b/packages/core/webpack.config.js @@ -23,6 +23,7 @@ module.exports = (env, argv) => { ...baseConfig, entry: { 'src/stepFunctions/asl/aslServer': './src/stepFunctions/asl/aslServer.ts', + 'src/awsService/sagemaker/detached-server/server': './src/awsService/sagemaker/detached-server/server.ts', }, } diff --git a/packages/toolkit/.changes/3.66.0.json b/packages/toolkit/.changes/3.66.0.json new file mode 100644 index 00000000000..60eeb3bb16f --- /dev/null +++ b/packages/toolkit/.changes/3.66.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-06-18", + "version": "3.66.0", + "entries": [] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.67.0.json b/packages/toolkit/.changes/3.67.0.json new file mode 100644 index 00000000000..21522dc5d87 --- /dev/null +++ b/packages/toolkit/.changes/3.67.0.json @@ -0,0 +1,18 @@ +{ + "date": "2025-06-25", + "version": "3.67.0", + "entries": [ + { + "type": "Bug Fix", + "description": "State Machine deployments can now be initiated directly from Workflow Studio without closing the editor" + }, + { + "type": "Bug Fix", + "description": "Step Function performance metrics now accurately reflect only Workflow Studio document activity" + }, + { + "type": "Feature", + "description": "AccessAnalyzer: CheckNoPublicAccess custom policy check supports additional resource types." + } + ] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.68.0.json b/packages/toolkit/.changes/3.68.0.json new file mode 100644 index 00000000000..2c650f157ad --- /dev/null +++ b/packages/toolkit/.changes/3.68.0.json @@ -0,0 +1,18 @@ +{ + "date": "2025-07-03", + "version": "3.68.0", + "entries": [ + { + "type": "Bug Fix", + "description": "[StepFunctions]: Cannot call TestState with variables in Workflow Studio" + }, + { + "type": "Feature", + "description": "Lambda to SAM Transformation: AWS Toolkit Explorer now can convert existing Lambda functions into SAM (Serverless Application Model) projects. This conversion creates a project structure that's ready for local development and can be managed using Application Builder" + }, + { + "type": "Feature", + "description": "Lambda Quick Edit: AWS Toolkit Explorer now offers a streamlined editing experience for Lambda functions. Download a function's code with double-click, make local modifications, and easily synchronize changes back to the cloud." + } + ] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.69.0.json b/packages/toolkit/.changes/3.69.0.json new file mode 100644 index 00000000000..dc2d04d1582 --- /dev/null +++ b/packages/toolkit/.changes/3.69.0.json @@ -0,0 +1,46 @@ +{ + "date": "2025-07-16", + "version": "3.69.0", + "entries": [ + { + "type": "Bug Fix", + "description": "SageMaker: Enable per-region manual filtering of Spaces" + }, + { + "type": "Bug Fix", + "description": "SageMaker: Show error message when connecting remotely from a remote workspace" + }, + { + "type": "Bug Fix", + "description": "SageMaker: Prompt user to use upgraded instance type if the chosen one has insufficient memory" + }, + { + "type": "Bug Fix", + "description": "Lambda upload from directory doesn't allow selection of directory" + }, + { + "type": "Bug Fix", + "description": "Toolkit fails to recognize it's logged in when editing Lambda function" + }, + { + "type": "Bug Fix", + "description": "SageMaker: Resolve race condition when reconnecting from multiple remote windows." + }, + { + "type": "Bug Fix", + "description": "SageMaker: Resolve connection issues to SageMaker Spaces with capital letters in the name" + }, + { + "type": "Feature", + "description": "SageMaker: Add support for deep-linked Space reconnection" + }, + { + "type": "Feature", + "description": "Lambda Remote Debugging: Remote invoke configuration webview now supports attaching a debugger to directly debug your lambda function in the cloud." + }, + { + "type": "Feature", + "description": "SageMaker: Enable auto-shutdown support for Spaces" + } + ] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.70.0.json b/packages/toolkit/.changes/3.70.0.json new file mode 100644 index 00000000000..a41386724ab --- /dev/null +++ b/packages/toolkit/.changes/3.70.0.json @@ -0,0 +1,10 @@ +{ + "date": "2025-07-30", + "version": "3.70.0", + "entries": [ + { + "type": "Feature", + "description": "Improved connection actions for SSO" + } + ] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.71.0.json b/packages/toolkit/.changes/3.71.0.json new file mode 100644 index 00000000000..9d22c0cd9e7 --- /dev/null +++ b/packages/toolkit/.changes/3.71.0.json @@ -0,0 +1,5 @@ +{ + "date": "2025-08-06", + "version": "3.71.0", + "entries": [] +} \ No newline at end of file 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/CHANGELOG.md b/packages/toolkit/CHANGELOG.md index 89b1793c1fc..9b8b5ed5854 100644 --- a/packages/toolkit/CHANGELOG.md +++ b/packages/toolkit/CHANGELOG.md @@ -1,3 +1,87 @@ +## 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 + +## 3.70.0 2025-07-30 + +- **Feature** Improved connection actions for SSO + +## 3.69.0 2025-07-16 + +- **Bug Fix** SageMaker: Enable per-region manual filtering of Spaces +- **Bug Fix** SageMaker: Show error message when connecting remotely from a remote workspace +- **Bug Fix** SageMaker: Prompt user to use upgraded instance type if the chosen one has insufficient memory +- **Bug Fix** Lambda upload from directory doesn't allow selection of directory +- **Bug Fix** Toolkit fails to recognize it's logged in when editing Lambda function +- **Bug Fix** SageMaker: Resolve race condition when reconnecting from multiple remote windows. +- **Bug Fix** SageMaker: Resolve connection issues to SageMaker Spaces with capital letters in the name +- **Feature** SageMaker: Add support for deep-linked Space reconnection +- **Feature** Lambda Remote Debugging: Remote invoke configuration webview now supports attaching a debugger to directly debug your lambda function in the cloud. +- **Feature** SageMaker: Enable auto-shutdown support for Spaces + +## 3.68.0 2025-07-03 + +- **Bug Fix** [StepFunctions]: Cannot call TestState with variables in Workflow Studio +- **Feature** Lambda to SAM Transformation: AWS Toolkit Explorer now can convert existing Lambda functions into SAM (Serverless Application Model) projects. This conversion creates a project structure that's ready for local development and can be managed using Application Builder +- **Feature** Lambda Quick Edit: AWS Toolkit Explorer now offers a streamlined editing experience for Lambda functions. Download a function's code with double-click, make local modifications, and easily synchronize changes back to the cloud. + +## 3.67.0 2025-06-25 + +- **Bug Fix** State Machine deployments can now be initiated directly from Workflow Studio without closing the editor +- **Bug Fix** Step Function performance metrics now accurately reflect only Workflow Studio document activity +- **Feature** AccessAnalyzer: CheckNoPublicAccess custom policy check supports additional resource types. + +## 3.66.0 2025-06-18 + +- Miscellaneous non-user-facing changes + ## 3.65.0 2025-06-13 - Miscellaneous non-user-facing changes diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index 4920a5e0141..6a697d27596 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.66.0-SNAPSHOT", + "version": "3.83.0-SNAPSHOT", "extensionKind": [ "workspace" ], @@ -258,6 +258,14 @@ "amazonqChatLSP": { "type": "boolean", "default": true + }, + "amazonqLSPInlineChat": { + "type": "boolean", + "default": false + }, + "amazonqLSPNEP": { + "type": "boolean", + "default": false } }, "additionalProperties": false @@ -291,6 +299,11 @@ "default": "", "description": "A JSON formatted file that specifies template parameter values, a stack policy, and tags. Only parameters are used from this file.", "scope": "machine-overridable" + }, + "aws.sagemaker.studio.spaces.enableIdentityFiltering": { + "type": "boolean", + "default": false, + "description": "Enable automatic filtration of spaces based on your AWS identity." } } }, @@ -766,6 +779,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", @@ -818,6 +836,10 @@ "command": "aws.downloadStateMachineDefinition", "when": "false" }, + { + "command": "aws.toolkit.lambda.convertToSam", + "when": "false" + }, { "command": "aws.ecr.createRepository", "when": "false" @@ -1236,6 +1258,18 @@ { "command": "aws.newThreatComposerFile", "when": "false" + }, + { + "command": "aws.sagemaker.filterSpaceApps", + "when": "false" + }, + { + "command": "aws.smus.switchProject", + "when": "false" + }, + { + "command": "aws.smus.refreshProject", + "when": "false" } ], "editor/title": [ @@ -1298,6 +1332,21 @@ } ], "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", @@ -1439,9 +1488,39 @@ "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": "view != aws.smus.rootView && viewItem =~ /^(awsSagemakerSpaceRunningRemoteEnabledNode|awsSagemakerSpaceRunningRemoteDisabledNode)$/" + }, + { + "command": "aws.smus.stopSpace", + "group": "inline@0", + "when": "view == aws.smus.rootView && viewItem =~ /^(awsSagemakerSpaceRunningRemoteEnabledNode|awsSagemakerSpaceRunningRemoteDisabledNode|awsSagemakerSpaceRunningNode)$/" + }, + { + "command": "aws.sagemaker.openRemoteConnection", + "group": "inline@1", + "when": "view != aws.smus.rootView && viewItem =~ /^(awsSagemakerSpaceRunningRemoteEnabledNode|awsSagemakerSpaceStoppedRemoteEnabledNode|awsSagemakerSpaceStoppedRemoteDisabledNode)$/" + }, + { + "command": "aws.smus.openRemoteConnection", + "group": "inline@1", + "when": "view == aws.smus.rootView && viewItem =~ /^(awsSagemakerSpaceRunningRemoteEnabledNode|awsSagemakerSpaceStoppedRemoteEnabledNode|awsSagemakerSpaceStoppedRemoteDisabledNode)$/" + }, { "command": "_aws.toolkit.notifications.dismiss", "when": "viewItem == toolkitNotificationStartUp", @@ -1597,6 +1676,16 @@ "when": "view == aws.explorer && viewItem == awsRegionNode", "group": "0@1" }, + { + "command": "aws.sagemaker.filterSpaceApps", + "when": "view == aws.explorer && viewItem == awsSagemakerParentNode", + "group": "inline@1" + }, + { + "command": "aws.smus.refreshProject", + "when": "view == aws.smus.rootView && viewItem == smusSelectedProject", + "group": "inline@1" + }, { "command": "aws.toolkit.lambda.createServerlessLandProject", "when": "view == aws.explorer && viewItem == awsLambdaNode || viewItem == awsRegionNode", @@ -1649,37 +1738,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|awsRegionFunctionNodeDownloadableOnly)$/", + "group": "0@6" + }, + { + "command": "aws.toolkit.lambda.convertToSam", + "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" }, { @@ -1767,6 +1866,11 @@ "when": "view == aws.explorer && viewItem =~ /^awsIotCertificateNode.(Things|Policies)/", "group": "0@1" }, + { + "command": "aws.sagemaker.filterSpaceApps", + "when": "view == aws.explorer && viewItem == awsSagemakerParentNode", + "group": "0@1" + }, { "command": "aws.s3.createBucket", "when": "view == aws.explorer && viewItem == awsS3Node", @@ -1819,17 +1923,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))/", "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)$/", "group": "2@3" }, { @@ -2097,11 +2201,6 @@ "when": "viewItem == awsAppBuilderAppNode", "group": "inline@2" }, - { - "command": "aws.launchDebugConfigForm", - "when": "viewItem == awsAppBuilderResourceNode.function", - "group": "inline@1" - }, { "command": "aws.appBuilder.deploy", "when": "viewItem == awsAppBuilderAppNode", @@ -2118,10 +2217,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", @@ -2149,7 +2268,7 @@ }, { "command": "aws.appBuilder.openHandler", - "when": "viewItem == awsAppBuilderResourceNode.function", + "when": "viewItem == awsAppBuilderResourceNode.function|| viewItem == awsAppBuilderResourceNode.deployed-function", "group": "1@1" }, { @@ -2159,7 +2278,7 @@ }, { "command": "aws.launchDebugConfigForm", - "when": "viewItem == awsAppBuilderResourceNode.function", + "when": "viewItem == awsAppBuilderResourceNode.function || viewItem == awsAppBuilderResourceNode.deployed-function", "group": "1@2" }, { @@ -2174,14 +2293,24 @@ }, { "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|awsRegionFunctionNodeDownloadableOnly)$/", + "group": "inline@3" + }, + { + "command": "aws.toolkit.lambda.convertToSam", + "when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNodeDownloadable)$/", + "group": "inline@4" + }, { "command": "aws.docdb.createCluster", "when": "view == aws.explorer && viewItem == awsDocDBNode", @@ -2268,7 +2397,7 @@ }, { "command": "aws.appBuilder.tailLogs", - "when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode)$/", + "when": "view =~ /^(aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly|awsCloudFormationFunctionNode)$/", "group": "inline@3" } ], @@ -2328,6 +2457,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%", @@ -2557,6 +2691,104 @@ } } }, + { + "command": "aws.sagemaker.filterSpaceApps", + "title": "%AWS.command.sagemaker.filterSpaces%", + "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", + "icon": "$(remote-explorer)", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "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", + "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.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%", @@ -2996,12 +3228,23 @@ } } }, + { + "command": "aws.lambda.remoteDebugging.clearSnapshot", + "title": "%AWS.command.remoteDebugging.clearSnapshot%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, { "command": "aws.invokeLambda", "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%", @@ -3009,11 +3252,37 @@ } } }, + { + "command": "aws.toolkit.lambda.convertToSam", + "title": "%AWS.command.lambda.convertToSam%", + "category": "%AWS.title%", + "enablement": "viewItem =~ /^(awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly)$/", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + }, + "icon": { + "light": "resources/icons/aws/lambda/create-stack.svg", + "dark": "resources/icons/aws/lambda/create-stack-light.svg" + } + }, { "command": "aws.downloadLambda", "title": "%AWS.command.downloadLambda%", "category": "%AWS.title%", - "enablement": "viewItem == awsRegionFunctionNodeDownloadable", + "enablement": "viewItem =~ /^(awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly)$/", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + } + }, + { + "command": "aws.lambda.openWorkspace", + "title": "%AWS.command.openLambdaWorkspace%", + "category": "%AWS.title%", + "enablement": "viewItem =~ /^(awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly)$/", "cloud9": { "cn": { "category": "%AWS.title.cn%" @@ -3029,7 +3298,27 @@ "cn": { "category": "%AWS.title.cn%" } - } + }, + "icon": "$(cloud-upload)" + }, + { + "command": "aws.openLambdaFile", + "title": "%AWS.command.openLambdaFile%", + "category": "%AWS.title%", + "enablement": "isCloud9 || !aws.isWebExtHost", + "icon": "$(preview)" + }, + { + "command": "aws.quickDeployLambda", + "title": "%AWS.command.quickDeployLambda%", + "category": "%AWS.title%", + "enablement": "viewItem =~ /^(awsRegionFunctionNodeDownloadable|awsRegionFunctionNodeDownloadableOnly)$/", + "cloud9": { + "cn": { + "category": "%AWS.title.cn%" + } + }, + "icon": "$(cloud-upload)" }, { "command": "aws.deleteLambda", @@ -4129,6 +4418,16 @@ "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" } ], "jsonValidation": [ @@ -4247,6 +4546,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%", @@ -4254,7 +4563,8 @@ "media": { "image": "./resources/walkthrough/appBuilder/install.png", "altText": "Showing GUI installer" - } + }, + "when": "!isWindows" }, { "id": "chooseTemplate", @@ -4557,110 +4867,187 @@ "fontCharacter": "\\f1d0" } }, - "aws-lambda-function": { + "aws-lambda-create-stack": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d1" } }, - "aws-mynah-MynahIconBlack": { + "aws-lambda-create-stack-light": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d2" } }, - "aws-mynah-MynahIconWhite": { + "aws-lambda-deployed-function": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d3" } }, - "aws-mynah-logo": { + "aws-lambda-function": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d4" } }, - "aws-redshift-cluster": { + "aws-lambda-invoke-remotely": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d5" } }, - "aws-redshift-cluster-connected": { + "aws-mynah-MynahIconBlack": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d6" } }, - "aws-redshift-database": { + "aws-mynah-MynahIconWhite": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d7" } }, - "aws-redshift-redshift-cluster-connected": { + "aws-mynah-logo": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d8" } }, - "aws-redshift-schema": { + "aws-redshift-cluster": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d9" } }, - "aws-redshift-table": { + "aws-redshift-cluster-connected": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1da" } }, - "aws-s3-bucket": { + "aws-redshift-database": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1db" } }, - "aws-s3-create-bucket": { + "aws-redshift-redshift-cluster-connected": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1dc" } }, - "aws-schemas-registry": { + "aws-redshift-schema": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1dd" } }, - "aws-schemas-schema": { + "aws-redshift-table": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1de" } }, - "aws-stepfunctions-preview": { + "aws-s3-bucket": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1df" } + }, + "aws-s3-create-bucket": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e0" + } + }, + "aws-sagemaker-code-editor": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e1" + } + }, + "aws-sagemaker-jupyter-lab": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e2" + } + }, + "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": [ diff --git a/packages/toolkit/scripts/build/copyFiles.ts b/packages/toolkit/scripts/build/copyFiles.ts index e081a2eb9b4..782c16ddb50 100644 --- a/packages/toolkit/scripts/build/copyFiles.ts +++ b/packages/toolkit/scripts/build/copyFiles.ts @@ -29,7 +29,6 @@ const tasks: CopyTask[] = [ ...['LICENSE', 'NOTICE'].map((f) => { return { target: path.join('../../', f), destination: path.join(projectRoot, f) } }), - { target: path.join('../core', 'resources'), destination: path.join('..', 'resources') }, { target: path.join('../core/', 'package.nls.json'), @@ -69,6 +68,21 @@ const tasks: CopyTask[] = [ destination: path.join('src', 'stepFunctions', 'asl', 'aslServer.js'), }, + // Sagemaker local server + { + target: path.join( + '../../node_modules', + 'aws-core-vscode', + 'dist', + 'src', + 'awsService', + 'sagemaker', + 'detached-server', + 'server.js' + ), + destination: path.join('src', 'awsService', 'sagemaker', 'detached-server', 'server.js'), + }, + // Serverless Land { target: path.join( diff --git a/packages/toolkit/tsconfig.json b/packages/toolkit/tsconfig.json index 2ec1c0534c1..0aef63efe5a 100644 --- a/packages/toolkit/tsconfig.json +++ b/packages/toolkit/tsconfig.json @@ -5,5 +5,6 @@ "baseUrl": ".", "rootDir": "." }, - "exclude": ["node_modules", ".vscode-test", "src/testFixtures", "dist"] + "exclude": ["node_modules", ".vscode-test", "src/testFixtures", "dist"], + "noEmitOnError": false // allow emitting even with type errors } diff --git a/packages/webpack.base.config.js b/packages/webpack.base.config.js index 652249e6577..9f281a23cd0 100644 --- a/packages/webpack.base.config.js +++ b/packages/webpack.base.config.js @@ -38,6 +38,7 @@ module.exports = (env = {}, argv = {}) => { externals: { vscode: 'commonjs vscode', vue: 'root Vue', + tls: 'commonjs tls', }, resolve: { extensions: ['.ts', '.js'], diff --git a/packages/webpack.web.config.js b/packages/webpack.web.config.js index 0f82af67389..4495088a773 100644 --- a/packages/webpack.web.config.js +++ b/packages/webpack.web.config.js @@ -50,6 +50,9 @@ module.exports = (env, argv) => { new webpack.IgnorePlugin({ resourceRegExp: /ps-list/, // matches the path in the require() statement }), + new webpack.IgnorePlugin({ + resourceRegExp: /svgdom/, // matches the path in the require() statement + }), ], resolve: { extensions: ['.ts', '.js'], 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/sagemaker-client/1.0.0.tgz b/src.gen/@amzn/sagemaker-client/1.0.0.tgz new file mode 100644 index 00000000000..4821da0e727 Binary files /dev/null and b/src.gen/@amzn/sagemaker-client/1.0.0.tgz differ diff --git a/tsconfig.json b/tsconfig.json index b5676bad46b..c1c1b0ee221 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,7 @@ "jsx": "preserve", "esModuleInterop": true, "incremental": true, - "noEmitOnError": true, + "noEmitOnError": false, "skipLibCheck": true } }