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/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..30e82c82433 --- /dev/null +++ b/.github/workflows/setup-release-candidate.yml @@ -0,0 +1,55 @@ +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 + git commit -m "Update third-party license attribution for $BRANCH_NAME" + + # 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..28b4dd7bd3d --- /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 ba1c82ad9a5..49a6ad00b87 100644 --- a/docs/lsp.md +++ b/docs/lsp.md @@ -26,6 +26,8 @@ sequenceDiagram ## Language Server Debugging +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. ``` @@ -55,6 +57,81 @@ sequenceDiagram 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 4c207195030..645d6e348fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,13 +15,14 @@ "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.324", + "@aws-toolkits/telemetry": "^1.0.329", "@playwright/browser-chromium": "^1.43.1", "@stylistic/eslint-plugin": "^2.11.0", "@types/he": "^1.2.3", @@ -70,2098 +71,8496 @@ "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", "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/ie11-detection": { + "version": "3.0.0", + "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", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-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-crypto/crc32c": { - "version": "5.2.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": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-utf8/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "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-crypto/sha1-browser": { - "version": "5.2.0", - "license": "Apache-2.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-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", + "@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-crypto/sha256-browser": { - "version": "5.2.0", - "license": "Apache-2.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-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", + "@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-crypto/sha256-js": { - "version": "5.2.0", - "license": "Apache-2.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-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", + "@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": ">=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-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-crypto/util": { - "version": "5.2.0", - "license": "Apache-2.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.222.0", - "@smithy/util-utf8": "^2.0.0", + "@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-api-gateway": { - "version": "3.693.0", - "license": "Apache-2.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-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", + "@aws-sdk/types": "3.723.0", + "@smithy/types": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/client-sso": { - "version": "3.693.0", - "license": "Apache-2.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-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.723.0", + "@smithy/protocol-http": "^5.0.0", + "@smithy/types": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.693.0", - "license": "Apache-2.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-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", + "@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": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.693.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/client-sts": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-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/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.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": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/core": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-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.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", + "@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": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-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/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/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": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-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/core": "3.693.0", - "@aws-sdk/credential-provider-env": "3.693.0", - "@aws-sdk/credential-provider-http": "3.693.0", - "@aws-sdk/credential-provider-process": "3.693.0", - "@aws-sdk/credential-provider-sso": "3.693.0", - "@aws-sdk/credential-provider-web-identity": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/credential-provider-imds": "^3.2.6", - "@smithy/property-provider": "^3.1.9", - "@smithy/shared-ini-file-loader": "^3.1.10", - "@smithy/types": "^3.7.0", + "@smithy/types": "^4.0.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-api-gateway/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.693.0", - "license": "Apache-2.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/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/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/client-api-gateway/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.693.0", - "license": "Apache-2.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/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/types": "3.723.0", + "@smithy/types": "^4.0.0", + "bowser": "^2.11.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", + "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/core": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/property-provider": "^3.1.9", - "@smithy/types": "^3.7.0", + "@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.693.0" + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-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": { - "@aws-sdk/types": "3.692.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/types": "^3.7.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/middleware-logger": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.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/client-api-gateway/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@aws-sdk/types": "3.692.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/types": "^3.7.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.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-api-gateway/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@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/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/client-api-gateway/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@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/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": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@aws-sdk/token-providers": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-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": { - "@aws-sdk/types": "3.692.0", - "@smithy/property-provider": "^3.1.9", - "@smithy/shared-ini-file-loader": "^3.1.10", - "@smithy/types": "^3.7.0", + "@smithy/types": "^4.3.1", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.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-api-gateway/node_modules/@aws-sdk/util-endpoints": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@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-api-gateway/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@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-api-gateway/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@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/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-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" }, - "peerDependencies": { - "aws-crt": ">=1.0.0" + "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" }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@smithy/is-array-buffer": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-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": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@smithy/util-buffer-from": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-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/is-array-buffer": "^3.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": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-api-gateway/node_modules/@smithy/util-utf8": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-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/util-buffer-from": "^3.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": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-apprunner": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@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/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/client-sso": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.693.0", - "@aws-sdk/middleware-host-header": "3.693.0", - "@aws-sdk/middleware-logger": "3.693.0", - "@aws-sdk/middleware-recursion-detection": "3.693.0", - "@aws-sdk/middleware-user-agent": "3.693.0", - "@aws-sdk/region-config-resolver": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-endpoints": "3.693.0", - "@aws-sdk/util-user-agent-browser": "3.693.0", - "@aws-sdk/util-user-agent-node": "3.693.0", - "@smithy/config-resolver": "^3.0.11", - "@smithy/core": "^2.5.2", - "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/hash-node": "^3.0.9", - "@smithy/invalid-dependency": "^3.0.9", - "@smithy/middleware-content-length": "^3.0.11", - "@smithy/middleware-endpoint": "^3.2.2", - "@smithy/middleware-retry": "^3.0.26", - "@smithy/middleware-serde": "^3.0.9", - "@smithy/middleware-stack": "^3.0.9", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/node-http-handler": "^3.3.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/url-parser": "^3.0.9", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.26", - "@smithy/util-defaults-mode-node": "^3.0.26", - "@smithy/util-endpoints": "^2.1.5", - "@smithy/util-middleware": "^3.0.9", - "@smithy/util-retry": "^3.0.9", - "@smithy/util-utf8": "^3.0.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@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", + "@smithy/util-uri-escape": "^4.0.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-apprunner/node_modules/@aws-sdk/client-sts": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.693.0", - "@aws-sdk/core": "3.693.0", - "@aws-sdk/credential-provider-node": "3.693.0", - "@aws-sdk/middleware-host-header": "3.693.0", - "@aws-sdk/middleware-logger": "3.693.0", - "@aws-sdk/middleware-recursion-detection": "3.693.0", - "@aws-sdk/middleware-user-agent": "3.693.0", - "@aws-sdk/region-config-resolver": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-endpoints": "3.693.0", - "@aws-sdk/util-user-agent-browser": "3.693.0", - "@aws-sdk/util-user-agent-node": "3.693.0", - "@smithy/config-resolver": "^3.0.11", - "@smithy/core": "^2.5.2", - "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/hash-node": "^3.0.9", - "@smithy/invalid-dependency": "^3.0.9", - "@smithy/middleware-content-length": "^3.0.11", - "@smithy/middleware-endpoint": "^3.2.2", - "@smithy/middleware-retry": "^3.0.26", - "@smithy/middleware-serde": "^3.0.9", - "@smithy/middleware-stack": "^3.0.9", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/node-http-handler": "^3.3.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/url-parser": "^3.0.9", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.26", - "@smithy/util-defaults-mode-node": "^3.0.26", - "@smithy/util-endpoints": "^2.1.5", - "@smithy/util-middleware": "^3.0.9", - "@smithy/util-retry": "^3.0.9", - "@smithy/util-utf8": "^3.0.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/core": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@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" + "@smithy/types": "^4.3.1" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@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": "^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/credential-provider-ini": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@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/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": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.693.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@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": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@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/querystring-parser": "^4.0.4", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@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" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.693.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@aws-sdk/types": "3.692.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-apprunner/node_modules/@aws-sdk/middleware-logger": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@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-apprunner/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@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-apprunner/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@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": ">=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-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": { - "@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/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": ">=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-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": { - "@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/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": ">=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-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": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", - "@smithy/util-endpoints": "^2.1.5", + "@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-apprunner/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@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-apprunner/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@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.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" + "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" }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "engines": { + "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-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": ">=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-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": { - "@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-apprunner/node_modules/@smithy/util-utf8": { - "version": "3.0.0", - "license": "Apache-2.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": "^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": { - "version": "3.693.0", - "license": "Apache-2.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/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", + "@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": ">=16.0.0" + "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-cloudcontrol/node_modules/@aws-sdk/client-sso": { - "version": "3.693.0", - "license": "Apache-2.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-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/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": ">=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-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-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", + "@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": ">=16.0.0" + "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" }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.693.0" + "engines": { + "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-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-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.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": ">=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-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/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", + "@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": ">=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-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.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.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": ">=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-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/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/types": "3.840.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/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" }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.693.0" + "engines": { + "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-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/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/types": "3.840.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/client-cloudcontrol/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.693.0", - "license": "Apache-2.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/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/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": ">=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-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/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.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": ">=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-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.692.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/types": "^3.7.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-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": ">=16.0.0" + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } } }, - "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-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": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@aws-sdk/types": "3.692.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/types": "^3.7.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudcontrol/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@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/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/client-cloudcontrol/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@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/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": ">=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-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": { - "@aws-sdk/types": "3.692.0", - "@smithy/property-provider": "^3.1.9", - "@smithy/shared-ini-file-loader": "^3.1.10", - "@smithy/types": "^3.7.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/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" - }, - "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-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": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", - "@smithy/util-endpoints": "^2.1.5", + "@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": ">=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-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": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", - "bowser": "^2.11.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-cloudcontrol/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.693.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/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/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.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-cloudcontrol/node_modules/@smithy/is-array-buffer": { - "version": "3.0.0", - "license": "Apache-2.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": ">=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-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/is-array-buffer": "^3.0.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/client-cloudcontrol/node_modules/@smithy/util-utf8": { - "version": "3.0.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/middleware-endpoint": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.16.tgz", + "integrity": "sha512-plpa50PIGLqzMR2ANKAw2yOW5YKS626KYKqae3atwucbz4Ve4uQ9K9BEZxDLIFmCu7hKLcrq2zmj4a+PfmUV5w==", "dependencies": { - "@smithy/util-buffer-from": "^3.0.0", + "@smithy/core": "^3.7.1", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-middleware": "^4.0.4", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation": { - "version": "3.682.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/middleware-retry": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.17.tgz", + "integrity": "sha512-gsCimeG6BApj0SBecwa1Be+Z+JOJe46iy3B3m3A8jKJHf7eIihP76Is4LwLrbJ1ygoS7Vg73lfqzejmLOrazUA==", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/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", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/service-error-classification": "^4.0.6", + "@smithy/smithy-client": "^4.4.8", + "@smithy/types": "^4.3.1", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", "tslib": "^2.6.2", "uuid": "^9.0.1" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/client-sso": { - "version": "3.682.0", - "license": "Apache-2.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": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.679.0", - "@aws-sdk/middleware-host-header": "3.679.0", - "@aws-sdk/middleware-logger": "3.679.0", - "@aws-sdk/middleware-recursion-detection": "3.679.0", - "@aws-sdk/middleware-user-agent": "3.682.0", - "@aws-sdk/region-config-resolver": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@aws-sdk/util-endpoints": "3.679.0", - "@aws-sdk/util-user-agent-browser": "3.679.0", - "@aws-sdk/util-user-agent-node": "3.682.0", - "@smithy/config-resolver": "^3.0.9", - "@smithy/core": "^2.4.8", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/hash-node": "^3.0.7", - "@smithy/invalid-dependency": "^3.0.7", - "@smithy/middleware-content-length": "^3.0.9", - "@smithy/middleware-endpoint": "^3.1.4", - "@smithy/middleware-retry": "^3.0.23", - "@smithy/middleware-serde": "^3.0.7", - "@smithy/middleware-stack": "^3.0.7", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/url-parser": "^3.0.7", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.23", - "@smithy/util-defaults-mode-node": "^3.0.23", - "@smithy/util-endpoints": "^2.1.3", - "@smithy/util-middleware": "^3.0.7", - "@smithy/util-retry": "^3.0.7", - "@smithy/util-utf8": "^3.0.0", + "@smithy/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": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.682.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/node-http-handler": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.1.0.tgz", + "integrity": "sha512-vqfSiHz2v8b3TTTrdXi03vNz1KLYYS3bhHCDv36FYDqxT7jvTll1mMnCrkD+gOvgwybuunh/2VmvOMqwBegxEg==", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.679.0", - "@aws-sdk/credential-provider-node": "3.682.0", - "@aws-sdk/middleware-host-header": "3.679.0", - "@aws-sdk/middleware-logger": "3.679.0", - "@aws-sdk/middleware-recursion-detection": "3.679.0", - "@aws-sdk/middleware-user-agent": "3.682.0", - "@aws-sdk/region-config-resolver": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@aws-sdk/util-endpoints": "3.679.0", - "@aws-sdk/util-user-agent-browser": "3.679.0", - "@aws-sdk/util-user-agent-node": "3.682.0", - "@smithy/config-resolver": "^3.0.9", - "@smithy/core": "^2.4.8", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/hash-node": "^3.0.7", - "@smithy/invalid-dependency": "^3.0.7", - "@smithy/middleware-content-length": "^3.0.9", - "@smithy/middleware-endpoint": "^3.1.4", - "@smithy/middleware-retry": "^3.0.23", - "@smithy/middleware-serde": "^3.0.7", - "@smithy/middleware-stack": "^3.0.7", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/url-parser": "^3.0.7", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.23", - "@smithy/util-defaults-mode-node": "^3.0.23", - "@smithy/util-endpoints": "^2.1.3", - "@smithy/util-middleware": "^3.0.7", - "@smithy/util-retry": "^3.0.7", - "@smithy/util-utf8": "^3.0.0", + "@smithy/abort-controller": "^4.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-sdk/client-sts": "^3.682.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/client-sts": { - "version": "3.682.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/property-provider": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.4.tgz", + "integrity": "sha512-qHJ2sSgu4FqF4U/5UUp4DhXNmdTrgmoAai6oQiM+c5RZ/sbDwJ12qxB1M6FnP+Tn/ggkPZf9ccn4jqKSINaquw==", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.682.0", - "@aws-sdk/core": "3.679.0", - "@aws-sdk/credential-provider-node": "3.682.0", - "@aws-sdk/middleware-host-header": "3.679.0", - "@aws-sdk/middleware-logger": "3.679.0", - "@aws-sdk/middleware-recursion-detection": "3.679.0", - "@aws-sdk/middleware-user-agent": "3.682.0", - "@aws-sdk/region-config-resolver": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@aws-sdk/util-endpoints": "3.679.0", - "@aws-sdk/util-user-agent-browser": "3.679.0", - "@aws-sdk/util-user-agent-node": "3.682.0", - "@smithy/config-resolver": "^3.0.9", - "@smithy/core": "^2.4.8", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/hash-node": "^3.0.7", - "@smithy/invalid-dependency": "^3.0.7", - "@smithy/middleware-content-length": "^3.0.9", - "@smithy/middleware-endpoint": "^3.1.4", - "@smithy/middleware-retry": "^3.0.23", - "@smithy/middleware-serde": "^3.0.7", - "@smithy/middleware-stack": "^3.0.7", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/url-parser": "^3.0.7", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.23", - "@smithy/util-defaults-mode-node": "^3.0.23", - "@smithy/util-endpoints": "^2.1.3", - "@smithy/util-middleware": "^3.0.7", - "@smithy/util-retry": "^3.0.7", - "@smithy/util-utf8": "^3.0.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/core": { - "version": "3.679.0", - "license": "Apache-2.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": { - "@aws-sdk/types": "3.679.0", - "@smithy/core": "^2.4.8", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/property-provider": "^3.1.7", - "@smithy/protocol-http": "^4.1.4", - "@smithy/signature-v4": "^4.2.0", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/util-middleware": "^3.0.7", - "fast-xml-parser": "4.4.1", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.679.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/querystring-builder": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.4.tgz", + "integrity": "sha512-SwREZcDnEYoh9tLNgMbpop+UTGq44Hl9tdj3rf+yeLcfH7+J8OXEBaMc2kDxtyRHu8BhSg9ADEx0gFHvpJgU8w==", "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/types": "^3.5.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-cloudformation/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.679.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/querystring-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.4.tgz", + "integrity": "sha512-6yZf53i/qB8gRHH/l2ZwUG5xgkPgQF15/KxH0DdXMDHjesA9MeZje/853ifkSY0x4m5S+dfDZ+c4x439PF0M2w==", "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/property-provider": "^3.1.7", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/util-stream": "^3.1.9", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.682.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/service-error-classification": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.6.tgz", + "integrity": "sha512-RRoTDL//7xi4tn5FrN2NzH17jbgmnKidUqd4KvquT0954/i6CXXkh1884jBiunq24g9cGtPBEXlU40W6EpNOOg==", "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/credential-provider-env": "3.679.0", - "@aws-sdk/credential-provider-http": "3.679.0", - "@aws-sdk/credential-provider-process": "3.679.0", - "@aws-sdk/credential-provider-sso": "3.682.0", - "@aws-sdk/credential-provider-web-identity": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/credential-provider-imds": "^3.2.4", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", - "tslib": "^2.6.2" + "@smithy/types": "^4.3.1" }, "engines": { - "node": ">=16.0.0" + "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" }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.682.0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.682.0", - "license": "Apache-2.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": { - "@aws-sdk/credential-provider-env": "3.679.0", - "@aws-sdk/credential-provider-http": "3.679.0", - "@aws-sdk/credential-provider-ini": "3.682.0", - "@aws-sdk/credential-provider-process": "3.679.0", - "@aws-sdk/credential-provider-sso": "3.682.0", - "@aws-sdk/credential-provider-web-identity": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/credential-provider-imds": "^3.2.4", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", + "@smithy/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-cloudformation/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.679.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/smithy-client": { + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.8.tgz", + "integrity": "sha512-pcW691/lx7V54gE+dDGC26nxz8nrvnvRSCJaIYD6XLPpOInEZeKdV/SpSux+wqeQ4Ine7LJQu8uxMvobTIBK0w==", "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", + "@smithy/core": "^3.7.1", + "@smithy/middleware-endpoint": "^4.1.16", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.3", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.682.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/types": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", + "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", "dependencies": { - "@aws-sdk/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": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.679.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/url-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.4.tgz", + "integrity": "sha512-eMkc144MuN7B0TDA4U2fKs+BqczVbk3W+qIvcoCY6D1JY3hnAdCuhCZODC+GAeaxj0p6Jroz4+XMUn3PCxQQeQ==", "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/types": "^3.5.0", + "@smithy/querystring-parser": "^4.0.4", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-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" }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.679.0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.679.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/protocol-http": "^4.1.4", - "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/middleware-logger": { - "version": "3.679.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.679.0", - "license": "Apache-2.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": { - "@aws-sdk/types": "3.679.0", - "@smithy/protocol-http": "^4.1.4", - "@smithy/types": "^3.5.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-cloudformation/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.682.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@aws-sdk/util-endpoints": "3.679.0", - "@smithy/core": "^2.4.8", - "@smithy/protocol-http": "^4.1.4", - "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.679.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.24", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.24.tgz", + "integrity": "sha512-UkQNgaQ+bidw1MgdgPO1z1k95W/v8Ej/5o/T/Is8PiVUYPspl/ZxV6WO/8DrzZQu5ULnmpB9CDdMSRwgRc21AA==", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/types": "^3.5.0", - "@smithy/util-config-provider": "^3.0.0", - "@smithy/util-middleware": "^3.0.7", + "@smithy/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": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/token-providers": { - "version": "3.679.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.24", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.24.tgz", + "integrity": "sha512-phvGi/15Z4MpuQibTLOYIumvLdXb+XIJu8TA55voGgboln85jytA3wiD7CkUE8SNcWqkkb+uptZKPiuFouX/7g==", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/smithy-client": "^4.4.8", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sso-oidc": "^3.679.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/types": { - "version": "3.679.0", - "license": "Apache-2.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/types": "^3.5.0", + "@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-cloudformation/node_modules/@aws-sdk/util-endpoints": { - "version": "3.679.0", - "license": "Apache-2.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": { - "@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": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.679.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/util-middleware": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.4.tgz", + "integrity": "sha512-9MLKmkBmf4PRb0ONJikCbCwORACcil6gUWojwARCClT7RmLzF04hUR4WdRprIXal7XVyrddadYNfp2eF3nrvtQ==", "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/types": "^3.5.0", - "bowser": "^2.11.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.682.0", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/util-retry": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.6.tgz", + "integrity": "sha512-+YekoF2CaSMv6zKrA6iI/N9yva3Gzn4L6n35Luydweu5MMPYpiGZlWqehPHDHyNbnyaYlz/WJyYAZnC+loBDZg==", "dependencies": { - "@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", + "@smithy/service-error-classification": "^4.0.6", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/fetch-http-handler": { - "version": "3.2.9", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-datazone/node_modules/@smithy/util-stream": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.3.tgz", + "integrity": "sha512-cQn412DWHHFNKrQfbHY8vSFI3nTROY1aIKji9N0tpp8gUABRilr7wdf8fqBbSlXresobM+tQFNk6I+0LXK/YZg==", "dependencies": { - "@smithy/protocol-http": "^4.1.4", - "@smithy/querystring-builder": "^3.0.7", - "@smithy/types": "^3.5.0", - "@smithy/util-base64": "^3.0.0", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/is-array-buffer": { - "version": "3.0.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": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/util-buffer-from": { - "version": "3.0.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": { - "@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-cloudformation/node_modules/@smithy/util-utf8": { - "version": "3.0.0", - "license": "Apache-2.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": { - "@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-cloudwatch-logs": { - "version": "3.682.0", + "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", "@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-sdk-ec2": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.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.8", "@types/uuid": "^9.0.1", "tslib": "^2.6.2", "uuid": "^9.0.1" @@ -2170,46 +8569,46 @@ "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-ec2/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" }, @@ -2217,47 +8616,47 @@ "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-ec2/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" }, @@ -2265,51 +8664,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-cloudwatch-logs/node_modules/@aws-sdk/client-sts": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-ec2/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" }, @@ -2317,19 +8716,19 @@ "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-ec2/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" }, @@ -2337,261 +8736,221 @@ "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", + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/credential-provider-http": { + "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/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" - }, - "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", + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.693.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", + "@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-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-ec2/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-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-ec2/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-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-ec2/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" + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/middleware-host-header": { + "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/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-logger": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/middleware-logger": { + "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", "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-ec2/node_modules/@aws-sdk/middleware-recursion-detection": { + "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/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-user-agent": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/middleware-user-agent": { + "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/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/region-config-resolver": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-ec2/node_modules/@aws-sdk/region-config-resolver": { + "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", + "@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.7", + "@smithy/util-middleware": "^3.0.9", "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-ec2/node_modules/@aws-sdk/token-providers": { + "version": "3.693.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", + "@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.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" + "@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-ec2/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-cloudwatch-logs/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.679.0", + "node_modules/@aws-sdk/client-ec2/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-cloudwatch-logs/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.682.0", + "node_modules/@aws-sdk/client-ec2/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": { @@ -2606,140 +8965,80 @@ } } }, - "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": { + "node_modules/@aws-sdk/client-ec2/node_modules/@smithy/is-array-buffer": { "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": { + "node_modules/@aws-sdk/client-ec2/node_modules/@smithy/util-buffer-from": { "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" + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "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-ec2/node_modules/@smithy/util-utf8": { + "version": "3.0.0", "license": "Apache-2.0", - "peer": true, + "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.758.0", - "@aws-sdk/middleware-host-header": "3.734.0", - "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.758.0", - "@aws-sdk/region-config-resolver": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.743.0", - "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.758.0", - "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.5", - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/hash-node": "^4.0.1", - "@smithy/invalid-dependency": "^4.0.1", - "@smithy/middleware-content-length": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.6", - "@smithy/middleware-retry": "^4.0.7", - "@smithy/middleware-serde": "^4.0.2", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/node-http-handler": "^4.0.3", - "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", + "@aws-sdk/core": "3.846.0", + "@aws-sdk/credential-provider-node": "3.848.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.848.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.848.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.848.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.7.0", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.15", + "@smithy/middleware-retry": "^4.1.16", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.7", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.7", - "@smithy/util-defaults-mode-node": "^4.0.7", - "@smithy/util-endpoints": "^3.0.1", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-retry": "^4.0.1", + "@smithy/util-defaults-mode-browser": "^4.0.23", + "@smithy/util-defaults-mode-node": "^4.0.23", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, @@ -2747,48 +9046,47 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/client-sso": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.848.0.tgz", + "integrity": "sha512-mD+gOwoeZQvbecVLGoCmY6pS7kg02BHesbtIxUj+PeBqYoZV5uLvjUOmuGfw1SfoSobKvS11urxC9S7zxU/Maw==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.758.0", - "@aws-sdk/credential-provider-node": "3.758.0", - "@aws-sdk/middleware-host-header": "3.734.0", - "@aws-sdk/middleware-logger": "3.734.0", - "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.758.0", - "@aws-sdk/region-config-resolver": "3.734.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.743.0", - "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.758.0", - "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.5", - "@smithy/fetch-http-handler": "^5.0.1", - "@smithy/hash-node": "^4.0.1", - "@smithy/invalid-dependency": "^4.0.1", - "@smithy/middleware-content-length": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.6", - "@smithy/middleware-retry": "^4.0.7", - "@smithy/middleware-serde": "^4.0.2", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/node-http-handler": "^4.0.3", - "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", + "@aws-sdk/core": "3.846.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.848.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.848.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.848.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.7.0", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.15", + "@smithy/middleware-retry": "^4.1.16", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.7", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.7", - "@smithy/util-defaults-mode-node": "^4.0.7", - "@smithy/util-endpoints": "^3.0.1", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-retry": "^4.0.1", + "@smithy/util-defaults-mode-browser": "^4.0.23", + "@smithy/util-defaults-mode-node": "^4.0.23", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, @@ -2796,187 +9094,260 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/core": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/core": { + "version": "3.846.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.846.0.tgz", + "integrity": "sha512-7CX0pM906r4WSS68fCTNMTtBCSkTtf3Wggssmx13gD40gcWEZXsU00KzPp1bYheNRyPlAq3rE22xt4wLPXbuxA==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/core": "^3.1.5", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/signature-v4": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/util-middleware": "^4.0.1", - "fast-xml-parser": "4.4.1", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/xml-builder": "3.821.0", + "@smithy/core": "^3.7.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/signature-v4": "^5.1.2", + "@smithy/smithy-client": "^4.4.7", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-utf8": "^4.0.0", + "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.846.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.846.0.tgz", + "integrity": "sha512-QuCQZET9enja7AWVISY+mpFrEIeHzvkx/JEEbHYzHhUkxcnC2Kq2c0bB7hDihGD0AZd3Xsm653hk1O97qu69zg==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/core": "3.846.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-logger": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.846.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.846.0.tgz", + "integrity": "sha512-Jh1iKUuepdmtreMYozV2ePsPcOF5W9p3U4tWhi3v6nDvz0GsBjzjAROW+BW8XMz9vAD3I9R+8VC3/aq63p5nlw==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", + "@aws-sdk/core": "3.846.0", + "@aws-sdk/types": "3.840.0", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.7", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.3", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.848.0.tgz", + "integrity": "sha512-r6KWOG+En2xujuMhgZu7dzOZV3/M5U/5+PXrG8dLQ3rdPRB3vgp5tc56KMqLwm/EXKRzAOSuw/UE4HfNOAB8Hw==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/core": "3.846.0", + "@aws-sdk/credential-provider-env": "3.846.0", + "@aws-sdk/credential-provider-http": "3.846.0", + "@aws-sdk/credential-provider-process": "3.846.0", + "@aws-sdk/credential-provider-sso": "3.848.0", + "@aws-sdk/credential-provider-web-identity": "3.848.0", + "@aws-sdk/nested-clients": "3.848.0", + "@aws-sdk/types": "3.840.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.848.0.tgz", + "integrity": "sha512-AblNesOqdzrfyASBCo1xW3uweiSro4Kft9/htdxLeCVU1KVOnFWA5P937MNahViRmIQm2sPBCqL8ZG0u9lnh5g==", "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.743.0", - "@smithy/core": "^3.1.5", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/credential-provider-env": "3.846.0", + "@aws-sdk/credential-provider-http": "3.846.0", + "@aws-sdk/credential-provider-ini": "3.848.0", + "@aws-sdk/credential-provider-process": "3.846.0", + "@aws-sdk/credential-provider-sso": "3.848.0", + "@aws-sdk/credential-provider-web-identity": "3.848.0", + "@aws-sdk/types": "3.840.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.846.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.846.0.tgz", + "integrity": "sha512-mEpwDYarJSH+CIXnnHN0QOe0MXI+HuPStD6gsv3z/7Q6ESl8KRWon3weFZCDnqpiJMUVavlDR0PPlAFg2MQoPg==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", + "@aws-sdk/core": "3.846.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/types": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.848.0.tgz", + "integrity": "sha512-pozlDXOwJZL0e7w+dqXLgzVDB7oCx4WvtY0sk6l4i07uFliWF/exupb6pIehFWvTUcOvn5aFTTqcQaEzAD5Wsg==", "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/client-sso": "3.848.0", + "@aws-sdk/core": "3.846.0", + "@aws-sdk/token-providers": "3.848.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/util-endpoints": { - "version": "3.743.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.848.0.tgz", + "integrity": "sha512-D1fRpwPxtVDhcSc/D71exa2gYweV+ocp4D3brF0PgFd//JR3XahZ9W24rVnTQwYEcK9auiBZB89Ltv+WbWN8qw==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "@smithy/util-endpoints": "^3.0.1", + "@aws-sdk/core": "3.846.0", + "@aws-sdk/nested-clients": "3.848.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.840.0.tgz", + "integrity": "sha512-ub+hXJAbAje94+Ya6c6eL7sYujoE8D4Bumu1NUI8TXjUhVVn0HzVWQjpRLshdLsUp1AW7XyeJaxyajRaJQ8+Xg==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "bowser": "^2.11.0", + "@aws-sdk/types": "3.840.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/middleware-logger": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.840.0.tgz", + "integrity": "sha512-lSV8FvjpdllpGaRspywss4CtXV8M7NNNH+2/j86vMH+YCOZ6fu2T/TyFd/tHwZ92vDfHctWkRbQxg0bagqwovA==", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.840.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.840.0.tgz", + "integrity": "sha512-Gu7lGDyfddyhIkj1Z1JtrY5NHb5+x/CRiB87GjaSrKxkDaydtX2CU977JIABtt69l9wLbcGDIQ+W0uJ5xPof7g==", + "dependencies": { + "@aws-sdk/types": "3.840.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/config-resolver": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.848.0.tgz", + "integrity": "sha512-rjMuqSWJEf169/ByxvBqfdei1iaduAnfolTshsZxwcmLIUtbYrFUmts0HrLQqsAG8feGPpDLHA272oPl+NTCCA==", "dependencies": { - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", + "@aws-sdk/core": "3.846.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.848.0", + "@smithy/core": "^3.7.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/core": { - "version": "3.1.5", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/nested-clients": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.848.0.tgz", + "integrity": "sha512-joLsyyo9u61jnZuyYzo1z7kmS7VgWRAkzSGESVzQHfOA1H2PYeUFek6vLT4+c9xMGrX/Z6B0tkRdzfdOPiatLg==", "dependencies": { - "@smithy/middleware-serde": "^4.0.2", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.846.0", + "@aws-sdk/middleware-host-header": "3.840.0", + "@aws-sdk/middleware-logger": "3.840.0", + "@aws-sdk/middleware-recursion-detection": "3.840.0", + "@aws-sdk/middleware-user-agent": "3.848.0", + "@aws-sdk/region-config-resolver": "3.840.0", + "@aws-sdk/types": "3.840.0", + "@aws-sdk/util-endpoints": "3.848.0", + "@aws-sdk/util-user-agent-browser": "3.840.0", + "@aws-sdk/util-user-agent-node": "3.848.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.7.0", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.15", + "@smithy/middleware-retry": "^4.1.16", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.1.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.7", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-stream": "^4.1.2", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.23", + "@smithy/util-defaults-mode-node": "^4.0.23", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, @@ -2984,583 +9355,610 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/fetch-http-handler": { - "version": "5.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.840.0.tgz", + "integrity": "sha512-Qjnxd/yDv9KpIMWr90ZDPtRj0v75AqGC92Lm9+oHXZ8p1MjG5JE2CW0HL8JRgK9iKzgKBL7pPQRXI8FkvEVfrA==", "dependencies": { - "@smithy/protocol-http": "^5.0.1", - "@smithy/querystring-builder": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-base64": "^4.0.0", + "@aws-sdk/types": "3.840.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/hash-node": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/token-providers": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.848.0.tgz", + "integrity": "sha512-oNPyM4+Di2Umu0JJRFSxDcKQ35+Chl/rAwD47/bS0cDPI8yrao83mLXLeDqpRPHyQW4sXlP763FZcuAibC0+mg==", "dependencies": { - "@smithy/types": "^4.1.0", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/core": "3.846.0", + "@aws-sdk/nested-clients": "3.848.0", + "@aws-sdk/types": "3.840.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/invalid-dependency": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/types": { + "version": "3.840.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.840.0.tgz", + "integrity": "sha512-xliuHaUFZxEx1NSXeLLZ9Dyu6+EJVQKEoD+yM+zqUo3YDZ7medKJWY6fIOKiPX/N7XbLdBYwajb15Q7IL8KkeA==", "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/middleware-content-length": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@aws-sdk/util-endpoints": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.848.0.tgz", + "integrity": "sha512-fY/NuFFCq/78liHvRyFKr+aqq1aA/uuVSANjzr5Ym8c+9Z3HRPE9OrExAHoMrZ6zC8tHerQwlsXYYH5XZ7H+ww==", "dependencies": { - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.840.0", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-endpoints": "^3.0.6", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/middleware-endpoint": { - "version": "4.0.6", - "license": "Apache-2.0", - "peer": true, + "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": { - "@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", + "@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-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/middleware-retry": { - "version": "4.0.7", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/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/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/types": "^4.3.1", + "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/middleware-serde": { - "version": "4.0.2", - "license": "Apache-2.0", - "peer": true, + "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/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/client-sso-oidc/node_modules/@smithy/middleware-stack": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/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/client-sso-oidc/node_modules/@smithy/node-config-provider": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/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/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/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/node-http-handler": { - "version": "4.0.3", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/fetch-http-handler": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.1.0.tgz", + "integrity": "sha512-mADw7MS0bYe2OGKkHYMaqarOXuDwRbO6ArD91XhHcl2ynjGCFF+hvqf0LyQcYxkA1zaWjefSkU7Ne9mqgApSgQ==", "dependencies": { - "@smithy/abort-controller": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/querystring-builder": "^4.0.1", - "@smithy/types": "^4.1.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/property-provider": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/hash-node": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.4.tgz", + "integrity": "sha512-qnbTPUhCVnCgBp4z4BUJUhOEkVwxiEi1cyFM+Zj6o+aY8OFGxUQleKWq8ltgp3dujuhXojIvJWdoqpm6dVO3lQ==", "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.3.1", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/protocol-http": { - "version": "5.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/invalid-dependency": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.4.tgz", + "integrity": "sha512-bNYMi7WKTJHu0gn26wg8OscncTt1t2b8KcsZxvOv56XA6cyXtOAAAaNP7+m45xfppXfOatXF3Sb1MNsLUgVLTw==", "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", "dependencies": { - "@smithy/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, + "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/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/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/smithy-client": { - "version": "4.1.6", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/middleware-endpoint": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.17.tgz", + "integrity": "sha512-S3hSGLKmHG1m35p/MObQCBCdRsrpbPU8B129BVzRqRfDvQqPMQ14iO4LyRw+7LNizYc605COYAcjqgawqi+6jA==", "dependencies": { - "@smithy/core": "^3.1.5", - "@smithy/middleware-endpoint": "^4.0.6", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-stream": "^4.1.2", + "@smithy/core": "^3.7.2", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-middleware": "^4.0.4", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/types": { - "version": "4.1.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/middleware-retry": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.18.tgz", + "integrity": "sha512-bYLZ4DkoxSsPxpdmeapvAKy7rM5+25gR7PGxq2iMiecmbrRGBHj9s75N74Ylg+aBiw9i5jIowC/cLU2NR0qH8w==", "dependencies": { - "tslib": "^2.6.2" + "@smithy/node-config-provider": "^4.1.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/service-error-classification": "^4.0.6", + "@smithy/smithy-client": "^4.4.9", + "@smithy/types": "^4.3.1", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.6", + "tslib": "^2.6.2", + "uuid": "^9.0.1" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/url-parser": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/middleware-serde": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.8.tgz", + "integrity": "sha512-iSSl7HJoJaGyMIoNn2B7czghOVwJ9nD7TMvLhMWeSB5vt0TnEYyRRqPJu/TqW76WScaNvYYB8nRoiBHR9S1Ddw==", "dependencies": { - "@smithy/querystring-parser": "^4.0.1", - "@smithy/types": "^4.1.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-base64": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/middleware-stack": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.4.tgz", + "integrity": "sha512-kagK5ggDrBUCCzI93ft6DjteNSfY8Ulr83UtySog/h09lTIOAJ/xUSObutanlPT0nhoHAkpmW9V5K8oPyLh+QA==", "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/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-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-body-length-node": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/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-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-config-provider": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/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-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.0.7", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/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/property-provider": "^4.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "bowser": "^2.11.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-defaults-mode-node": { - "version": "4.0.7", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/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/config-resolver": "^4.0.1", - "@smithy/credential-provider-imds": "^4.0.1", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.3.1", + "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-endpoints": { - "version": "3.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/querystring-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.4.tgz", + "integrity": "sha512-6yZf53i/qB8gRHH/l2ZwUG5xgkPgQF15/KxH0DdXMDHjesA9MeZje/853ifkSY0x4m5S+dfDZ+c4x439PF0M2w==", "dependencies": { - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-middleware": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/service-error-classification": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.6.tgz", + "integrity": "sha512-RRoTDL//7xi4tn5FrN2NzH17jbgmnKidUqd4KvquT0954/i6CXXkh1884jBiunq24g9cGtPBEXlU40W6EpNOOg==", "dependencies": { - "@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/client-sso-oidc/node_modules/@smithy/util-retry": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/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/service-error-classification": "^4.0.1", - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/signature-v4": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.2.tgz", + "integrity": "sha512-d3+U/VpX7a60seHziWnVZOHuEgJlclufjkS6zhXvxcJgkJq4UWdH5eOBLzHRMx6gXjsdT9h6lfpmLzbrdupHgQ==", "dependencies": { - "@smithy/util-buffer-from": "^4.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": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/core": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/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/client-sso/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/types": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", + "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/middleware-logger": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/url-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.4.tgz", + "integrity": "sha512-eMkc144MuN7B0TDA4U2fKs+BqczVbk3W+qIvcoCY6D1JY3hnAdCuhCZODC+GAeaxj0p6Jroz4+XMUn3PCxQQeQ==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", + "@smithy/querystring-parser": "^4.0.4", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-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/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", - "@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/@aws-sdk/middleware-user-agent": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", "dependencies": { - "@aws-sdk/core": "3.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, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/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, + "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": { - "@smithy/types": "^4.1.0", + "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-endpoints": { - "version": "3.743.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "@smithy/util-endpoints": "^3.0.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.734.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-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.734.0", - "@smithy/types": "^4.1.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/smithy-client": "^4.4.9", + "@smithy/types": "^4.3.1", "bowser": "^2.11.0", "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.758.0", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.25", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.25.tgz", + "integrity": "sha512-+w4n4hKFayeCyELZLfsSQG5mCC3TwSkmRHv4+el5CzFU8ToQpYGhpV7mrRzqlwKkntlPilT1HJy1TVeEvEjWOQ==", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", + "@smithy/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-glue/node_modules/@smithy/util-endpoints": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.6.tgz", + "integrity": "sha512-YARl3tFL3WgPuLzljRUnrS2ngLiUtkwhQtj8PAL13XZSyUiNLQxwG3fBBq3QXFqGFUXepIN73pINp3y8c2nBmA==", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/config-resolver": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-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/node-config-provider": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/core": { - "version": "3.1.5", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-middleware": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.4.tgz", + "integrity": "sha512-9MLKmkBmf4PRb0ONJikCbCwORACcil6gUWojwARCClT7RmLzF04hUR4WdRprIXal7XVyrddadYNfp2eF3nrvtQ==", "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/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/fetch-http-handler": { - "version": "5.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-retry": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.6.tgz", + "integrity": "sha512-+YekoF2CaSMv6zKrA6iI/N9yva3Gzn4L6n35Luydweu5MMPYpiGZlWqehPHDHyNbnyaYlz/WJyYAZnC+loBDZg==", "dependencies": { - "@smithy/protocol-http": "^5.0.1", - "@smithy/querystring-builder": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-base64": "^4.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-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/hash-node": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-stream": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.3.tgz", + "integrity": "sha512-cQn412DWHHFNKrQfbHY8vSFI3nTROY1aIKji9N0tpp8gUABRilr7wdf8fqBbSlXresobM+tQFNk6I+0LXK/YZg==", "dependencies": { - "@smithy/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" }, @@ -3568,1154 +9966,1575 @@ "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, + "node_modules/@aws-sdk/client-glue/node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", "dependencies": { - "@smithy/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, + "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/protocol-http": "^5.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/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/middleware-endpoint": { - "version": "4.0.6", + "node_modules/@aws-sdk/client-glue/node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@aws-sdk/client-glue/node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ] + }, + "node_modules/@aws-sdk/client-iam": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/core": "^3.1.5", - "@smithy/middleware-serde": "^4.0.2", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", - "@smithy/util-middleware": "^4.0.1", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/client-sts": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "@smithy/util-waiter": "^3.1.8", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/middleware-retry": { - "version": "4.0.7", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/client-sso": { + "version": "3.693.0", "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-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/middleware-serde": { - "version": "4.0.2", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/middleware-stack": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/client-sts": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/node-config-provider": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/core": { + "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/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/node-http-handler": { - "version": "4.0.3", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/abort-controller": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/querystring-builder": "^4.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/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/property-provider": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/protocol-http": { - "version": "5.0.1", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/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/shared-ini-file-loader": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/client-sso": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/token-providers": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/signature-v4": { - "version": "5.0.1", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-uri-escape": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/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/smithy-client": { - "version": "4.1.6", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/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/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/types": { - "version": "4.1.0", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/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/@aws-sdk/client-sso/node_modules/@smithy/url-parser": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/querystring-parser": "^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/util-base64": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@smithy/core": "^2.5.2", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.9", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-body-length-node": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/token-providers": { + "version": "3.693.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", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-config-provider": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-endpoints": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "@smithy/util-endpoints": "^2.1.5", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.0.7", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/property-provider": "^4.0.1", - "@smithy/smithy-client": "^4.1.6", - "@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/@smithy/util-defaults-mode-node": { - "version": "4.0.7", + "node_modules/@aws-sdk/client-iam/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@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/middleware-user-agent": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-endpoints": { - "version": "3.0.1", + "node_modules/@aws-sdk/client-iam/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", "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": ">=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-iam/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/node_modules/@smithy/util-retry": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-iam/node_modules/@smithy/util-utf8": { + "version": "3.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/service-error-classification": "^4.0.1", - "@smithy/types": "^4.1.0", + "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-utf8": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-lambda": { + "version": "3.637.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.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/core": { - "version": "3.693.0", + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/types": { + "version": "3.609.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/types": "^3.3.0", "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", + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.4", "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", + "@smithy/protocol-http": "^4.1.0", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", + "@smithy/util-base64": "^3.0.0", "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-env/node_modules/@aws-sdk/core": { - "version": "3.758.0", + "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/core": "^3.1.5", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/signature-v4": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/util-middleware": "^4.0.1", - "fast-xml-parser": "4.4.1", "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-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/credential-provider-env/node_modules/@smithy/core": { - "version": "3.1.5", + "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/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/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/middleware-endpoint": { - "version": "4.0.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-serde": "^4.0.2", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", - "@smithy/util-middleware": "^4.0.1", + "@aws-crypto/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/middleware-serde": { - "version": "4.0.2", - "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": { - "@smithy/types": "^4.1.0", - "tslib": "^2.6.2" + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/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/middleware-stack": { - "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/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/node-config-provider": { - "version": "4.0.1", - "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": { - "@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", + "@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/property-provider": { - "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/protocol-http": { - "version": "5.0.1", - "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/types": "^4.1.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-env/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/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": { - "@smithy/types": "^4.1.0", + "@aws-sdk/core": "3.858.0", + "@aws-sdk/credential-provider-env": "3.858.0", + "@aws-sdk/credential-provider-http": "3.858.0", + "@aws-sdk/credential-provider-process": "3.858.0", + "@aws-sdk/credential-provider-sso": "3.859.0", + "@aws-sdk/credential-provider-web-identity": "3.858.0", + "@aws-sdk/nested-clients": "3.858.0", + "@aws-sdk/types": "3.840.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "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, + "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": { - "@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/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-env/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/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/core": "^3.1.5", - "@smithy/middleware-endpoint": "^4.0.6", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-stream": "^4.1.2", + "@aws-sdk/core": "3.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-env/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/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": { + "@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-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/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/querystring-parser": "^4.0.1", - "@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/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-body-length-browser": { - "version": "4.0.0", - "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": { + "@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-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/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-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/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/util-buffer-from": "^4.0.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": { - "version": "3.758.0", - "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": { - "@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/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/@aws-sdk/core": { - "version": "3.758.0", - "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": { - "@aws-sdk/types": "3.734.0", - "@smithy/core": "^3.1.5", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/signature-v4": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/util-middleware": "^4.0.1", - "fast-xml-parser": "4.4.1", + "@aws-sdk/core": "3.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/@aws-sdk/types": { - "version": "3.734.0", - "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/core": { - "version": "3.1.5", - "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/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.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/fetch-http-handler": { - "version": "5.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/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/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/middleware-endpoint": { - "version": "4.0.6", - "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/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/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/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/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, + "node_modules/@aws-sdk/client-s3-control/node_modules/@aws-sdk/util-endpoints": { + "version": "3.848.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.848.0.tgz", + "integrity": "sha512-fY/NuFFCq/78liHvRyFKr+aqq1aA/uuVSANjzr5Ym8c+9Z3HRPE9OrExAHoMrZ6zC8tHerQwlsXYYH5XZ7H+ww==", "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.840.0", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-endpoints": "^3.0.6", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/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/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/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/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-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/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": { - "@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/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/property-provider": { - "version": "4.0.1", - "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/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-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/@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.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-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/@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/signature-v4": { - "version": "5.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.0.0.tgz", + "integrity": "sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig==", "dependencies": { - "@smithy/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-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/smithy-client": { - "version": "4.1.6", - "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": { - "@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/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/types": { - "version": "4.1.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/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-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/@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/querystring-parser": "^4.0.1", - "@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-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/@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/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.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-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/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-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/hash-node": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.4.tgz", + "integrity": "sha512-qnbTPUhCVnCgBp4z4BUJUhOEkVwxiEi1cyFM+Zj6o+aY8OFGxUQleKWq8ltgp3dujuhXojIvJWdoqpm6dVO3lQ==", "dependencies": { - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.3.1", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-http/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/hash-stream-node": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.0.4.tgz", + "integrity": "sha512-wHo0d8GXyVmpmMh/qOR0R7Y46/G1y6OR8U+bSTB4ppEzRxd1xVAQ9xOE9hOc0bSjhz0ujCPAbfNLkLrpa6cevg==", "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", + "@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-node": { - "version": "3.758.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": { - "@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/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-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/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/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, + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/md5-js": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.0.4.tgz", + "integrity": "sha512-uGLBVqcOwrLvGh/v/jw423yWHq/ofUGK1W31M2TNspLQbUV1Va0F5kTxtirkoHawODAZcjXTSGi7JwbnPcDPJg==", "dependencies": { - "@smithy/types": "^4.1.0", + "@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-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/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-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/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-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/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/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" + "@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/@aws-sdk/core": { - "version": "3.758.0", - "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": { - "@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/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/@aws-sdk/types": { - "version": "3.734.0", - "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/core": { - "version": "3.1.5", - "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/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/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/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/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/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/middleware-serde": { - "version": "4.0.2", - "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/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/middleware-stack": { - "version": "4.0.1", - "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.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/node-config-provider": { - "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/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-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/property-provider": { - "version": "4.0.1", - "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.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/protocol-http": { - "version": "5.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/shared-ini-file-loader": { - "version": "4.0.1", - "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/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/signature-v4": { - "version": "5.0.1", - "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": { "@smithy/is-array-buffer": "^4.0.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", + "@smithy/util-middleware": "^4.0.4", "@smithy/util-uri-escape": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" @@ -4724,27 +11543,27 @@ "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/smithy-client": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.9.tgz", + "integrity": "sha512-mbMg8mIUAWwMmb74LoYiArP04zWElPzDoA1jVOp3or0cjlDMgoS6WTC3QXK0Vxoc9I4zdrX0tq6qsOmaIoTWEQ==", "dependencies": { - "@smithy/core": "^3.1.5", - "@smithy/middleware-endpoint": "^4.0.6", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-stream": "^4.1.2", + "@smithy/core": "^3.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-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/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" }, @@ -4752,326 +11571,493 @@ "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/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.1", - "@smithy/types": "^4.1.0", + "@smithy/querystring-parser": "^4.0.4", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/util-body-length-browser": { + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/util-base64": { "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/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/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/client-codecatalyst/node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/util-utf8": { + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/util-body-length-node": { "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", "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, + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", "dependencies": { - "@aws-sdk/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", "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/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/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, + "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/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-s3-control/node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.25", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.25.tgz", + "integrity": "sha512-+w4n4hKFayeCyELZLfsSQG5mCC3TwSkmRHv4+el5CzFU8ToQpYGhpV7mrRzqlwKkntlPilT1HJy1TVeEvEjWOQ==", "dependencies": { - "@aws-sdk/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", + "@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/@aws-sdk/types": { - "version": "3.734.0", - "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/core": { - "version": "3.1.5", - "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/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, + "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-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/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-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/types": "^4.1.0", + "@smithy/service-error-classification": "^4.0.6", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-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-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/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/node-config-provider": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "node_modules/@aws-sdk/client-s3-control/node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", "dependencies": { - "@smithy/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-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/protocol-http": { - "version": "5.0.1", + "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": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "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", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/signature-v4": { - "version": "5.0.1", + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sts": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-uri-escape": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/smithy-client": { - "version": "4.1.6", + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/core": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/core": "^3.1.5", - "@smithy/middleware-endpoint": "^4.0.6", - "@smithy/middleware-stack": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-stream": "^4.1.2", + "@aws-sdk/types": "3.692.0", + "@smithy/core": "^2.5.2", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/signature-v4": "^4.2.2", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-middleware": "^3.0.9", + "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/types": { - "version": "4.1.0", + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.693.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", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/url-parser": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/querystring-parser": "^4.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/credential-provider-sso/node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.693.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", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/util-middleware": { - "version": "4.0.1", + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.693.0", "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/@aws-sdk/credential-provider-sso/node_modules/@smithy/util-utf8": { - "version": "4.0.0", + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.693.0", "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", + "@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/middleware-host-header": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-host-header": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -5084,7 +12070,7 @@ "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/middleware-logger": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -5096,7 +12082,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/middleware-recursion-detection": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-recursion-detection": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -5109,7 +12095,7 @@ "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/middleware-user-agent": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -5125,7 +12111,7 @@ "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/region-config-resolver": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -5140,7 +12126,7 @@ "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/token-providers": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -5157,7 +12143,7 @@ "@aws-sdk/client-sso-oidc": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/util-endpoints": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-endpoints": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -5170,7 +12156,7 @@ "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/util-user-agent-browser": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -5180,7 +12166,7 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@aws-sdk/util-user-agent-node": { + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-node": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -5202,422 +12188,529 @@ } } }, - "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", + "node_modules/@aws-sdk/client-s3/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", "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/@smithy/is-array-buffer": { - "version": "4.0.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-builder": { - "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-uri-escape": "^4.0.0", + "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/querystring-builder/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/querystring-parser": { - "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/querystring-parser/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_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" + "node": ">=16.0.0" }, - "engines": { - "node": ">=18.0.0" + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/service-error-classification/node_modules/@smithy/types": { - "version": "4.1.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": { + "@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-buffer-from": { - "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": { - "@smithy/is-array-buffer": "^4.0.0", + "@aws-sdk/types": "3.692.0", + "@smithy/core": "^2.5.2", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/signature-v4": "^4.2.2", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-middleware": "^3.0.9", + "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-hex-encoding": { - "version": "4.0.0", + "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": { + "@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": { - "version": "4.1.2", + "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/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/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@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" + "node": ">=16.0.0" }, - "engines": { - "node": ">=18.0.0" + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" } }, - "node_modules/@aws-sdk/client-codecatalyst/node_modules/@smithy/util-stream/node_modules/@smithy/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" @@ -5626,30 +12719,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", @@ -5661,7 +12760,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", @@ -5693,7 +12791,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" @@ -5702,8 +12799,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", @@ -5749,8 +12848,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", @@ -5800,8 +12901,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", @@ -5849,8 +12952,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", @@ -5869,8 +12974,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", @@ -5888,8 +12995,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", @@ -5912,8 +13021,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", @@ -5933,8 +13044,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", @@ -5950,8 +13063,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", @@ -5967,8 +13082,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", @@ -5980,8 +13097,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", @@ -5992,8 +13111,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", @@ -6005,8 +13126,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", @@ -6021,8 +13144,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", @@ -6036,8 +13161,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", @@ -6053,8 +13180,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", @@ -6066,8 +13195,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", @@ -6076,8 +13207,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", @@ -6098,8 +13231,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" @@ -6108,8 +13243,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", @@ -6119,8 +13256,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", @@ -6130,7 +13269,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": { @@ -6175,13 +13314,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": { @@ -6228,7 +13369,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": { @@ -6279,7 +13420,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": { @@ -6328,7 +13469,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": { @@ -6348,7 +13489,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": { @@ -6367,7 +13508,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": { @@ -6391,7 +13532,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": { @@ -6412,7 +13553,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": { @@ -6429,7 +13570,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": { @@ -6446,7 +13587,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": { @@ -6459,7 +13600,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": { @@ -6471,7 +13612,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": { @@ -6484,7 +13625,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": { @@ -6500,7 +13641,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": { @@ -6515,7 +13656,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": { @@ -6532,7 +13673,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": { @@ -6545,7 +13686,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": { @@ -6555,7 +13696,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": { @@ -6577,7 +13718,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": { @@ -6587,7 +13728,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": { @@ -6598,7 +13739,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": { @@ -6609,14 +13750,59 @@ "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/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", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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/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", @@ -6630,9 +13816,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", @@ -6655,16 +13838,17 @@ "@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" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.637.0" } }, - "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/types": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/types": { "version": "3.609.0", "license": "Apache-2.0", "dependencies": { @@ -6675,7 +13859,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/fetch-http-handler": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/fetch-http-handler": { "version": "3.2.4", "license": "Apache-2.0", "dependencies": { @@ -6686,7 +13870,7 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/is-array-buffer": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/is-array-buffer": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { @@ -6696,7 +13880,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/util-utf8": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-utf8": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { @@ -6707,7 +13891,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-lambda/node_modules/@smithy/util-utf8/node_modules/@smithy/util-buffer-from": { + "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": { @@ -6718,314 +13902,239 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3": { - "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-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/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.4", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.0", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", "@smithy/util-base64": "^3.0.0", - "@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" + } + }, + "node_modules/@aws-sdk/client-sso/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso": { - "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-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.693.0", - "@aws-sdk/middleware-host-header": "3.693.0", - "@aws-sdk/middleware-logger": "3.693.0", - "@aws-sdk/middleware-recursion-detection": "3.693.0", - "@aws-sdk/middleware-user-agent": "3.693.0", - "@aws-sdk/region-config-resolver": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-endpoints": "3.693.0", - "@aws-sdk/util-user-agent-browser": "3.693.0", - "@aws-sdk/util-user-agent-node": "3.693.0", - "@smithy/config-resolver": "^3.0.11", - "@smithy/core": "^2.5.2", - "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/hash-node": "^3.0.9", - "@smithy/invalid-dependency": "^3.0.9", - "@smithy/middleware-content-length": "^3.0.11", - "@smithy/middleware-endpoint": "^3.2.2", - "@smithy/middleware-retry": "^3.0.26", - "@smithy/middleware-serde": "^3.0.9", - "@smithy/middleware-stack": "^3.0.9", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/node-http-handler": "^3.3.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/url-parser": "^3.0.9", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.26", - "@smithy/util-defaults-mode-node": "^3.0.26", - "@smithy/util-endpoints": "^2.1.5", - "@smithy/util-middleware": "^3.0.9", - "@smithy/util-retry": "^3.0.9", - "@smithy/util-utf8": "^3.0.0", + "@smithy/is-array-buffer": "^3.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.693.0", + "node_modules/@aws-sdk/client-sts": { + "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.693.0", - "@aws-sdk/credential-provider-node": "3.693.0", - "@aws-sdk/middleware-host-header": "3.693.0", - "@aws-sdk/middleware-logger": "3.693.0", - "@aws-sdk/middleware-recursion-detection": "3.693.0", - "@aws-sdk/middleware-user-agent": "3.693.0", - "@aws-sdk/region-config-resolver": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-endpoints": "3.693.0", - "@aws-sdk/util-user-agent-browser": "3.693.0", - "@aws-sdk/util-user-agent-node": "3.693.0", - "@smithy/config-resolver": "^3.0.11", - "@smithy/core": "^2.5.2", - "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/hash-node": "^3.0.9", - "@smithy/invalid-dependency": "^3.0.9", - "@smithy/middleware-content-length": "^3.0.11", - "@smithy/middleware-endpoint": "^3.2.2", - "@smithy/middleware-retry": "^3.0.26", - "@smithy/middleware-serde": "^3.0.9", - "@smithy/middleware-stack": "^3.0.9", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/node-http-handler": "^3.3.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/url-parser": "^3.0.9", + "@aws-sdk/client-sso-oidc": "3.637.0", + "@aws-sdk/core": "3.635.0", + "@aws-sdk/credential-provider-node": "3.637.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.637.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.637.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.4.0", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.26", - "@smithy/util-defaults-mode-node": "^3.0.26", - "@smithy/util-endpoints": "^2.1.5", - "@smithy/util-middleware": "^3.0.9", - "@smithy/util-retry": "^3.0.9", + "@smithy/util-defaults-mode-browser": "^3.0.15", + "@smithy/util-defaults-mode-node": "^3.0.15", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/types": { + "version": "3.609.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.693.0" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sts": { - "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-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/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" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/core": { - "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/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/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-http": { - "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/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/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-ini": { - "version": "3.693.0", + "node_modules/@aws-sdk/core": { + "version": "3.635.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.693.0", - "@aws-sdk/credential-provider-env": "3.693.0", - "@aws-sdk/credential-provider-http": "3.693.0", - "@aws-sdk/credential-provider-process": "3.693.0", - "@aws-sdk/credential-provider-sso": "3.693.0", - "@aws-sdk/credential-provider-web-identity": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/credential-provider-imds": "^3.2.6", - "@smithy/property-provider": "^3.1.9", - "@smithy/shared-ini-file-loader": "^3.1.10", - "@smithy/types": "^3.7.0", + "@smithy/core": "^2.4.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/signature-v4": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-cognito-identity": { + "version": "3.730.0", + "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/client-cognito-identity": "3.730.0", + "@aws-sdk/types": "3.723.0", + "@smithy/property-provider": "^4.0.0", + "@smithy/types": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-cognito-identity/node_modules/@aws-sdk/types": { + "version": "3.723.0", + "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/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" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.693.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.693.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": { - "@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": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-sso": { + "node_modules/@aws-sdk/credential-provider-env": { "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" }, @@ -7033,1280 +14142,951 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-web-identity": { + "node_modules/@aws-sdk/credential-provider-env/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", + "@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-s3/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.635.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/types": "^3.7.0", + "@aws-sdk/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-s3/node_modules/@aws-sdk/middleware-logger": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-http/node_modules/@aws-sdk/types": { + "version": "3.609.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/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/credential-provider-http/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" + } + }, + "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" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-user-agent": { - "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-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-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.5", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.7", + "@smithy/util-defaults-mode-node": "^4.0.7", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/region-config-resolver": { - "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/node-config-provider": "^3.1.10", - "@smithy/types": "^3.7.0", - "@smithy/util-config-provider": "^3.0.0", - "@smithy/util-middleware": "^3.0.9", + "@aws-sdk/types": "3.734.0", + "@smithy/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-s3/node_modules/@aws-sdk/token-providers": { - "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/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/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" - }, - "peerDependencies": { - "@aws-sdk/client-sso-oidc": "^3.693.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-endpoints": { - "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/types": "3.692.0", - "@smithy/types": "^3.7.0", - "@smithy/util-endpoints": "^2.1.5", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/property-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/util-stream": "^4.1.2", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-browser": { - "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/types": "3.692.0", - "@smithy/types": "^3.7.0", - "bowser": "^2.11.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@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-s3/node_modules/@aws-sdk/util-user-agent-node": { - "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/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/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" - }, - "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", + "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/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-s3/node_modules/@smithy/util-buffer-from": { - "version": "3.0.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": { - "@smithy/is-array-buffer": "^3.0.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-s3/node_modules/@smithy/util-utf8": { - "version": "3.0.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": { - "@smithy/util-buffer-from": "^3.0.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-sfn": { - "version": "3.693.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sfn/-/client-sfn-3.693.0.tgz", - "integrity": "sha512-B2K3aXGnP7eD1ITEIx4kO43l1N5OLqHdLW4AUbwoopwU5qzicc9jADrthXpGxymJI8AhJz9T2WtLmceBU2EpNg==", + "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-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" + "@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-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==", + "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-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.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-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==", + "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-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", + "@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" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.693.0" + "node": ">=18.0.0" } }, - "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==", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/types": { + "version": "3.734.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", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "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==", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/util-endpoints": { + "version": "3.743.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", + "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": ">=16.0.0" + "node": ">=18.0.0" } }, - "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==", + "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/core": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/node-http-handler": "^3.3.0", - "@smithy/property-provider": "^3.1.9", - "@smithy/protocol-http": "^4.1.6", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/util-stream": "^3.3.0", + "@aws-sdk/types": "3.734.0", + "@smithy/types": "^4.1.0", + "bowser": "^2.11.0", "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" } }, - "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==", + "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/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/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-sdk/client-sts": "^3.693.0" + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } } }, - "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==", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/abort-controller": { + "version": "4.0.1", "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", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "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==", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/config-resolver": { + "version": "4.0.1", "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", + "@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-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==", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/core": { + "version": "3.1.5", "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/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" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.693.0" + "node": ">=18.0.0" } }, - "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==", + "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-sdk/types": "3.692.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/types": "^3.7.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-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==", + "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-sdk/types": "3.692.0", - "@smithy/types": "^3.7.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/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==", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/hash-node": { + "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/types": "^3.7.0", + "@smithy/types": "^4.1.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-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==", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/invalid-dependency": { + "version": "4.0.1", "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", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "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==", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/types": "^3.7.0", - "@smithy/util-config-provider": "^3.0.0", - "@smithy/util-middleware": "^3.0.9", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "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==", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/middleware-content-length": { + "version": "4.0.1", "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/protocol-http": "^5.0.1", + "@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-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==", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/middleware-endpoint": { + "version": "4.0.6", "license": "Apache-2.0", + "peer": true, "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", - "@smithy/util-endpoints": "^2.1.5", + "@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-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==", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/middleware-retry": { + "version": "4.0.7", "license": "Apache-2.0", + "peer": true, "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", - "bowser": "^2.11.0", - "tslib": "^2.6.2" + "@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-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==", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/middleware-serde": { + "version": "4.0.2", "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", + "@smithy/types": "^4.1.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-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==", + "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": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "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==", + "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/is-array-buffer": "^3.0.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-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==", + "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/util-buffer-from": "^3.0.0", + "@smithy/abort-controller": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/querystring-builder": "^4.0.1", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ssm": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/property-provider": { + "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.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.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/client-sso": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/protocol-http": { + "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.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.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/client-sso-oidc": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/querystring-builder": { + "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.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.1.0", + "@smithy/util-uri-escape": "^4.0.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/client-sts": { - "version": "3.693.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.693.0", - "@aws-sdk/core": "3.693.0", - "@aws-sdk/credential-provider-node": "3.693.0", - "@aws-sdk/middleware-host-header": "3.693.0", - "@aws-sdk/middleware-logger": "3.693.0", - "@aws-sdk/middleware-recursion-detection": "3.693.0", - "@aws-sdk/middleware-user-agent": "3.693.0", - "@aws-sdk/region-config-resolver": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-endpoints": "3.693.0", - "@aws-sdk/util-user-agent-browser": "3.693.0", - "@aws-sdk/util-user-agent-node": "3.693.0", - "@smithy/config-resolver": "^3.0.11", - "@smithy/core": "^2.5.2", - "@smithy/fetch-http-handler": "^4.1.0", - "@smithy/hash-node": "^3.0.9", - "@smithy/invalid-dependency": "^3.0.9", - "@smithy/middleware-content-length": "^3.0.11", - "@smithy/middleware-endpoint": "^3.2.2", - "@smithy/middleware-retry": "^3.0.26", - "@smithy/middleware-serde": "^3.0.9", - "@smithy/middleware-stack": "^3.0.9", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/node-http-handler": "^3.3.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "@smithy/url-parser": "^3.0.9", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.26", - "@smithy/util-defaults-mode-node": "^3.0.26", - "@smithy/util-endpoints": "^2.1.5", - "@smithy/util-middleware": "^3.0.9", - "@smithy/util-retry": "^3.0.9", - "@smithy/util-utf8": "^3.0.0", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "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/client-ssm/node_modules/@aws-sdk/core": { - "version": "3.693.0", + "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": { - "@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.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-http": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/signature-v4": { + "version": "5.0.1", "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", + "@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-ssm/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/smithy-client": { + "version": "4.1.6", "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", + "@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" - }, - "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/@smithy/types": { + "version": "4.1.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", "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/@smithy/url-parser": { + "version": "4.0.1", "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", + "@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/client-ssm/node_modules/@aws-sdk/credential-provider-web-identity": { - "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-ini/node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "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/middleware-host-header": { - "version": "3.693.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.692.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/middleware-logger": { - "version": "3.693.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": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", + "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-config-provider": { + "version": "4.0.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", "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/@smithy/util-defaults-mode-browser": { + "version": "4.0.7", "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", + "@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": ">=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/@smithy/util-defaults-mode-node": { + "version": "4.0.7", "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", + "@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": ">=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/@smithy/util-endpoints": { + "version": "3.0.1", "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/node-config-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/credential-provider-ini/node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.6.2" }, - "peerDependencies": { - "@aws-sdk/client-sso-oidc": "^3.693.0" + "engines": { + "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/@smithy/util-middleware": { + "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", - "@smithy/util-endpoints": "^2.1.5", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.693.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-retry": { + "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", - "bowser": "^2.11.0", + "@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-ssm/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.693.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/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.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": ">=16.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/@smithy/is-array-buffer": { - "version": "3.0.0", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "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/util-utf8": { + "version": "4.0.0", "license": "Apache-2.0", + "peer": true, "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-ssm/node_modules/@smithy/util-utf8": { - "version": "3.0.0", + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.637.0", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^3.0.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": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sso": { - "version": "3.637.0", + "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.620.1", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.635.0", - "@aws-sdk/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/property-provider": "^3.1.3", "@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-sso-oidc": { + "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/credential-provider-ini": { "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/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", - "@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/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/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": { @@ -8316,10 +15096,13 @@ "@aws-sdk/client-sts": "^3.637.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/types": { - "version": "3.609.0", + "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.620.1", "license": "Apache-2.0", "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", "tslib": "^2.6.2" }, @@ -8327,53 +15110,77 @@ "node": ">=16.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-node/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.621.0", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^4.1.0", - "@smithy/querystring-builder": "^3.0.3", + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.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.621.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/is-array-buffer": { - "version": "3.0.0", + "node_modules/@aws-sdk/credential-provider-node/node_modules/@aws-sdk/types": { + "version": "3.609.0", "license": "Apache-2.0", "dependencies": { + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-utf8": { - "version": "3.0.0", + "node_modules/@aws-sdk/credential-provider-process": { + "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/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-sso-oidc/node_modules/@smithy/util-utf8/node_modules/@smithy/util-buffer-from": { - "version": "3.0.0", + "node_modules/@aws-sdk/credential-provider-process/node_modules/@aws-sdk/core": { + "version": "3.693.0", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^3.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": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/types": { - "version": "3.609.0", + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.637.0", "license": "Apache-2.0", "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" }, @@ -8381,261 +15188,235 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sso/node_modules/@smithy/fetch-http-handler": { - "version": "3.2.4", + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/types": { + "version": "3.609.0", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^4.1.0", - "@smithy/querystring-builder": "^3.0.3", "@smithy/types": "^3.3.0", - "@smithy/util-base64": "^3.0.0", "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sso/node_modules/@smithy/is-array-buffer": { - "version": "3.0.0", + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.758.0", "license": "Apache-2.0", + "peer": true, "dependencies": { + "@aws-sdk/core": "3.758.0", + "@aws-sdk/nested-clients": "3.758.0", + "@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-sso/node_modules/@smithy/util-utf8": { - "version": "3.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, "dependencies": { - "@smithy/util-buffer-from": "^3.0.0", + "@aws-sdk/types": "3.734.0", + "@smithy/core": "^3.1.5", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/signature-v4": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/util-middleware": "^4.0.1", + "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "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-web-identity/node_modules/@aws-sdk/types": { + "version": "3.734.0", "license": "Apache-2.0", + "peer": true, "dependencies": { - "@smithy/is-array-buffer": "^3.0.0", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sts": { - "version": "3.637.0", + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/abort-controller": { + "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-web-identity/node_modules/@smithy/core": { + "version": "3.1.5", "license": "Apache-2.0", + "peer": true, "dependencies": { - "@smithy/types": "^3.3.0", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-stream": "^4.1.2", + "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-sts/node_modules/@smithy/fetch-http-handler": { - "version": "3.2.4", + "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": "^4.1.0", - "@smithy/querystring-builder": "^3.0.3", - "@smithy/types": "^3.3.0", - "@smithy/util-base64": "^3.0.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/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-sts/node_modules/@smithy/is-array-buffer": { - "version": "3.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, "dependencies": { "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-web-identity/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-sts/node_modules/@smithy/util-utf8/node_modules/@smithy/util-buffer-from": { - "version": "3.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, "dependencies": { - "@smithy/is-array-buffer": "^3.0.0", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/core": { - "version": "3.635.0", + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/middleware-stack": { + "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/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-web-identity/node_modules/@smithy/node-config-provider": { + "version": "4.0.1", "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/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/credential-provider-env/node_modules/@aws-sdk/core": { - "version": "3.693.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, "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/abort-controller": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/querystring-builder": "^4.0.1", + "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.635.0", + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/property-provider": { + "version": "4.0.1", "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", + "@smithy/types": "^4.1.0", "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-web-identity/node_modules/@smithy/protocol-http": { + "version": "5.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/credential-provider-http/node_modules/@smithy/fetch-http-handler": { - "version": "3.2.4", + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/querystring-builder": { + "version": "4.0.1", "license": "Apache-2.0", + "peer": true, "dependencies": { - "@smithy/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", + "@smithy/util-uri-escape": "^4.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-web-identity/node_modules/@smithy/querystring-parser": { + "version": "4.0.1", "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" }, @@ -8643,47 +15424,29 @@ "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-web-identity/node_modules/@smithy/shared-ini-file-loader": { + "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.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", + "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, + "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-retry": "^4.0.1", + "@smithy/util-uri-escape": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, @@ -8691,130 +15454,99 @@ "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-web-identity/node_modules/@smithy/smithy-client": { + "version": "4.1.6", "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/middleware-endpoint": "^4.0.6", + "@smithy/middleware-stack": "^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/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-env": { - "version": "3.758.0", + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/types": { + "version": "4.1.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-web-identity/node_modules/@smithy/url-parser": { + "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/querystring-parser": "^4.0.1", "@smithy/types": "^4.1.0", - "@smithy/util-stream": "^4.1.2", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.758.0", + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-base64": { + "version": "4.0.0", "license": "Apache-2.0", "peer": true, "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@smithy/property-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", + "@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/@aws-sdk/credential-provider-sso": { - "version": "3.758.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, "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/credential-provider-ini/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.734.0", + "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": { - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", - "@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/credential-provider-ini/node_modules/@aws-sdk/middleware-logger": { - "version": "3.734.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, "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/credential-provider-ini/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.734.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": { - "@aws-sdk/types": "3.734.0", - "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, @@ -8822,197 +15554,371 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.758.0", + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-stream": { + "version": "4.1.2", "license": "Apache-2.0", "peer": true, "dependencies": { - "@aws-sdk/core": "3.758.0", - "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.743.0", - "@smithy/core": "^3.1.5", - "@smithy/protocol-http": "^5.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/region-config-resolver": { - "version": "3.734.0", + "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": { - "@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/credential-provider-ini/node_modules/@aws-sdk/token-providers": { - "version": "3.758.0", + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-utf8": { + "version": "4.0.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", + "@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/types": { - "version": "3.734.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": { - "@smithy/types": "^4.1.0", + "@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/@aws-sdk/util-endpoints": { - "version": "3.743.0", - "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": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "@smithy/util-endpoints": "^3.0.1", + "@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/@aws-sdk/util-user-agent-browser": { - "version": "3.734.0", - "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": { - "@aws-sdk/types": "3.734.0", - "@smithy/types": "^4.1.0", - "bowser": "^2.11.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/@aws-sdk/util-user-agent-node": { - "version": "3.758.0", - "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": { - "@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.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" }, - "peerDependencies": { - "aws-crt": ">=1.0.0" + "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" }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/abort-controller": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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": { - "@smithy/types": "^4.1.0", + "@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-provider-ini/node_modules/@smithy/config-resolver": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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": { - "@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/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-provider-ini/node_modules/@smithy/core": { - "version": "3.1.5", - "license": "Apache-2.0", - "peer": true, + "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": { - "@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.723.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/credential-provider-imds": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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": { - "@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.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-provider-ini/node_modules/@smithy/fetch-http-handler": { - "version": "5.0.1", - "license": "Apache-2.0", - "peer": true, + "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": { - "@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.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-provider-ini/node_modules/@smithy/hash-node": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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": { - "@smithy/types": "^4.1.0", - "@smithy/util-buffer-from": "^4.0.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" }, @@ -9020,413 +15926,452 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/invalid-dependency": { - "version": "4.0.1", - "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": { - "@smithy/types": "^4.1.0", + "@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/is-array-buffer": { - "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": { + "@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-ini/node_modules/@smithy/middleware-content-length": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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": { - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.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/middleware-endpoint": { - "version": "4.0.6", - "license": "Apache-2.0", - "peer": true, + "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": { - "@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.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/credential-provider-ini/node_modules/@smithy/middleware-retry": { - "version": "4.0.7", - "license": "Apache-2.0", - "peer": true, + "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": { - "@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" + "@aws-sdk/types": "3.723.0", + "@smithy/types": "^4.0.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/middleware-serde": { - "version": "4.0.2", - "license": "Apache-2.0", - "peer": true, + "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": { - "@smithy/types": "^4.1.0", + "@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/credential-provider-ini/node_modules/@smithy/middleware-stack": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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": { - "@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-ini/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/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/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/credential-provider-ini/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/core": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.5.3.tgz", + "integrity": "sha512-xa5byV9fEguZNofCclv6v9ra0FYh5FATQW/da7FQUVTic94DfrN/NvmKZjrMyzbpqfot9ZjBaO8U1UeTbmSLuA==", "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/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/credential-provider-ini/node_modules/@smithy/property-provider": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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": "^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/credential-provider-ini/node_modules/@smithy/protocol-http": { - "version": "5.0.1", - "license": "Apache-2.0", - "peer": true, + "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": { - "@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/credential-provider-ini/node_modules/@smithy/querystring-builder": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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.1.0", - "@smithy/util-uri-escape": "^4.0.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/credential-provider-ini/node_modules/@smithy/querystring-parser": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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": { - "@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-ini/node_modules/@smithy/service-error-classification": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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": { - "@smithy/types": "^4.1.0" + "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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": "^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-ini/node_modules/@smithy/signature-v4": { - "version": "5.0.1", - "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": { - "@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/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-ini/node_modules/@smithy/smithy-client": { - "version": "4.1.6", - "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": { - "@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" + "@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-ini/node_modules/@smithy/types": { - "version": "4.1.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/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-ini/node_modules/@smithy/url-parser": { - "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/querystring-parser": "^4.0.1", - "@smithy/types": "^4.1.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-base64": { - "version": "4.0.0", - "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/util-buffer-from": "^4.0.0", - "@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-ini/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/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/credential-provider-ini/node_modules/@smithy/util-body-length-node": { - "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-ini/node_modules/@smithy/util-buffer-from": { - "version": "4.0.0", - "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/is-array-buffer": "^4.0.0", + "@smithy/types": "^4.3.1", "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", - "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.3.1", + "@smithy/util-uri-escape": "^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-browser": { - "version": "4.0.7", - "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/property-provider": "^4.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "bowser": "^2.11.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-defaults-mode-node": { - "version": "4.0.7", - "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/config-resolver": "^4.0.1", - "@smithy/credential-provider-imds": "^4.0.1", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", + "@smithy/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-ini/node_modules/@smithy/util-endpoints": { - "version": "3.0.1", - "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/node-config-provider": "^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-ini/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/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/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/@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-ini/node_modules/@smithy/util-retry": { - "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/service-error-classification": "^4.0.1", - "@smithy/types": "^4.1.0", + "@smithy/querystring-parser": "^4.0.4", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/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/@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/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" }, @@ -9434,10 +16379,10 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-uri-escape": { + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-body-length-browser": { "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "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" }, @@ -9445,491 +16390,486 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/util-utf8": { + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/util-body-length-node": { "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", "dependencies": { - "@smithy/util-buffer-from": "^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/@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/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/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-node/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.620.1", - "license": "Apache-2.0", + "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": { - "@aws-sdk/types": "3.609.0", - "@smithy/property-provider": "^3.1.3", - "@smithy/types": "^3.3.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/@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": { - "@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", + "@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": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.637.0" + "node": ">=18.0.0" } }, - "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/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": { - "@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/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": ">=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/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.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", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.621.0" + "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/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/types": "^3.3.0", "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/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/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/types": "^4.3.1", "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/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": { - "@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/service-error-classification": "^4.0.5", + "@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/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": { - "@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", + "@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": ">=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/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", "dependencies": { - "@smithy/types": "^3.3.0", "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/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/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-buffer-from": "^4.0.0", "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", + "node_modules/@aws-sdk/lib-storage": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@aws-sdk/types": "3.734.0", - "@smithy/core": "^3.1.5", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/property-provider": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/signature-v4": "^5.0.1", - "@smithy/smithy-client": "^4.1.6", - "@smithy/types": "^4.1.0", - "@smithy/util-middleware": "^4.0.1", - "fast-xml-parser": "4.4.1", + "@smithy/abort-controller": "^3.1.7", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/smithy-client": "^3.4.3", + "buffer": "5.6.0", + "events": "3.3.0", + "stream-browserify": "3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-s3": "^3.693.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@aws-sdk/types": { - "version": "3.734.0", + "node_modules/@aws-sdk/lib-storage/node_modules/buffer": { + "version": "5.6.0", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "node_modules/@aws-sdk/lib-storage/node_modules/events": { + "version": "3.3.0", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-arn-parser": "3.693.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "@smithy/util-config-provider": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/abort-controller": { - "version": "4.0.1", + "node_modules/@aws-sdk/middleware-expect-continue": { + "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/credential-provider-web-identity/node_modules/@smithy/core": { - "version": "3.1.5", + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.693.0", "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/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-stream": "^3.3.0", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/fetch-http-handler": { - "version": "5.0.1", + "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/core": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/protocol-http": "^5.0.1", - "@smithy/querystring-builder": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-base64": "^4.0.0", + "@aws-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/credential-provider-web-identity/node_modules/@smithy/is-array-buffer": { - "version": "4.0.0", + "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/middleware-endpoint": { - "version": "4.0.6", + "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/core": "^3.1.5", - "@smithy/middleware-serde": "^4.0.2", - "@smithy/node-config-provider": "^4.0.1", - "@smithy/shared-ini-file-loader": "^4.0.1", - "@smithy/types": "^4.1.0", - "@smithy/url-parser": "^4.0.1", - "@smithy/util-middleware": "^4.0.1", + "@smithy/is-array-buffer": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/middleware-serde": { - "version": "4.0.2", + "node_modules/@aws-sdk/middleware-flexible-checksums/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/credential-provider-web-identity/node_modules/@smithy/middleware-stack": { - "version": "4.0.1", + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.620.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.609.0", + "@smithy/protocol-http": "^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/credential-provider-web-identity/node_modules/@smithy/node-config-provider": { - "version": "4.0.1", + "node_modules/@aws-sdk/middleware-host-header/node_modules/@aws-sdk/types": { + "version": "3.609.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", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/node-http-handler": { - "version": "4.0.3", + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/abort-controller": "^4.0.1", - "@smithy/protocol-http": "^5.0.1", - "@smithy/querystring-builder": "^4.0.1", - "@smithy/types": "^4.1.0", + "@aws-sdk/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/credential-provider-web-identity/node_modules/@smithy/property-provider": { - "version": "4.0.1", + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.609.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/protocol-http": { - "version": "5.0.1", + "node_modules/@aws-sdk/middleware-logger/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/credential-provider-web-identity/node_modules/@smithy/querystring-builder": { - "version": "4.0.1", + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.620.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/types": "^4.1.0", - "@smithy/util-uri-escape": "^4.0.0", + "@aws-sdk/types": "3.609.0", + "@smithy/protocol-http": "^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/credential-provider-web-identity/node_modules/@smithy/querystring-parser": { - "version": "4.0.1", + "node_modules/@aws-sdk/middleware-recursion-detection/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/credential-provider-web-identity/node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.1", + "node_modules/@aws-sdk/middleware-sdk-api-gateway": { + "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/credential-provider-web-identity/node_modules/@smithy/signature-v4": { - "version": "5.0.1", + "node_modules/@aws-sdk/middleware-sdk-ec2": { + "version": "3.693.0", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", - "@smithy/protocol-http": "^5.0.1", - "@smithy/types": "^4.1.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-middleware": "^4.0.1", - "@smithy/util-uri-escape": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-format-url": "3.693.0", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/protocol-http": "^4.1.6", + "@smithy/signature-v4": "^4.2.2", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/smithy-client": { - "version": "4.1.6", + "node_modules/@aws-sdk/middleware-sdk-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-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-arn-parser": "3.693.0", + "@smithy/core": "^2.5.2", + "@smithy/node-config-provider": "^3.1.10", + "@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-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-stream": "^3.3.0", + "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.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/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/credential-provider-web-identity/node_modules/@smithy/url-parser": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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": { - "@smithy/querystring-parser": "^4.0.1", - "@smithy/types": "^4.1.0", + "@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/credential-provider-web-identity/node_modules/@smithy/util-base64": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "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/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/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/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" }, @@ -9937,168 +16877,132 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-buffer-from": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "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": { - "@smithy/is-array-buffer": "^4.0.0", + "@aws-sdk/types": "3.840.0", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-endpoints": "^3.0.6", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/util-hex-encoding": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "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/credential-provider-web-identity/node_modules/@smithy/util-middleware": { - "version": "4.0.1", - "license": "Apache-2.0", - "peer": true, + "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.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/util-stream": { - "version": "4.1.2", - "license": "Apache-2.0", - "peer": true, + "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/fetch-http-handler": "^5.0.1", - "@smithy/node-http-handler": "^4.0.3", - "@smithy/types": "^4.1.0", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/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-uri-escape": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "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/credential-provider-web-identity/node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "license": "Apache-2.0", - "peer": true, + "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/util-buffer-from": "^4.0.0", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/lib-storage": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@smithy/abort-controller": "^3.1.7", - "@smithy/middleware-endpoint": "^3.2.2", - "@smithy/smithy-client": "^3.4.3", - "buffer": "5.6.0", - "events": "3.3.0", - "stream-browserify": "3.0.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-s3": "^3.693.0" - } - }, - "node_modules/@aws-sdk/lib-storage/node_modules/buffer": { - "version": "5.6.0", - "license": "MIT", - "dependencies": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "node_modules/@aws-sdk/lib-storage/node_modules/events": { - "version": "3.3.0", - "license": "MIT", - "engines": { - "node": ">=0.8.x" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-bucket-endpoint": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-arn-parser": "3.693.0", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/protocol-http": "^4.1.6", - "@smithy/types": "^3.7.0", - "@smithy/util-config-provider": "^3.0.0", + "@smithy/querystring-parser": "^4.0.4", + "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-expect-continue": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@aws-sdk/types": "3.692.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@aws-crypto/crc32": "5.2.0", - "@aws-crypto/crc32c": "5.2.0", - "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@smithy/is-array-buffer": "^3.0.0", - "@smithy/node-config-provider": "^3.1.10", - "@smithy/protocol-http": "^4.1.6", - "@smithy/types": "^3.7.0", - "@smithy/util-middleware": "^3.0.9", - "@smithy/util-stream": "^3.3.0", - "@smithy/util-utf8": "^3.0.0", + "@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/middleware-flexible-checksums/node_modules/@aws-sdk/core": { + "node_modules/@aws-sdk/middleware-sdk-s3/node_modules/@aws-sdk/core": { "version": "3.693.0", "license": "Apache-2.0", "dependencies": { @@ -10118,7 +17022,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@smithy/is-array-buffer": { + "node_modules/@aws-sdk/middleware-sdk-s3/node_modules/@smithy/is-array-buffer": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { @@ -10128,7 +17032,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@smithy/util-buffer-from": { + "node_modules/@aws-sdk/middleware-sdk-s3/node_modules/@smithy/util-buffer-from": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { @@ -10139,7 +17043,7 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@smithy/util-utf8": { + "node_modules/@aws-sdk/middleware-sdk-s3/node_modules/@smithy/util-utf8": { "version": "3.0.0", "license": "Apache-2.0", "dependencies": { @@ -10150,192 +17054,230 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.620.0", - "license": "Apache-2.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/types": "3.609.0", - "@smithy/protocol-http": "^4.1.0", - "@smithy/types": "^3.3.0", - "tslib": "^2.6.2" + "@aws-sdk/middleware-signing": "3.363.0", + "@aws-sdk/types": "3.357.0", + "@smithy/types": "^1.1.0", + "tslib": "^2.5.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/middleware-host-header/node_modules/@aws-sdk/types": { - "version": "3.609.0", - "license": "Apache-2.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": { - "@smithy/types": "^3.3.0", - "tslib": "^2.6.2" + "tslib": "^2.5.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/middleware-location-constraint": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@aws-sdk/types": "3.692.0", - "@smithy/types": "^3.7.0", - "tslib": "^2.6.2" + "tslib": "^2.5.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/middleware-logger": { - "version": "3.609.0", - "license": "Apache-2.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.609.0", - "@smithy/types": "^3.3.0", - "tslib": "^2.6.2" + "@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": ">=16.0.0" + "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/middleware-logger/node_modules/@aws-sdk/types": { - "version": "3.609.0", - "license": "Apache-2.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": { - "@smithy/types": "^3.3.0", - "tslib": "^2.6.2" + "@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": ">=16.0.0" + "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.620.0", - "license": "Apache-2.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-sdk/types": "3.609.0", - "@smithy/protocol-http": "^4.1.0", - "@smithy/types": "^3.3.0", - "tslib": "^2.6.2" + "@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": ">=16.0.0" + "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/middleware-recursion-detection/node_modules/@aws-sdk/types": { - "version": "3.609.0", - "license": "Apache-2.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": "^3.3.0", - "tslib": "^2.6.2" + "@smithy/types": "^1.2.0", + "tslib": "^2.5.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/middleware-sdk-api-gateway": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@aws-sdk/types": "3.692.0", - "@smithy/protocol-http": "^4.1.6", - "@smithy/types": "^3.7.0", - "tslib": "^2.6.2" + "@smithy/types": "^1.2.0", + "tslib": "^2.5.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/middleware-sdk-ec2": { - "version": "3.693.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-format-url": "3.693.0", - "@smithy/middleware-endpoint": "^3.2.2", - "@smithy/protocol-http": "^4.1.6", - "@smithy/signature-v4": "^4.2.2", - "@smithy/smithy-client": "^3.4.3", - "@smithy/types": "^3.7.0", - "tslib": "^2.6.2" + "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": ">=16.0.0" + "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@aws-sdk/core": "3.693.0", - "@aws-sdk/types": "3.692.0", - "@aws-sdk/util-arn-parser": "3.693.0", - "@smithy/core": "^2.5.2", - "@smithy/node-config-provider": "^3.1.10", - "@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-config-provider": "^3.0.0", - "@smithy/util-middleware": "^3.0.9", - "@smithy/util-stream": "^3.3.0", - "@smithy/util-utf8": "^3.0.0", - "tslib": "^2.6.2" + "@smithy/is-array-buffer": "^1.1.0", + "tslib": "^2.5.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/middleware-sdk-s3/node_modules/@aws-sdk/core": { - "version": "3.693.0", - "license": "Apache-2.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": { - "@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" + "tslib": "^2.5.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/middleware-sdk-s3/node_modules/@smithy/is-array-buffer": { - "version": "3.0.0", - "license": "Apache-2.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.6.2" + "tslib": "^2.5.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/middleware-sdk-s3/node_modules/@smithy/util-buffer-from": { - "version": "3.0.0", - "license": "Apache-2.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": { - "@smithy/is-array-buffer": "^3.0.0", - "tslib": "^2.6.2" + "tslib": "^2.5.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/middleware-sdk-s3/node_modules/@smithy/util-utf8": { - "version": "3.0.0", - "license": "Apache-2.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": "^3.0.0", - "tslib": "^2.6.2" + "@smithy/util-buffer-from": "^1.1.0", + "tslib": "^2.5.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=14.0.0" } }, "node_modules/@aws-sdk/middleware-ssec": { @@ -11393,6 +18335,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", @@ -11405,9 +18355,9 @@ } }, "node_modules/@aws-toolkits/telemetry": { - "version": "1.0.324", - "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.324.tgz", - "integrity": "sha512-O4K9Ip3ge+EdTITOhMNcVxp+DPxK/1JHm9XcDwg/5N3q9SbwQ7/WeVtTHkvgq+IiQGKjnJ/4Vuyw3/3h29K7ww==", + "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": { @@ -11431,20 +18381,22 @@ } }, "node_modules/@aws/chat-client-ui-types": { - "version": "0.1.26", + "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/language-server-runtimes": { - "version": "0.2.97", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes/-/language-server-runtimes-0.2.97.tgz", - "integrity": "sha512-Wzt09iC5YTVRJmmW6DwunBFSR0mV+cHjDwJ5iic1sEvXlI9CnrxlEjfn09crkVQ2XZj3dNJHoLQPptH+AEQfNg==", - "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.39", + "@aws/language-server-runtimes-types": "^0.1.56", "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.200.0", "@opentelemetry/core": "^2.0.0", @@ -11459,7 +18411,7 @@ "hpagent": "^1.2.0", "jose": "^5.9.6", "mac-ca": "^3.1.1", - "os-proxy-config": "^1.1.2", + "registry-js": "^1.16.1", "rxjs": "^7.8.2", "vscode-languageserver": "^9.0.1", "vscode-languageserver-protocol": "^3.17.5", @@ -11471,10 +18423,10 @@ } }, "node_modules/@aws/language-server-runtimes-types": { - "version": "0.1.39", - "resolved": "https://registry.npmjs.org/@aws/language-server-runtimes-types/-/language-server-runtimes-types-0.1.39.tgz", - "integrity": "sha512-HjZ9tYcs++vcSyNwCcGLC8k1nvdWTD7XRa6sI71OYwFzJvyMa4/BY7Womq/kmyuD/IB6MRVvuRdgYQxuU1mSGA==", - "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" @@ -11484,7 +18436,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz", "integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==", - "dev": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -11499,7 +18450,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", - "dev": true, "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -11515,7 +18465,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz", "integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==", - "dev": true, "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1" @@ -11531,7 +18480,6 @@ "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", @@ -11545,7 +18493,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", @@ -11562,7 +18509,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", @@ -11576,7 +18522,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", @@ -11591,7 +18536,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" @@ -11604,7 +18548,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" @@ -11615,7 +18558,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", @@ -11630,7 +18572,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" @@ -11638,12 +18579,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" @@ -11651,7 +18590,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" @@ -11662,7 +18600,6 @@ }, "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", @@ -11672,8 +18609,7 @@ "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==", - "dev": true + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==" }, "node_modules/@aws/mynah-ui": { "version": "4.35.4", @@ -12253,7 +19189,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" @@ -12263,7 +19198,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" @@ -12276,7 +19210,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" @@ -12292,7 +19225,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", @@ -12312,7 +19244,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", @@ -12332,7 +19263,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", @@ -12349,7 +19279,6 @@ "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", @@ -12371,7 +19300,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", @@ -12388,7 +19316,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", @@ -12406,7 +19333,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" @@ -12422,7 +19348,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", @@ -12439,7 +19364,6 @@ "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", @@ -12456,7 +19380,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", @@ -12474,7 +19397,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" @@ -12515,35 +19437,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", @@ -12554,35 +19471,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": { @@ -12963,6 +19875,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", @@ -13820,11 +20780,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": "*" @@ -15275,14 +22235,14 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true + "license": "ISC" }, "node_modules/are-we-there-yet": { "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.", - "dev": true, + "license": "ISC", "dependencies": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -15292,7 +22252,7 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -15307,13 +22267,13 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "license": "MIT" }, "node_modules/are-we-there-yet/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } @@ -15669,7 +22629,6 @@ }, "node_modules/bl": { "version": "4.1.0", - "dev": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -15679,7 +22638,6 @@ }, "node_modules/bl/node_modules/buffer": { "version": "5.7.1", - "dev": true, "funding": [ { "type": "github", @@ -16226,7 +23184,6 @@ }, "node_modules/chownr": { "version": "1.1.4", - "dev": true, "license": "ISC" }, "node_modules/chrome-trace-event": { @@ -16422,7 +23379,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", - "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -16577,7 +23534,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "dev": true + "license": "ISC" }, "node_modules/content-disposition": { "version": "0.5.4", @@ -17007,7 +23964,6 @@ }, "node_modules/deep-extend": { "version": "0.6.0", - "dev": true, "license": "MIT", "engines": { "node": ">=4.0.0" @@ -17089,7 +24045,6 @@ }, "node_modules/delegates": { "version": "1.0.0", - "dev": true, "license": "MIT" }, "node_modules/depd": { @@ -18082,7 +25037,6 @@ }, "node_modules/expand-template": { "version": "2.0.3", - "dev": true, "license": "(MIT OR WTFPL)", "engines": { "node": ">=6" @@ -18251,7 +25205,6 @@ }, "node_modules/fast-uri": { "version": "3.0.6", - "dev": true, "funding": [ { "type": "github", @@ -18553,7 +25506,6 @@ }, "node_modules/fs-constants": { "version": "1.0.0", - "dev": true, "license": "MIT" }, "node_modules/fs-extra": { @@ -18625,7 +25577,7 @@ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==", "deprecated": "This package is no longer supported.", - "dev": true, + "license": "ISC", "dependencies": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -18641,7 +25593,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -18650,7 +25602,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", - "dev": true, + "license": "MIT", "dependencies": { "number-is-nan": "^1.0.0" }, @@ -18662,7 +25614,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", - "dev": true, + "license": "MIT", "dependencies": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -18676,7 +25628,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^2.0.0" }, @@ -18753,7 +25705,6 @@ }, "node_modules/github-from-package": { "version": "0.0.0", - "dev": true, "license": "MIT" }, "node_modules/glob": { @@ -19000,7 +25951,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "dev": true + "license": "ISC" }, "node_modules/hash-base": { "version": "3.1.0", @@ -19112,7 +26063,6 @@ }, "node_modules/hpagent": { "version": "1.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -19462,7 +26412,6 @@ }, "node_modules/ini": { "version": "1.3.8", - "dev": true, "license": "ISC" }, "node_modules/internal-slot": { @@ -19645,7 +26594,6 @@ }, "node_modules/is-electron": { "version": "2.2.2", - "dev": true, "license": "MIT" }, "node_modules/is-extglob": { @@ -20644,10 +27592,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": { @@ -20678,19 +27625,12 @@ }, "node_modules/mac-ca": { "version": "3.1.1", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "node-forge": "^1.3.1", "undici": "^6.16.1" } }, - "node_modules/mac-system-proxy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mac-system-proxy/-/mac-system-proxy-1.0.4.tgz", - "integrity": "sha512-IAkNLxXZrYuM99A2OhPrvUoAxohsxQciJh2D2xnD+R6vypn/AVyOYLsbZsMVCS/fEbLIe67nQ8krEAfqP12BVg==", - "dev": true - }, "node_modules/magic-string": { "version": "0.30.0", "license": "MIT", @@ -21056,7 +27996,6 @@ }, "node_modules/mkdirp-classic": { "version": "0.5.3", - "dev": true, "license": "MIT" }, "node_modules/mocha": { @@ -21367,7 +28306,6 @@ }, "node_modules/napi-build-utils": { "version": "1.0.2", - "dev": true, "license": "MIT" }, "node_modules/natural-compare": { @@ -21467,7 +28405,6 @@ }, "node_modules/node-forge": { "version": "1.3.1", - "dev": true, "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" @@ -21482,7 +28419,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", "integrity": "sha512-6kM8CLXvuW5crTxsAtva2YLrRrDaiTIkIePWs9moLHqbFWT94WpNFjwS/5dfLfECg5i/lkmw3aoqVidxt23TEQ==", - "dev": true + "license": "MIT" }, "node_modules/normalize-package-data": { "version": "3.0.3", @@ -21532,7 +28469,7 @@ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "deprecated": "This package is no longer supported.", - "dev": true, + "license": "ISC", "dependencies": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -21555,7 +28492,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", - "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -21599,7 +28536,6 @@ }, "node_modules/object-assign": { "version": "4.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -21745,16 +28681,6 @@ "version": "0.3.0", "license": "MIT" }, - "node_modules/os-proxy-config": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/os-proxy-config/-/os-proxy-config-1.1.2.tgz", - "integrity": "sha512-sV7htE8y6NQORU0oKOUGTwQYe1gSFK3a3Z1i4h6YaqdrA9C0JIsUPQAqEkO8ejjYbRrQ+jsnks5qjtisr7042Q==", - "dev": true, - "dependencies": { - "mac-system-proxy": "^1.0.0", - "windows-system-proxy": "^1.0.0" - } - }, "node_modules/p-cancelable": { "version": "2.1.1", "license": "MIT", @@ -22046,7 +28972,6 @@ }, "node_modules/pify": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -22418,10 +29343,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": { @@ -22624,7 +29548,6 @@ }, "node_modules/rc": { "version": "1.2.8", - "dev": true, "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", @@ -22638,7 +29561,6 @@ }, "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -22888,8 +29810,8 @@ "version": "1.16.1", "resolved": "https://registry.npmjs.org/registry-js/-/registry-js-1.16.1.tgz", "integrity": "sha512-pQ2kD36lh+YNtpaXm6HCCb0QZtV/zQEeKnkfEIj5FDSpF/oFts7pwizEUkWSvP8IbGb4A4a5iBhhS9eUearMmQ==", - "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { "node-addon-api": "^3.2.1", "prebuild-install": "^5.3.5" @@ -22899,7 +29821,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", - "dev": true, + "license": "MIT", "dependencies": { "mimic-response": "^2.0.0" }, @@ -22911,7 +29833,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "dev": true, + "license": "Apache-2.0", "bin": { "detect-libc": "bin/detect-libc.js" }, @@ -22923,7 +29845,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", - "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -22935,7 +29857,7 @@ "version": "2.30.1", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", - "dev": true, + "license": "MIT", "dependencies": { "semver": "^5.4.1" } @@ -22944,13 +29866,13 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", - "dev": true + "license": "MIT" }, "node_modules/registry-js/node_modules/prebuild-install": { "version": "5.3.6", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.6.tgz", "integrity": "sha512-s8Aai8++QQGi4sSbs/M1Qku62PFK49Jm1CbgXklGz4nmHveDq0wzJkg7Na5QbnO1uNH8K7iqx2EQ/mV0MZEmOg==", - "dev": true, + "license": "MIT", "dependencies": { "detect-libc": "^1.0.3", "expand-template": "^2.0.3", @@ -22979,7 +29901,7 @@ "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver" } @@ -22988,7 +29910,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", - "dev": true, + "license": "MIT", "dependencies": { "decompress-response": "^4.2.0", "once": "^1.3.1", @@ -23089,7 +30011,6 @@ }, "node_modules/require-from-string": { "version": "2.0.2", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -23286,7 +30207,6 @@ }, "node_modules/rxjs": { "version": "7.8.2", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" @@ -23598,7 +30518,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true + "license": "ISC" }, "node_modules/set-function-length": { "version": "1.2.2", @@ -23704,12 +30624,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", @@ -24283,7 +31201,6 @@ }, "node_modules/tar-fs": { "version": "2.1.1", - "dev": true, "license": "MIT", "dependencies": { "chownr": "^1.1.1", @@ -24294,7 +31211,6 @@ }, "node_modules/tar-stream": { "version": "2.2.0", - "dev": true, "license": "MIT", "dependencies": { "bl": "^4.0.3", @@ -24730,7 +31646,6 @@ }, "node_modules/tunnel-agent": { "version": "0.6.0", - "dev": true, "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" @@ -24863,7 +31778,6 @@ }, "node_modules/undici": { "version": "6.21.2", - "dev": true, "license": "MIT", "engines": { "node": ">=18.17" @@ -25838,7 +32752,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", - "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -25865,7 +32779,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dev": true, + "license": "ISC", "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } @@ -25877,7 +32791,6 @@ }, "node_modules/win-ca": { "version": "3.5.1", - "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -25889,7 +32802,6 @@ }, "node_modules/win-ca/node_modules/make-dir": { "version": "1.3.0", - "dev": true, "license": "MIT", "dependencies": { "pify": "^3.0.0" @@ -25898,15 +32810,6 @@ "node": ">=4" } }, - "node_modules/windows-system-proxy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/windows-system-proxy/-/windows-system-proxy-1.0.0.tgz", - "integrity": "sha512-qd1WfyX9gjAqI36RHt95di2+FBr74DhvELd1EASgklCGScjwReHnWnXfUyabp/CJWl/IdnkUzG0Ub6Cv2R4KJQ==", - "dev": true, - "dependencies": { - "registry-js": "^1.15.1" - } - }, "node_modules/winston": { "version": "3.11.0", "license": "MIT", @@ -26022,7 +32925,6 @@ }, "node_modules/ws": { "version": "8.17.1", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -26360,7 +33262,7 @@ }, "packages/amazonq": { "name": "amazon-q-vscode", - "version": "1.80.0-SNAPSHOT", + "version": "1.99.0-SNAPSHOT", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" @@ -26378,6 +33280,7 @@ "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-api-gateway": "<3.731.0", "@aws-sdk/client-apprunner": "<3.731.0", "@aws-sdk/client-cloudcontrol": "<3.731.0", @@ -26385,12 +33288,16 @@ "@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-glue": "^3.852.0", "@aws-sdk/client-iam": "<3.731.0", "@aws-sdk/client-lambda": "<3.731.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-sfn": "<3.731.0", "@aws-sdk/client-ssm": "<3.731.0", "@aws-sdk/client-sso": "<3.731.0", @@ -26398,6 +33305,7 @@ "@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", @@ -26443,6 +33351,7 @@ "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", @@ -26457,15 +33366,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.97", - "@aws/language-server-runtimes-types": "^0.1.39", + "@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", @@ -28079,7 +34989,7 @@ }, "packages/toolkit": { "name": "aws-toolkit-vscode", - "version": "3.68.0-SNAPSHOT", + "version": "3.79.0-SNAPSHOT", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" @@ -28094,6 +35004,7 @@ "version": "1.0.0", "license": "Apache-2.0", "devDependencies": { + "@types/eslint": "^8.56.0", "mocha": "^10.1.0" }, "engines": { diff --git a/package.json b/package.json index 8dab28aba35..dd196da079f 100644 --- a/package.json +++ b/package.json @@ -38,10 +38,11 @@ "reset": "npm run clean && ts-node ./scripts/clean.ts node_modules && npm install", "generateNonCodeFiles": "npm run generateNonCodeFiles -w packages/ --if-present", "mergeReports": "ts-node ./scripts/mergeReports.ts", - "skippedTestReport": "ts-node ./scripts/skippedTestReport.ts ./packages/amazonq/test/e2e/" + "skippedTestReport": "ts-node ./scripts/skippedTestReport.ts ./packages/amazonq/test/e2e/", + "scan-licenses": "ts-node ./scripts/scan-licenses.ts" }, "devDependencies": { - "@aws-toolkits/telemetry": "^1.0.324", + "@aws-toolkits/telemetry": "^1.0.329", "@playwright/browser-chromium": "^1.43.1", "@stylistic/eslint-plugin": "^2.11.0", "@types/he": "^1.2.3", @@ -74,6 +75,7 @@ "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", 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/CHANGELOG.md b/packages/amazonq/CHANGELOG.md index e4d0ff47c77..afef3bdc7a7 100644 --- a/packages/amazonq/CHANGELOG.md +++ b/packages/amazonq/CHANGELOG.md @@ -1,3 +1,89 @@ +## 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 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 9b6c3dd50bd..cfe150bd418 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.80.0-SNAPSHOT", + "description": "The most capable generative AI–powered assistant for software development.", + "version": "1.99.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%" @@ -831,18 +851,42 @@ }, { "command": "aws.amazonq.inline.acceptEdit", - "title": "%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%" + "title": "%AWS.amazonq.inline.rejectEdit%", + "category": "%AWS.amazonq.title%", + "enablement": "aws.codewhisperer.connected" }, { "command": "aws.amazonq.toggleNextEditPredictionPanel", - "title": "%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", @@ -875,7 +919,7 @@ }, { "command": "aws.amazonq.fixCode", - "win": "win+alt+y", + "win": "win+alt+h", "mac": "cmd+alt+y", "linux": "meta+alt+y" }, @@ -893,7 +937,7 @@ }, { "command": "aws.amazonq.generateUnitTests", - "key": "win+alt+t", + "key": "win+alt+n", "mac": "cmd+alt+t", "linux": "meta+alt+t" }, @@ -917,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" }, { @@ -1227,110 +1275,138 @@ "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-schemas-registry": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e1" + } + }, + "aws-schemas-schema": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e2" + } + }, + "aws-stepfunctions-preview": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e3" + } } }, "walkthroughs": [ 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 index 24014d692ea..7f9ca54b6aa 100644 --- a/packages/amazonq/src/app/inline/EditRendering/diffUtils.ts +++ b/packages/amazonq/src/app/inline/EditRendering/diffUtils.ts @@ -16,21 +16,13 @@ export type LineDiff = * @param unifiedDiff The unified diff content * @returns The modified code after applying the diff */ -export function applyUnifiedDiff( - docText: string, - unifiedDiff: string -): { appliedCode: string; addedCharacterCount: number; deletedCharacterCount: number } { +export function applyUnifiedDiff(docText: string, unifiedDiff: string): string { try { - const { addedCharacterCount, deletedCharacterCount } = getAddedAndDeletedCharCount(unifiedDiff) // First try the standard diff package try { const result = applyPatch(docText, unifiedDiff) if (result !== false) { - return { - appliedCode: result, - addedCharacterCount: addedCharacterCount, - deletedCharacterCount: deletedCharacterCount, - } + return result } } catch (error) {} @@ -94,49 +86,8 @@ export function applyUnifiedDiff( // Replace the text result = result.replace(textToReplace, newText) } - return { - appliedCode: result, - addedCharacterCount: addedCharacterCount, - deletedCharacterCount: deletedCharacterCount, - } + return result } catch (error) { - return { - appliedCode: docText, // Return original text if all methods fail - addedCharacterCount: 0, - deletedCharacterCount: 0, - } - } -} - -export function getAddedAndDeletedCharCount(diff: string): { - addedCharacterCount: number - deletedCharacterCount: number -} { - let addedCharacterCount = 0 - let deletedCharacterCount = 0 - let i = 0 - const lines = diff.split('\n') - while (i < lines.length) { - const line = lines[i] - if (line.startsWith('+') && !line.startsWith('+++')) { - addedCharacterCount += line.length - 1 - } else if (line.startsWith('-') && !line.startsWith('---')) { - const removedLine = line.substring(1) - deletedCharacterCount += removedLine.length - - // Check if this is a modified line rather than a pure deletion - const nextLine = lines[i + 1] - if (nextLine && nextLine.startsWith('+') && !nextLine.startsWith('+++') && nextLine.includes(removedLine)) { - // This is a modified line, not a pure deletion - // We've already counted the deletion, so we'll just increment i to skip the next line - // since we'll process the addition on the next iteration - i += 1 - } - } - i += 1 - } - return { - addedCharacterCount, - deletedCharacterCount, + 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 index 80d5231f113..0af4d4801c0 100644 --- a/packages/amazonq/src/app/inline/EditRendering/displayImage.ts +++ b/packages/amazonq/src/app/inline/EditRendering/displayImage.ts @@ -3,15 +3,20 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { getLogger, setContext } from 'aws-core-vscode/shared' +import { getContext, getLogger, setContext } from 'aws-core-vscode/shared' import * as vscode from 'vscode' -import { diffLines } from 'diff' +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 export class EditDecorationManager { private imageDecorationType: vscode.TextEditorDecorationType @@ -19,7 +24,7 @@ export class EditDecorationManager { private currentImageDecoration: vscode.DecorationOptions | undefined private currentRemovedCodeDecorations: vscode.DecorationOptions[] = [] private acceptHandler: (() => void) | undefined - private rejectHandler: (() => void) | undefined + private rejectHandler: ((isDiscard: boolean) => void) | undefined constructor() { this.registerCommandHandlers() @@ -121,19 +126,21 @@ export class EditDecorationManager { /** * Displays an edit suggestion as an SVG image in the editor and highlights removed code */ - public displayEditSuggestion( + public async displayEditSuggestion( editor: vscode.TextEditor, svgImage: vscode.Uri, startLine: number, - onAccept: () => void, - onReject: () => void, + onAccept: () => Promise, + onReject: (isDiscard: boolean) => Promise, originalCode: string, newCode: string, originalCodeHighlightRanges: Array<{ line: number; start: number; end: number }> - ): void { - this.clearDecorations(editor) - - void setContext('aws.amazonq.editSuggestionActive' as any, true) + ): 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 @@ -156,14 +163,15 @@ export class EditDecorationManager { /** * Clears all edit suggestion decorations */ - public clearDecorations(editor: vscode.TextEditor): void { + 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 - void setContext('aws.amazonq.editSuggestionActive' as any, false) + await setContext('aws.amazonq.editSuggestionActive' as any, false) + EditSuggestionState.setEditSuggestionActive(false) } /** @@ -178,9 +186,9 @@ export class EditDecorationManager { }) // Register Esc key handler for rejecting suggestion - vscode.commands.registerCommand('aws.amazonq.inline.rejectEdit', () => { + vscode.commands.registerCommand('aws.amazonq.inline.rejectEdit', (isDiscard: boolean = false) => { if (this.rejectHandler) { - this.rejectHandler() + this.rejectHandler(isDiscard) } }) } @@ -210,7 +218,7 @@ export const decorationManager = EditDecorationManager.getDecorationManager() /** * Function to replace editor's content with new code */ -function replaceEditorContent(editor: vscode.TextEditor, newCode: string): void { +async function replaceEditorContent(editor: vscode.TextEditor, newCode: string): Promise { const document = editor.document const fullRange = new vscode.Range( 0, @@ -219,7 +227,7 @@ function replaceEditorContent(editor: vscode.TextEditor, newCode: string): void document.lineAt(document.lineCount - 1).text.length ) - void editor.edit((editBuilder) => { + await editor.edit((editBuilder) => { editBuilder.replace(fullRange, newCode) }) } @@ -268,6 +276,28 @@ function getEndOfEditPosition(originalCode: string, newCode: string): vscode.Pos 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 */ @@ -280,21 +310,90 @@ export async function displaySvgDecoration( session: CodeWhispererSession, languageClient: LanguageClient, item: InlineCompletionItemWithReferences, - addedCharacterCount: number, - deletedCharacterCount: number + inlineCompletionProvider?: AmazonQInlineCompletionItemProvider ) { const originalCode = editor.document.getText() - decorationManager.displayEditSuggestion( + // Set edit state immediately to prevent race condition with completion requests + await setContext('aws.amazonq.editSuggestionActive' as any, true) + EditSuggestionState.setEditSuggestionActive(true) + + // Check if a completion suggestion is currently active - if so, discard edit suggestion + if (inlineCompletionProvider && (await inlineCompletionProvider.isCompletionActive())) { + // Clean up state since we're not showing the edit + await setContext('aws.amazonq.editSuggestionActive' as any, false) + EditSuggestionState.setEditSuggestionActive(false) + + // Emit DISCARD telemetry for edit suggestion that can't be shown due to active completion + const params = createDiscardTelemetryParams(session, item) + languageClient.sendNotification('aws/logInlineCompletionSessionResults', params) + getLogger('nextEditPrediction').debug( + `Auto discarded edit suggestion for active completion suggestion: ${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) + 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) { + getLogger('nextEditPrediction').debug( + `Auto rejected edit suggestion for invalid patch: ${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) { + void vscode.commands.executeCommand('aws.amazonq.inline.rejectEdit') + } + }) + await decorationManager.displayEditSuggestion( editor, svgImage, startLine, - () => { + async () => { // Handle accept getLogger().info('Edit suggestion accepted') // Replace content - replaceEditorContent(editor, newCode) + 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) @@ -303,7 +402,9 @@ export async function displaySvgDecoration( // Move cursor to end of the actual changed content editor.selection = new vscode.Selection(endPosition, endPosition) - decorationManager.clearDecorations(editor) + await decorationManager.clearDecorations(editor) + documentChangeListener.dispose() + cursorChangeListener.dispose() const params: LogInlineCompletionSessionResultsParams = { sessionId: session.sessionId, completionSessionResult: { @@ -315,26 +416,53 @@ export async function displaySvgDecoration( }, totalSessionDisplayTime: Date.now() - session.requestStartTime, firstCompletionDisplayLatency: session.firstCompletionDisplayLatency, - addedCharacterCount: addedCharacterCount, - deletedCharacterCount: deletedCharacterCount, + isInlineEdit: true, } languageClient.sendNotification('aws/logInlineCompletionSessionResults', params) + session.triggerOnAcceptance = true + // VS Code triggers suggestion on every keystroke, temporarily disable trigger on acceptance + // if (inlineCompletionProvider && session.editsStreakPartialResultToken) { + // await inlineCompletionProvider.provideInlineCompletionItems( + // editor.document, + // endPosition, + // { + // triggerKind: vscode.InlineCompletionTriggerKind.Automatic, + // selectedCompletionInfo: undefined, + // }, + // new vscode.CancellationTokenSource().token, + // { emitTelemetry: false, showUi: false, editsStreakToken: session.editsStreakPartialResultToken } + // ) + // } }, - () => { + async (isDiscard: boolean) => { // Handle reject - getLogger().info('Edit suggestion rejected') - decorationManager.clearDecorations(editor) + 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]: { - seen: true, - accepted: false, - discarded: false, - }, + [item.itemId]: suggestionState, }, - // addedCharacterCount: addedCharacterCount, - // deletedCharacterCount: deletedCharacterCount, + totalSessionDisplayTime: Date.now() - session.requestStartTime, + firstCompletionDisplayLatency: session.firstCompletionDisplayLatency, + isInlineEdit: true, } languageClient.sendNotification('aws/logInlineCompletionSessionResults', params) }, diff --git a/packages/amazonq/src/app/inline/EditRendering/imageRenderer.ts b/packages/amazonq/src/app/inline/EditRendering/imageRenderer.ts index 2dd6bd67712..6c52dc2d6a0 100644 --- a/packages/amazonq/src/app/inline/EditRendering/imageRenderer.ts +++ b/packages/amazonq/src/app/inline/EditRendering/imageRenderer.ts @@ -10,12 +10,14 @@ 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 + languageClient: LanguageClient, + inlineCompletionProvider?: AmazonQInlineCompletionItemProvider ) { if (!editor) { return @@ -24,14 +26,16 @@ export async function showEdits( const svgGenerationService = new SvgGenerationService() // Generate your SVG image with the file contents const currentFile = editor.document.uri.fsPath - const { - svgImage, - startLine, - newCode, - origionalCodeHighlightRange, - addedCharacterCount, - deletedCharacterCount, - } = await svgGenerationService.generateDiffSvg(currentFile, item.insertText as string) + 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 @@ -40,12 +44,11 @@ export async function showEdits( svgImage, startLine, newCode, - origionalCodeHighlightRange, + originalCodeHighlightRange, session, languageClient, item, - addedCharacterCount, - deletedCharacterCount + inlineCompletionProvider ) } else { getLogger('nextEditPrediction').error('SVG image generation returned an empty result.') diff --git a/packages/amazonq/src/app/inline/EditRendering/stringUtils.ts b/packages/amazonq/src/app/inline/EditRendering/stringUtils.ts new file mode 100644 index 00000000000..b8c9a52d052 --- /dev/null +++ b/packages/amazonq/src/app/inline/EditRendering/stringUtils.ts @@ -0,0 +1,28 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Strips common indentation from each line of code that may contain HTML tags + * @param lines Array of code lines (may contain HTML tags) + * @returns Array of code lines with common indentation removed + */ +export function stripCommonIndentation(lines: string[]): string[] { + if (lines.length === 0) { + return lines + } + const removeFirstTag = (line: string) => line.replace(/^<[^>]*>/, '') + const getLeadingWhitespace = (text: string) => text.match(/^\s*/)?.[0] || '' + + // Find minimum indentation across all lines + const minIndentLength = Math.min(...lines.map((line) => getLeadingWhitespace(removeFirstTag(line)).length)) + + // Remove common indentation from each line + return lines.map((line) => { + const firstTagRemovedLine = removeFirstTag(line) + const leadingWhitespace = getLeadingWhitespace(firstTagRemovedLine) + const reducedWhitespace = leadingWhitespace.substring(minIndentLength) + return line.replace(leadingWhitespace, reducedWhitespace) + }) +} diff --git a/packages/amazonq/src/app/inline/EditRendering/svgGenerator.ts b/packages/amazonq/src/app/inline/EditRendering/svgGenerator.ts index 8c7a9d57fd9..59752a7b08a 100644 --- a/packages/amazonq/src/app/inline/EditRendering/svgGenerator.ts +++ b/packages/amazonq/src/app/inline/EditRendering/svgGenerator.ts @@ -3,15 +3,23 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { diffChars } from 'diff' +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 { applyUnifiedDiff } from './diffUtils' +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 { /** @@ -28,22 +36,15 @@ export class SvgGenerationService { svgImage: vscode.Uri startLine: number newCode: string - origionalCodeHighlightRange: Range[] - addedCharacterCount: number - deletedCharacterCount: number + originalCodeHighlightRange: Range[] }> { const textDoc = await vscode.workspace.openTextDocument(filePath) - const originalCode = textDoc.getText() + const originalCode = textDoc.getText().replaceAll('\r\n', '\n') if (originalCode === '') { logger.error(`udiff format error`) throw new ToolkitError('udiff format error') } - const { addedCharacterCount, deletedCharacterCount } = applyUnifiedDiff(originalCode, udiff) const newCode = await diffUtilities.getPatchedCode(filePath, udiff) - const modifiedLines = diffUtilities.getModifiedLinesFromUnifiedDiff(udiff) - // TODO remove - // eslint-disable-next-line aws-toolkits/no-json-stringify-in-log - logger.info(`Line mapping: ${JSON.stringify(modifiedLines)}`) const { createSVGWindow } = await import('svgdom') @@ -55,9 +56,30 @@ export class SvgGenerationService { const currentTheme = this.getEditorTheme() // Get edit diffs with highlight - const { addedLines, removedLines } = this.getEditedLinesFromDiff(udiff) + 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() @@ -65,19 +87,12 @@ export class SvgGenerationService { registerWindow(window, document) const draw = SVG(document.documentElement) as any - // Calculate dimensions based on code content - const { offset, editStartLine } = this.calculatePosition( - originalCode.split('\n'), - newCode.split('\n'), - addedLines, - currentTheme - ) 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(diffAddedWithHighlight, styles, offset) + const htmlContent = this.generateHtmlContent(normalizedDiffLines, styles, offset) // Create foreignObject to embed HTML const foreignObject = draw.foreignObject(width + offset, height) @@ -90,9 +105,7 @@ export class SvgGenerationService { svgImage: vscode.Uri.parse(svgResult), startLine: editStartLine, newCode: newCode, - origionalCodeHighlightRange: highlightRanges.removedRanges, - addedCharacterCount, - deletedCharacterCount, + originalCodeHighlightRange: highlightRanges.removedRanges, } } @@ -151,6 +164,9 @@ export class SvgGenerationService { white-space: pre-wrap; /* Preserve whitespace */ background-color: ${diffAdded}; } + .diff-unchanged { + white-space: pre-wrap; /* Preserve indentation for unchanged lines */ + } ` } @@ -167,43 +183,25 @@ export class SvgGenerationService { } /** - * Extract added and removed lines from the unified diff - * @param unifiedDiff The unified diff string + * 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 getEditedLinesFromDiff(unifiedDiff: string): { addedLines: string[]; removedLines: string[] } { + private getEditedLinesFromCode( + originalCode: string, + newCode: string + ): { addedLines: string[]; removedLines: string[] } { const addedLines: string[] = [] const removedLines: string[] = [] - const diffLines = unifiedDiff.split('\n') - - // Find all hunks in the diff - const hunkStarts = diffLines - .map((line, index) => (line.startsWith('@@ ') ? index : -1)) - .filter((index) => index !== -1) - // Process each hunk to find added and removed lines - for (const hunkStart of hunkStarts) { - const hunkHeader = diffLines[hunkStart] - const match = hunkHeader.match(/@@ -(\d+),(\d+) \+(\d+),(\d+) @@/) + const changes = diffLines(originalCode, newCode) - if (!match) { - continue - } - - // Extract the content lines for this hunk - let i = hunkStart + 1 - while (i < diffLines.length && !diffLines[i].startsWith('@@')) { - // Include lines that were added (start with '+') - if (diffLines[i].startsWith('+') && !diffLines[i].startsWith('+++')) { - const lineContent = diffLines[i].substring(1) - addedLines.push(lineContent) - } - // Include lines that were removed (start with '-') - else if (diffLines[i].startsWith('-') && !diffLines[i].startsWith('---')) { - const lineContent = diffLines[i].substring(1) - removedLines.push(lineContent) - } - i++ + 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)) } } @@ -236,7 +234,7 @@ export class SvgGenerationService { // If no ranges for this line, leave it as-is with HTML escaping if (lineRanges.length === 0) { - result.push(this.escapeHtml(line)) + result.push(`${this.escapeHtml(line)}`) continue } @@ -251,7 +249,7 @@ export class SvgGenerationService { // Add text before the current range (with HTML escaping) if (range.start > currentPos) { const beforeText = line.substring(currentPos, range.start) - highlightedLine += this.escapeHtml(beforeText) + highlightedLine += `${this.escapeHtml(beforeText)}` } // Add the highlighted part (with HTML escaping) @@ -265,7 +263,7 @@ export class SvgGenerationService { // Add any remaining text after the last range (with HTML escaping) if (currentPos < line.length) { const afterText = line.substring(currentPos) - highlightedLine += this.escapeHtml(afterText) + highlightedLine += `${this.escapeHtml(afterText)}` } result.push(highlightedLine) @@ -362,12 +360,23 @@ export class SvgGenerationService { newLines: string[], diffLines: string[], theme: editorThemeInfo - ): { offset: number; editStartLine: number } { + ): { 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 @@ -392,7 +401,7 @@ export class SvgGenerationService { const startLineLength = originalLines[startLine]?.length || 0 const offset = (maxLineLength - startLineLength) * theme.fontSize * 0.7 + 10 // padding - return { offset, editStartLine: editStartLineInOldFile } + return { offset, editStartLine: editStartLineInOldFile, isPositionValid: true } } private escapeHtml(text: string): string { @@ -419,45 +428,6 @@ export class SvgGenerationService { const originalRanges: Range[] = [] const afterRanges: Range[] = [] - /** - * Merges ranges on the same line that are separated by only one character - */ - const mergeAdjacentRanges = (ranges: Range[]): Range[] => { - const sortedRanges = [...ranges].sort((a, b) => { - if (a.line !== b.line) { - return a.line - b.line - } - return a.start - b.start - }) - - const result: Range[] = [] - - // Process all ranges - for (let i = 0; i < sortedRanges.length; i++) { - const current = sortedRanges[i] - - // If this is the last range or ranges are on different lines, add it directly - if (i === sortedRanges.length - 1 || current.line !== sortedRanges[i + 1].line) { - result.push(current) - continue - } - - // Check if current range and next range can be merged - const next = sortedRanges[i + 1] - if (current.line === next.line && next.start - current.end <= 1) { - sortedRanges[i + 1] = { - line: current.line, - start: current.start, - end: Math.max(current.end, next.end), - } - } else { - result.push(current) - } - } - - return result - } - // Create reverse mapping for quicker lookups const reverseMap = new Map() for (const [original, modified] of modifiedLines.entries()) { @@ -468,10 +438,14 @@ export class SvgGenerationService { for (let lineIndex = 0; lineIndex < originalCode.length; lineIndex++) { const line = originalCode[lineIndex] + /** + * If [line] is an empty line or only contains whitespace char, [diffWordsWithSpace] will say it's not an "remove", i.e. [part.removed] will be undefined, + * therefore the deletion will not be highlighted. Thus fallback this scenario to highlight the entire line + */ // If line exists in modifiedLines as a key, process character diffs - if (Array.from(modifiedLines.keys()).includes(line)) { + if (Array.from(modifiedLines.keys()).includes(line) && line.trim().length > 0) { const modifiedLine = modifiedLines.get(line)! - const changes = diffChars(line, modifiedLine) + const changes = diffWordsWithSpace(line, modifiedLine) let charPos = 0 for (const part of changes) { @@ -492,7 +466,7 @@ export class SvgGenerationService { originalRanges.push({ line: lineIndex, start: 0, - end: line.length, + end: line.length ?? defaultLineHighlightLength, }) } } @@ -503,7 +477,7 @@ export class SvgGenerationService { if (reverseMap.has(line)) { const originalLine = reverseMap.get(line)! - const changes = diffChars(originalLine, line) + const changes = diffWordsWithSpace(originalLine, line) let charPos = 0 for (const part of changes) { @@ -528,12 +502,9 @@ export class SvgGenerationService { } } - const mergedOriginalRanges = mergeAdjacentRanges(originalRanges) - const mergedAfterRanges = mergeAdjacentRanges(afterRanges) - return { - removedRanges: mergedOriginalRanges, - addedRanges: mergedAfterRanges, + removedRanges: originalRanges, + addedRanges: afterRanges, } } } diff --git a/packages/amazonq/src/app/inline/activation.ts b/packages/amazonq/src/app/inline/activation.ts index 867ae95d9b5..dbcbab5fd05 100644 --- a/packages/amazonq/src/app/inline/activation.ts +++ b/packages/amazonq/src/app/inline/activation.ts @@ -5,30 +5,76 @@ import vscode from 'vscode' import { + acceptSuggestion, AuthUtil, + CodeSuggestionsState, + CodeWhispererCodeCoverageTracker, CodeWhispererConstants, + CodeWhispererSettings, + ConfigurationEntry, + DefaultCodeWhispererClient, + invokeRecommendation, isInlineCompletionEnabled, + KeyStrokeHandler, + RecommendationHandler, runtimeLanguageContext, TelemetryHelper, UserWrittenCodeTracker, vsCodeState, } from 'aws-core-vscode/codewhisperer' -import { globals, sleep } from 'aws-core-vscode/shared' +import { Commands, getLogger, globals, sleep } from 'aws-core-vscode/shared' +import { LanguageClient } from 'vscode-languageclient' + +export async function activate(languageClient: LanguageClient) { + const codewhispererSettings = CodeWhispererSettings.instance + const client = new DefaultCodeWhispererClient() -export async function activate() { if (isInlineCompletionEnabled()) { // Debugging purpose: only initialize NextEditPredictionPanel when development // NextEditPredictionPanel.getInstance() await setSubscriptionsforInlineCompletion() await AuthUtil.instance.setVscodeContextProps() + RecommendationHandler.instance.setLanguageClient(languageClient) + } + + function getAutoTriggerStatus(): boolean { + return CodeSuggestionsState.instance.isSuggestionsEnabled() + } + + async function getConfigEntry(): Promise { + const isShowMethodsEnabled: boolean = + vscode.workspace.getConfiguration('editor').get('suggest.showMethods') || false + const isAutomatedTriggerEnabled: boolean = getAutoTriggerStatus() + const isManualTriggerEnabled: boolean = true + const isSuggestionsWithCodeReferencesEnabled = codewhispererSettings.isSuggestionsWithCodeReferencesEnabled() + + // TODO:remove isManualTriggerEnabled + return { + isShowMethodsEnabled, + isManualTriggerEnabled, + isAutomatedTriggerEnabled, + isSuggestionsWithCodeReferencesEnabled, + } } async function setSubscriptionsforInlineCompletion() { + RecommendationHandler.instance.subscribeSuggestionCommands() + /** * Automated trigger */ globals.context.subscriptions.push( + acceptSuggestion.register(globals.context), + vscode.window.onDidChangeActiveTextEditor(async (editor) => { + await RecommendationHandler.instance.onEditorChange() + }), + vscode.window.onDidChangeWindowState(async (e) => { + await RecommendationHandler.instance.onFocusChange() + }), + vscode.window.onDidChangeTextEditorSelection(async (e) => { + await RecommendationHandler.instance.onCursorChange(e) + }), vscode.workspace.onDidChangeTextDocument(async (e) => { const editor = vscode.window.activeTextEditor if (!editor) { @@ -41,6 +87,7 @@ export async function activate() { return } + CodeWhispererCodeCoverageTracker.getTracker(e.document.languageId)?.countTotalTokens(e) UserWrittenCodeTracker.instance.onTextDocumentChange(e) /** * Handle this keystroke event only when @@ -54,10 +101,10 @@ export async function activate() { if (vsCodeState.lastUserModificationTime) { TelemetryHelper.instance.setTimeSinceLastModification( - performance.now() - vsCodeState.lastUserModificationTime + Date.now() - vsCodeState.lastUserModificationTime ) } - vsCodeState.lastUserModificationTime = performance.now() + vsCodeState.lastUserModificationTime = Date.now() /** * Important: Doing this sleep(10) is to make sure * 1. this event is processed by vs code first @@ -65,6 +112,19 @@ export async function activate() { * Then this event can be processed by our code. */ await sleep(CodeWhispererConstants.vsCodeCursorUpdateDelay) + if (!RecommendationHandler.instance.isSuggestionVisible()) { + await KeyStrokeHandler.instance.processKeyStroke(e, editor, client, await getConfigEntry()) + } + }), + // manual trigger + Commands.register({ id: 'aws.amazonq.invokeInlineCompletion', autoconnect: true }, async () => { + invokeRecommendation( + vscode.window.activeTextEditor as vscode.TextEditor, + client, + await getConfigEntry() + ).catch((e: Error) => { + getLogger().error('invokeRecommendation failed: %s', (e as Error).message) + }) }) ) } diff --git a/packages/amazonq/src/app/inline/completion.ts b/packages/amazonq/src/app/inline/completion.ts index 069a6bc5128..bb53ed21386 100644 --- a/packages/amazonq/src/app/inline/completion.ts +++ b/packages/amazonq/src/app/inline/completion.ts @@ -2,7 +2,7 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ - +import * as vscode from 'vscode' import { CancellationToken, InlineCompletionContext, @@ -32,18 +32,20 @@ import { ImportAdderProvider, CodeSuggestionsState, vsCodeState, - inlineCompletionsDebounceDelay, noInlineSuggestionsMsg, - ReferenceInlineProvider, + getDiagnosticsDifferences, + getDiagnosticsOfCurrentFile, + toIdeDiagnostics, + handleExtraBrackets, } from 'aws-core-vscode/codewhisperer' -import { InlineGeneratingMessage } from './inlineGeneratingMessage' import { LineTracker } from './stateTracker/lineTracker' import { InlineTutorialAnnotation } from './tutorials/inlineTutorialAnnotation' import { TelemetryHelper } from './telemetryHelper' -import { Experiments, getLogger } from 'aws-core-vscode/shared' -import { debounce, messageUtils } from 'aws-core-vscode/utils' +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 @@ -52,9 +54,10 @@ export class InlineCompletionManager implements Disposable { private sessionManager: SessionManager private recommendationService: RecommendationService private lineTracker: LineTracker - private incomingGeneratingMessage: InlineGeneratingMessage + private inlineTutorialAnnotation: InlineTutorialAnnotation private readonly logSessionResultMessageName = 'aws/logInlineCompletionSessionResults' + private documentEventListener: DocumentEventListener constructor( languageClient: LanguageClient, @@ -66,24 +69,21 @@ export class InlineCompletionManager implements Disposable { this.languageClient = languageClient this.sessionManager = sessionManager this.lineTracker = lineTracker - this.incomingGeneratingMessage = new InlineGeneratingMessage(this.lineTracker) - this.recommendationService = new RecommendationService( - this.sessionManager, - this.incomingGeneratingMessage, - cursorUpdateRecorder - ) + this.recommendationService = new RecommendationService(this.sessionManager, cursorUpdateRecorder) this.inlineTutorialAnnotation = inlineTutorialAnnotation + this.documentEventListener = new DocumentEventListener() this.inlineCompletionProvider = new AmazonQInlineCompletionItemProvider( languageClient, this.recommendationService, this.sessionManager, - this.inlineTutorialAnnotation + this.inlineTutorialAnnotation, + this.documentEventListener ) + this.disposable = languages.registerInlineCompletionItemProvider( CodeWhispererConstants.platformLanguageIds, this.inlineCompletionProvider ) - this.lineTracker.ready() } @@ -94,9 +94,11 @@ export class InlineCompletionManager implements Disposable { public dispose(): void { if (this.disposable) { this.disposable.dispose() - this.incomingGeneratingMessage.dispose() this.lineTracker.dispose() } + if (this.documentEventListener) { + this.documentEventListener.dispose() + } } public registerInlineCompletion() { @@ -105,104 +107,185 @@ 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, - }, - }, - 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 + 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() ) - ReferenceLogViewProvider.instance.addReferenceLog(referenceLog) - ReferenceHoverProvider.instance.addCodeReferences(item.insertText as string, item.references) - - // Show codelense for 5 seconds. - ReferenceInlineProvider.instance.setInlineReference( - startLine, - item.insertText as string, - item.references + // 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: 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 ) - setTimeout(() => { - ReferenceInlineProvider.instance.removeInlineReference() - }, 5000) - } - 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 } - this.sessionManager.incrementSuggestionCount() - // clear session manager states once accepted - this.sessionManager.clear() } 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) - // clear session manager states once rejected - this.sessionManager.clear() } commands.registerCommand('aws.amazonq.rejectCodeSuggestion', onInlineRejection) } } export class AmazonQInlineCompletionItemProvider implements InlineCompletionItemProvider { - private logger = getLogger('nextEditPrediction') + private logger = getLogger() constructor( private readonly languageClient: LanguageClient, private readonly recommendationService: RecommendationService, private readonly sessionManager: SessionManager, - private readonly inlineTutorialAnnotation: InlineTutorialAnnotation + private readonly inlineTutorialAnnotation: InlineTutorialAnnotation, + private readonly documentEventListener: DocumentEventListener ) {} private readonly logSessionResultMessageName = 'aws/logInlineCompletionSessionResults' - provideInlineCompletionItems = debounce( - this._provideInlineCompletionItems.bind(this), - inlineCompletionsDebounceDelay, - true - ) - private async _provideInlineCompletionItems( + // 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, @@ -214,33 +297,53 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem position, context, triggerKind: context.triggerKind === InlineCompletionTriggerKind.Automatic ? 'Automatic' : 'Invoke', + options: JSON.stringify(getAllRecommendationsOptions), }) - // prevent concurrent API calls and write to shared state variables - if (vsCodeState.isRecommendationsActive) { - getLogger().info('Recommendations already active, returning empty') + 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 [] } - let logstr = `GenerateCompletion metadata:\\n` + + // yield event loop to let the document listen catch updates + await sleep(1) + + let logstr = `GenerateCompletion activity:\\n` try { - const t0 = performance.now() + const t0 = Date.now() vsCodeState.isRecommendationsActive = true - const isAutoTrigger = context.triggerKind === InlineCompletionTriggerKind.Automatic - if (isAutoTrigger && !CodeSuggestionsState.instance.isSuggestionsEnabled()) { - // return early when suggestions are disabled with auto trigger - return [] - } - // 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 - if (prevSession && prevSessionId && prevItemId && prevStartPosition) { + // 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 = { @@ -251,7 +354,7 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem item, editor, prevSession?.requestStartTime, - position.line, + position, prevSession?.firstCompletionDisplayLatency, ], } @@ -261,23 +364,30 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem } // re-use previous suggestions as long as new typed prefix matches if (prevItemMatchingPrefix.length > 0) { - getLogger().debug(`Re-using suggestions that match user typed characters`) + 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 } - getLogger().debug(`Auto rejecting suggestions from previous session`) - // if no such suggestions, report the previous suggestion as Reject + + // if no such suggestions, report the previous suggestion as Reject or Discarded const params: LogInlineCompletionSessionResultsParams = { sessionId: prevSessionId, completionSessionResult: { [prevItemId]: { - seen: true, + seen: prevSession.displayed, accepted: false, - discarded: 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 [] } // TODO: this line will take ~200ms each trigger, need to root cause and maybe better to disable it for now @@ -287,14 +397,19 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem TelemetryHelper.instance.setInvokeSuggestionStartTime() TelemetryHelper.instance.setTriggerType(context.triggerKind) - const t1 = performance.now() + const t1 = Date.now() await this.recommendationService.getAllRecommendations( this.languageClient, document, position, - context, + { + triggerKind: isAutoTrigger ? 1 : 0, + selectedCompletionInfo: context.selectedCompletionInfo, + }, token, + isAutoTrigger, + this.documentEventListener, getAllRecommendationsOptions ) // get active item from session for displaying @@ -304,13 +419,15 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem // eslint-disable-next-line @typescript-eslint/no-base-to-string const itemLog = items[0] ? `${items[0].insertText.toString()}` : `no suggestion` - const t2 = performance.now() + const t2 = Date.now() - logstr = logstr += `- number of suggestions: ${items.length} + logstr += `- number of suggestions: ${items.length} +- sessionId: ${this.sessionManager.getActiveSession()?.sessionId} - first suggestion content (next line): ${itemLog} -- duration since trigger to before sending Flare call: ${t1 - t0}ms -- duration since trigger to receiving responses from Flare: ${t2 - t0}ms +- 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() @@ -320,29 +437,44 @@ ${itemLog} } if (!session || !items.length || !editor) { - getLogger().debug( - `Failed to produce inline suggestion results. Received ${items.length} items from service` - ) + logstr += `Failed to produce inline suggestion results. Received ${items.length} items from service` return [] } const cursorPosition = document.validatePosition(position) - if (position.isAfter(editor.selection.active)) { - getLogger().debug(`Cursor moved behind trigger position. Discarding suggestion...`) - const params: LogInlineCompletionSessionResultsParams = { - sessionId: session.sessionId, - completionSessionResult: { - [itemId]: { - seen: false, - accepted: false, - discarded: true, + // 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 } - this.languageClient.sendNotification(this.logSessionResultMessageName, params) - this.sessionManager.clear() - return [] } // the user typed characters from invoking suggestion cursor position to receiving suggestion position @@ -353,12 +485,9 @@ ${itemLog} for (const item of items) { if (item.isInlineEdit) { // Check if Next Edit Prediction feature flag is enabled - if (Experiments.instance.isExperimentEnabled('amazonqLSPNEP')) { - void showEdits(item, editor, session, this.languageClient).then(() => { - const t3 = performance.now() - logstr = logstr + `- duration since trigger to NEP suggestion is displayed: ${t3 - t0}ms` - this.logger.info(logstr) - }) + 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 [] } @@ -373,21 +502,17 @@ ${itemLog} item, editor, session.requestStartTime, - cursorPosition.line, + cursorPosition, session.firstCompletionDisplayLatency, ], } item.range = new Range(cursorPosition, cursorPosition) itemsMatchingTypeahead.push(item) - ImportAdderProvider.instance.onShowRecommendation(document, cursorPosition.line, item) } } // report discard if none of suggestions match typeahead if (itemsMatchingTypeahead.length === 0) { - getLogger().debug( - `Suggestion does not match user typeahead from insertion position. Discarding suggestion...` - ) const params: LogInlineCompletionSessionResultsParams = { sessionId: session.sessionId, completionSessionResult: { @@ -400,16 +525,22 @@ ${itemLog} } 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) } } } 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/inlineGeneratingMessage.ts b/packages/amazonq/src/app/inline/inlineGeneratingMessage.ts deleted file mode 100644 index 6c2d97fdad2..00000000000 --- a/packages/amazonq/src/app/inline/inlineGeneratingMessage.ts +++ /dev/null @@ -1,98 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { editorUtilities } from 'aws-core-vscode/shared' -import * as vscode from 'vscode' -import { LineSelection, LineTracker } from './stateTracker/lineTracker' -import { AuthUtil } from 'aws-core-vscode/codewhisperer' -import { cancellableDebounce } from 'aws-core-vscode/utils' - -/** - * Manages the inline ghost text message show when Inline Suggestions is "thinking". - */ -export class InlineGeneratingMessage implements vscode.Disposable { - private readonly _disposable: vscode.Disposable - - private readonly cwLineHintDecoration: vscode.TextEditorDecorationType = - vscode.window.createTextEditorDecorationType({ - after: { - margin: '0 0 0 3em', - contentText: 'Amazon Q is generating...', - textDecoration: 'none', - fontWeight: 'normal', - fontStyle: 'normal', - color: 'var(--vscode-editorCodeLens-foreground)', - }, - rangeBehavior: vscode.DecorationRangeBehavior.OpenOpen, - isWholeLine: true, - }) - - constructor(private readonly lineTracker: LineTracker) { - this._disposable = vscode.Disposable.from( - AuthUtil.instance.auth.onDidChangeConnectionState(async (e) => { - if (e.state !== 'authenticating') { - this.hideGenerating() - } - }), - AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(async () => { - this.hideGenerating() - }) - ) - } - - dispose() { - this._disposable.dispose() - } - - readonly refreshDebounced = cancellableDebounce(async () => { - await this._refresh(true) - }, 1000) - - async showGenerating(triggerType: vscode.InlineCompletionTriggerKind) { - if (triggerType === vscode.InlineCompletionTriggerKind.Invoke) { - // 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(true) - } else { - await this.refreshDebounced.promise() - } - } - - async _refresh(shouldDisplay: boolean) { - const editor = vscode.window.activeTextEditor - if (!editor) { - return - } - - const selections = this.lineTracker.selections - if (!editor || !selections || !editorUtilities.isTextEditor(editor)) { - this.hideGenerating() - return - } - - if (!AuthUtil.instance.isConnectionValid()) { - this.hideGenerating() - return - } - - await this.updateDecorations(editor, selections, shouldDisplay) - } - - hideGenerating() { - vscode.window.activeTextEditor?.setDecorations(this.cwLineHintDecoration, []) - } - - async updateDecorations(editor: vscode.TextEditor, lines: LineSelection[], shouldDisplay: boolean) { - const range = editor.document.validateRange( - new vscode.Range(lines[0].active, lines[0].active, lines[0].active, lines[0].active) - ) - - if (shouldDisplay) { - editor.setDecorations(this.cwLineHintDecoration, [range]) - } else { - editor.setDecorations(this.cwLineHintDecoration, []) - } - } -} 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 1b121da9047..60fa8749cb0 100644 --- a/packages/amazonq/src/app/inline/recommendationService.ts +++ b/packages/amazonq/src/app/inline/recommendationService.ts @@ -2,33 +2,43 @@ * 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 { InlineGeneratingMessage } from './inlineGeneratingMessage' -import { CodeWhispererStatusBarManager } from 'aws-core-vscode/codewhisperer' +import { + AuthUtil, + CodeWhispererConstants, + CodeWhispererStatusBarManager, + vsCodeState, +} from 'aws-core-vscode/codewhisperer' import { TelemetryHelper } from './telemetryHelper' import { ICursorUpdateRecorder } from './cursorUpdateManager' -import { globals, getLogger } from 'aws-core-vscode/shared' +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 readonly inlineGeneratingMessage: InlineGeneratingMessage, private cursorUpdateRecorder?: ICursorUpdateRecorder ) {} - /** * Set the recommendation service */ @@ -36,25 +46,63 @@ export class RecommendationService { 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, + isAutoTrigger: boolean, + documentEventListener: DocumentEventListener, options: GetAllRecommendationsOptions = { emitTelemetry: true, showUi: true } ) { + const documentChangeEvent = documentEventListener?.getLastDocumentChangeEvent(document.uri.fsPath)?.event + // Record that a regular request is being made this.cursorUpdateRecorder?.recordCompletionRequest() - - const request: InlineCompletionWithReferencesParams = { + 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, } - const requestStartTime = globals.clock.Date.now() + 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 // Only track telemetry if enabled @@ -65,7 +113,6 @@ export class RecommendationService { try { // Show UI indicators only if UI is enabled if (options.showUi) { - await this.inlineGeneratingMessage.showGenerating(context.triggerKind) await statusBar.setLoading() } @@ -76,15 +123,60 @@ export class RecommendationService { textDocument: request.textDocument, position: request.position, context: request.context, + nextToken: request.partialResultToken, }, }) - let result: InlineCompletionListWithReferences = await languageClient.sendRequest( - inlineCompletionWithReferencesRequestType.method, + 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 ) - getLogger().info('Received inline completion response: %O', { + 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 + } + } + } + + getLogger().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, @@ -96,6 +188,42 @@ export class RecommendationService { })), }) + if (result.items.length > 0 && result.items[0].isInlineEdit === false) { + if (isTriggerByDeletion) { + 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) { + 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() + getLogger().info( + 'Completion discarded due to 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) + getLogger().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) { @@ -103,7 +231,7 @@ export class RecommendationService { } TelemetryHelper.instance.setFirstSuggestionShowTime() - const firstCompletionDisplayLatency = globals.clock.Date.now() - requestStartTime + const firstCompletionDisplayLatency = Date.now() - requestStartTime this.sessionManager.startSession( result.sessionId, result.items, @@ -112,34 +240,74 @@ export class RecommendationService { firstCompletionDisplayLatency ) - // If there are more results to fetch, handle them in the background - try { - while (result.partialResultToken) { - const paginatedRequest = { ...request, partialResultToken: result.partialResultToken } - result = await languageClient.sendRequest( - inlineCompletionWithReferencesRequestType.method, - paginatedRequest, - token + 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) { + if (!isInlineEdit) { + // If the suggestion is COMPLETIONS and there are more results to fetch, handle them in the background + getLogger().info( + 'Suggestion type is COMPLETIONS. Start fetching for more items if partialResultToken exists.' ) - this.sessionManager.updateSessionSuggestions(result.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. + getLogger().info('Suggestion type is EDITS. Skip fetching for more items.') + this.sessionManager.updateActiveEditsStreakToken(result.partialResultToken) } - } catch (error) { - languageClient.warn(`Error when getting suggestions: ${error}`) } - - // Close session and finalize telemetry regardless of pagination path - this.sessionManager.closeSession() - TelemetryHelper.instance.setAllPaginationEndTime() - options.emitTelemetry && TelemetryHelper.instance.tryRecordClientComponentLatency() - } catch (error) { + } catch (error: any) { getLogger().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) { - this.inlineGeneratingMessage.hideGenerating() void statusBar.refreshStatusBar() // effectively "stop loading" } } } + + private async processRemainingRequests( + languageClient: LanguageClient, + initialRequest: InlineCompletionWithReferencesParams, + firstResult: InlineCompletionListWithReferences, + token: CancellationToken + ): Promise { + let nextToken = firstResult.partialResultToken + while (nextToken) { + const request = { ...initialRequest, partialResultToken: nextToken } + + 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 7b3971ae2c1..ef2ee2a84d0 100644 --- a/packages/amazonq/src/app/inline/sessionManager.ts +++ b/packages/amazonq/src/app/inline/sessionManager.ts @@ -4,6 +4,12 @@ */ 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 export interface CodeWhispererSession { @@ -14,12 +20,21 @@ export interface CodeWhispererSession { 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 _acceptedSuggestionCount: number = 0 - + private _refreshedSessions = new Set() + private _currentSuggestionIndex = 0 constructor() {} public startSession( @@ -29,6 +44,7 @@ export class SessionManager { startPosition: vscode.Position, firstCompletionDisplayLatency?: number ) { + const diagnosticsBeforeAccept = getDiagnosticsOfCurrentFile() this.activeSession = { sessionId, suggestions, @@ -36,7 +52,11 @@ export class SessionManager { requestStartTime, startPosition, firstCompletionDisplayLatency, + diagnosticsBeforeAccept, + displayed: false, + lastVisibleTime: 0, } + this._currentSuggestionIndex = 0 } public closeSession() { @@ -69,7 +89,95 @@ export class SessionManager { this._acceptedSuggestionCount += 1 } + public updateActiveEditsStreakToken(partialResultToken: number | string) { + if (!this.activeSession) { + return + } + this.activeSession.editsStreakPartialResultToken = partialResultToken + } + public clear() { this.activeSession = undefined + this._currentSuggestionIndex = 0 + this.clearReferenceInlineHintsAndImportHints() + } + + // 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) + } + } + + public onNextSuggestion() { + if (this.activeSession?.suggestions && this.activeSession?.suggestions.length > 0) { + this._currentSuggestionIndex = (this._currentSuggestionIndex + 1) % this.activeSession.suggestions.length + this.updateCodeReferenceAndImports() + } + } + + 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() + } + } + + public checkInlineSuggestionVisibility() { + if (this.activeSession) { + this.activeSession.displayed = true + this.activeSession.lastVisibleTime = Date.now() + } + } + + 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/telemetryHelper.ts b/packages/amazonq/src/app/inline/telemetryHelper.ts index dffd267bee1..41db4c7469a 100644 --- a/packages/amazonq/src/app/inline/telemetryHelper.ts +++ b/packages/amazonq/src/app/inline/telemetryHelper.ts @@ -41,7 +41,7 @@ export class TelemetryHelper { public setInvokeSuggestionStartTime() { this.resetClientComponentLatencyTime() - this._invokeSuggestionStartTime = performance.now() + this._invokeSuggestionStartTime = Date.now() } get invokeSuggestionStartTime(): number { @@ -49,7 +49,7 @@ export class TelemetryHelper { } public setPreprocessEndTime() { - this._preprocessEndTime = performance.now() + this._preprocessEndTime = Date.now() } get preprocessEndTime(): number { @@ -58,7 +58,7 @@ export class TelemetryHelper { public setSdkApiCallStartTime() { if (this._sdkApiCallStartTime === 0) { - this._sdkApiCallStartTime = performance.now() + this._sdkApiCallStartTime = Date.now() } } @@ -68,7 +68,7 @@ export class TelemetryHelper { public setSdkApiCallEndTime() { if (this._sdkApiCallEndTime === 0 && this._sdkApiCallStartTime !== 0) { - this._sdkApiCallEndTime = performance.now() + this._sdkApiCallEndTime = Date.now() } } @@ -78,7 +78,7 @@ export class TelemetryHelper { public setAllPaginationEndTime() { if (this._allPaginationEndTime === 0 && this._sdkApiCallEndTime !== 0) { - this._allPaginationEndTime = performance.now() + this._allPaginationEndTime = Date.now() } } @@ -88,7 +88,7 @@ export class TelemetryHelper { public setFirstSuggestionShowTime() { if (this._firstSuggestionShowTime === 0 && this._sdkApiCallEndTime !== 0) { - this._firstSuggestionShowTime = performance.now() + this._firstSuggestionShowTime = Date.now() } } diff --git a/packages/amazonq/src/app/inline/tutorials/inlineTutorialAnnotation.ts b/packages/amazonq/src/app/inline/tutorials/inlineTutorialAnnotation.ts index bd12b1d28dd..ad0807df94c 100644 --- a/packages/amazonq/src/app/inline/tutorials/inlineTutorialAnnotation.ts +++ b/packages/amazonq/src/app/inline/tutorials/inlineTutorialAnnotation.ts @@ -5,13 +5,7 @@ import * as vscode from 'vscode' import * as os from 'os' -import { - AnnotationChangeSource, - AuthUtil, - inlinehintKey, - runtimeLanguageContext, - TelemetryHelper, -} from 'aws-core-vscode/codewhisperer' +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' @@ -296,28 +290,27 @@ export class InlineTutorialAnnotation implements vscode.Disposable { } async triggered(triggerType: vscode.InlineCompletionTriggerKind): Promise { - 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) + // 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 { diff --git a/packages/amazonq/src/extension.ts b/packages/amazonq/src/extension.ts index 9ca13136eab..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' @@ -126,16 +126,18 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is // 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', true)) { - 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/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 90a0adbc61f..1f443bed875 100644 --- a/packages/amazonq/src/lsp/chat/activation.ts +++ b/packages/amazonq/src/lsp/chat/activation.ts @@ -17,12 +17,16 @@ import { activate as registerLegacyChatListeners } from '../../app/chat/activati 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, languageClient) + // Set the chat view provider for AutoDebug to use + AutoDebugLspClient.setChatViewProvider(provider) + disposables.push( window.registerWebviewViewProvider(AmazonQChatViewProvider.viewType, provider, { webviewOptions: { 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 74b56a9bada..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, @@ -60,25 +61,36 @@ import { 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 { AmazonQChatViewProvider } from './webviewProvider' -import { AuthUtil, ReferenceLogViewProvider } from 'aws-core-vscode/codewhisperer' -import { amazonQDiffScheme, AmazonQPromptSettings, messages, openUrl, isTextEditor } 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 @@ -119,10 +131,13 @@ export function registerLanguageServerEventListener(languageClient: LanguageClie // This passes through metric data from LSP events to Toolkit telemetry with all fields from the LSP server languageClient.onTelemetry((e) => { const telemetryName: string = e.name - - if (telemetryName in telemetry) { - languageClient.info(`[VSCode 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}`) } }) } @@ -279,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, @@ -288,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}` @@ -309,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( @@ -340,7 +415,8 @@ export function registerMessageListeners( encryptionKey, provider, message.params.tabId, - quickActionDisposable + quickActionDisposable, + languageClient ) break } @@ -351,6 +427,41 @@ export function registerMessageListeners( 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: @@ -372,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 @@ -461,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> => { @@ -517,31 +651,53 @@ export function registerMessageListeners( ) languageClient.onNotification(openFileDiffNotificationType.method, async (params: OpenFileDiffParams) => { - const ecc = new EditorContentController() - const uri = params.originalFileUri - const doc = await vscode.workspace.openTextDocument(uri) - const entireDocumentSelection = new vscode.Selection( - new vscode.Position(0, 0), - new vscode.Position(doc.lineCount - 1, doc.lineAt(doc.lineCount - 1).text.length) - ) - const viewDiffMessage: ViewDiffMessage = { - context: { - activeFileContext: { - filePath: params.originalFileUri, - fileText: params.originalFileContent ?? '', - fileLanguage: undefined, - matchPolicy: undefined, - }, - focusAreaContext: { - selectionInsideExtendedCodeBlock: entireDocumentSelection, - codeBlock: '', - extendedCodeBlock: '', - names: undefined, - }, - }, - code: params.fileContent ?? '', + // Handle both file:// URIs and raw file paths, ensuring proper Windows path handling + let currentFileUri: vscode.Uri + + // Check if it's already a proper file:// URI + if (params.originalFileUri.startsWith('file://')) { + currentFileUri = vscode.Uri.parse(params.originalFileUri) + } else { + // Decode URL-encoded characters and treat as file path + const decodedPath = decodeURIComponent(params.originalFileUri) + currentFileUri = vscode.Uri.file(decodedPath) + } + + const originalContent = params.originalFileContent ?? '' + const fileName = path.basename(currentFileUri.fsPath) + + // Use custom scheme to avoid adding to recent files + const originalFileUri = vscode.Uri.parse(`amazonq-diff:${fileName}_original_${Date.now()}`) + + // Register content provider for the custom scheme + const disposable = vscode.workspace.registerTextDocumentContentProvider('amazonq-diff', { + provideTextDocumentContent: () => originalContent, + }) + + try { + // Open diff view with custom scheme URI (left) vs current file (right) + await vscode.commands.executeCommand( + 'vscode.diff', + originalFileUri, + currentFileUri, + `${vscode.workspace.asRelativePath(currentFileUri)} (Original ↔ Current, Editable)`, + { preview: false } + ) + + // Clean up content provider when diff view is closed + const cleanupDisposable = vscode.window.onDidChangeVisibleTextEditors(() => { + const isDiffViewOpen = vscode.window.visibleTextEditors.some( + (editor) => editor.document.uri.toString() === originalFileUri.toString() + ) + if (!isDiffViewOpen) { + disposable.dispose() + cleanupDisposable.dispose() + } + }) + } catch (error) { + disposable.dispose() + languageClient.error(`[VSCode Client] Failed to open diff view: ${error}`) } - await ecc.viewDiff(viewDiffMessage, amazonQDiffScheme) }) languageClient.onNotification(chatUpdateNotificationType.method, (params: ChatUpdateParams) => { @@ -563,6 +719,14 @@ function isServerEvent(command: string) { return command.startsWith('aws/chat/') || command === 'telemetry/event' } +function enterFocus(params: any) { + return params.name === 'enterFocus' +} + +function exitFocus(params: any) { + return params.name === 'exitFocus' +} + /** * Decodes partial chat responses from the language server before sending them to mynah UI */ @@ -574,6 +738,16 @@ async function handlePartialResult( ) { 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({ command: chatRequestType.method, @@ -594,10 +768,13 @@ async function handleCompleteResult( encryptionKey: Buffer | undefined, provider: AmazonQChatViewProvider, tabId: string, - disposable: Disposable + disposable: Disposable, + languageClient: LanguageClient ) { const decryptedMessage = await decryptResponse(result, encryptionKey) + await handleSecurityFindings(decryptedMessage, languageClient) + void provider.webview?.postMessage({ command: chatRequestType.method, params: decryptedMessage, @@ -611,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 7d51648398d..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, @@ -149,7 +150,7 @@ 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, modelSelectionEnabled: ${modelSelectionEnabled}}, 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) => { /** diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index 8fcfef0d397..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,17 +12,19 @@ import { CreateFilesParams, DeleteFilesParams, DidChangeWorkspaceFoldersParams, - DidSaveTextDocumentParams, GetConfigurationFromServerParams, RenameFilesParams, ResponseMessage, WorkspaceFolder, + ConnectionMetadata, } from '@aws/language-server-runtimes/protocol' import { AuthUtil, CodeWhispererSettings, + FeatureConfigProvider, getSelectedCustomization, TelemetryHelper, + vsCodeState, } from 'aws-core-vscode/codewhisperer' import { Settings, @@ -37,11 +39,14 @@ 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' @@ -50,6 +55,7 @@ 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') @@ -129,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 @@ -151,7 +166,7 @@ export async function startLanguageServer( initializationOptions: { aws: { clientInfo: { - name: env.appName, + name: getClientName(), version: version, extension: { name: 'AmazonQ-For-VSCode', @@ -163,25 +178,33 @@ export async function startLanguageServer( 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: { - inlineEditSupport: Experiments.instance.isExperimentEnabled('amazonqLSPNEP'), - }, + 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 }, }, /** @@ -207,6 +230,32 @@ 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(extensionContext, auth, client, resourcePaths, toDispose) @@ -220,6 +269,59 @@ 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, @@ -238,8 +340,42 @@ async function onLanguageServerReady( // tutorial for inline chat const inlineChatTutorialAnnotation = new InlineChatTutorialAnnotation(inlineTutorialAnnotation) - const inlineManager = new InlineCompletionManager(client, sessionManager, lineTracker, inlineTutorialAnnotation) - inlineManager.registerInlineCompletion() + const enableInlineRollback = FeatureConfigProvider.instance.getPreFlareRollbackGroup() === 'treatment' + if (enableInlineRollback) { + // use VSC inline + getLogger().info('Entering preflare logic') + await activateInline(client) + } else { + // use language server for inline completion + getLogger().info('Entering postflare logic') + const inlineManager = new InlineCompletionManager(client, sessionManager, lineTracker, inlineTutorialAnnotation) + inlineManager.registerInlineCompletion() + toDispose.push( + inlineManager, + Commands.register('aws.amazonq.showPrev', async () => { + await sessionManager.maybeRefreshSessionUx() + await vscode.commands.executeCommand('editor.action.inlineSuggest.showPrevious') + sessionManager.onPrevSuggestion() + }), + Commands.register('aws.amazonq.showNext', async () => { + await sessionManager.maybeRefreshSessionUx() + await vscode.commands.executeCommand('editor.action.inlineSuggest.showNext') + sessionManager.onNextSuggestion() + }), + // this is a workaround since handleDidShowCompletionItem is not public API + Commands.register('aws.amazonq.checkInlineSuggestionVisibility', async () => { + sessionManager.checkInlineSuggestionVisibility() + }), + Commands.register({ id: 'aws.amazonq.invokeInlineCompletion', autoconnect: true }, async () => { + vsCodeState.lastManualTriggerTime = performance.now() + await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger') + }), + vscode.workspace.onDidCloseTextDocument(async () => { + await vscode.commands.executeCommand('aws.amazonq.rejectCodeSuggestion') + }) + ) + } + activateInlineChat(extensionContext, client, encryptionKey, inlineChatTutorialAnnotation) if (Experiments.instance.get('amazonqChatLSP', true)) { @@ -251,20 +387,9 @@ 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( - inlineManager, - Commands.register({ id: 'aws.amazonq.invokeInlineCompletion', autoconnect: true }, async () => { - await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger') - }), Commands.register('aws.amazonq.refreshAnnotation', async (forceProceed: boolean) => { telemetry.record({ traceId: TelemetryHelper.instance.traceId, @@ -290,9 +415,6 @@ async function onLanguageServerReady( getLogger().debug(`codewhisperer: user dismiss tutorial.`) } }), - vscode.workspace.onDidCloseTextDocument(async () => { - await vscode.commands.executeCommand('aws.amazonq.rejectCodeSuggestion') - }), AuthUtil.instance.auth.onDidChangeActiveConnection(async () => { await auth.refreshConnection() }), @@ -330,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: { @@ -359,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, - }) - } } /** @@ -385,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 66edc9ff6f1..6b88eb98d21 100644 --- a/packages/amazonq/src/lsp/config.ts +++ b/packages/amazonq/src/lsp/config.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as vscode from 'vscode' -import { DevSettings, getServiceEnvVarConfig, BaseLspInstaller } from 'aws-core-vscode/shared' +import { DevSettings, getServiceEnvVarConfig, BaseLspInstaller, getLogger } from 'aws-core-vscode/shared' import { LanguageClient } from 'vscode-languageclient' import { DidChangeConfigurationNotification, @@ -68,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/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/inline/inline.test.ts b/packages/amazonq/test/e2e/inline/inline.test.ts index bcc41851eca..43a9f67ab73 100644 --- a/packages/amazonq/test/e2e/inline/inline.test.ts +++ b/packages/amazonq/test/e2e/inline/inline.test.ts @@ -5,10 +5,18 @@ import * as vscode from 'vscode' import assert from 'assert' -import { closeAllEditors, registerAuthHook, TestFolder, toTextEditor, using } from 'aws-core-vscode/test' +import { + closeAllEditors, + getTestWindow, + registerAuthHook, + resetCodeWhispererGlobalVariables, + TestFolder, + toTextEditor, + using, +} from 'aws-core-vscode/test' +import { RecommendationHandler, RecommendationService, session } from 'aws-core-vscode/codewhisperer' import { Commands, globals, sleep, waitUntil, collectionUtil } from 'aws-core-vscode/shared' import { loginToIdC } from '../amazonq/utils/setup' -import { vsCodeState } from 'aws-core-vscode/codewhisperer' describe('Amazon Q Inline', async function () { const retries = 3 @@ -32,6 +40,7 @@ describe('Amazon Q Inline', async function () { const folder = await TestFolder.create() tempFolder = folder.path await closeAllEditors() + await resetCodeWhispererGlobalVariables() }) afterEach(async function () { @@ -45,6 +54,7 @@ describe('Amazon Q Inline', async function () { const events = getUserTriggerDecision() console.table({ 'telemetry events': JSON.stringify(events), + 'recommendation service status': RecommendationService.instance.isRunning, }) } @@ -61,6 +71,31 @@ describe('Amazon Q Inline', async function () { }) } + async function waitForRecommendations() { + const suggestionShown = await waitUntil(async () => session.getSuggestionState(0) === 'Showed', waitOptions) + if (!suggestionShown) { + throw new Error(`Suggestion did not show. Suggestion States: ${JSON.stringify(session.suggestionStates)}`) + } + const suggestionVisible = await waitUntil( + async () => RecommendationHandler.instance.isSuggestionVisible(), + waitOptions + ) + if (!suggestionVisible) { + throw new Error( + `Suggestions failed to become visible. Suggestion States: ${JSON.stringify(session.suggestionStates)}` + ) + } + console.table({ + 'suggestions states': JSON.stringify(session.suggestionStates), + 'valid recommendation': RecommendationHandler.instance.isValidResponse(), + 'recommendation service status': RecommendationService.instance.isRunning, + recommendations: session.recommendations, + }) + if (!RecommendationHandler.instance.isValidResponse()) { + throw new Error('Did not find a valid response') + } + } + /** * Waits for a specific telemetry event to be emitted with the expected suggestion state. * It looks like there might be a potential race condition in codewhisperer causing telemetry @@ -114,9 +149,8 @@ describe('Amazon Q Inline', async function () { await invokeCompletion() originalEditorContents = vscode.window.activeTextEditor?.document.getText() - // wait until all the recommendations have finished - await waitUntil(() => Promise.resolve(vsCodeState.isRecommendationsActive === true), waitOptions) - await waitUntil(() => Promise.resolve(vsCodeState.isRecommendationsActive === false), waitOptions) + // wait until the ghost text appears + await waitForRecommendations() } beforeEach(async () => { @@ -129,12 +163,14 @@ describe('Amazon Q Inline', async function () { try { await setup() console.log(`test run ${attempt} succeeded`) + logUserDecisionStatus() break } catch (e) { console.log(`test run ${attempt} failed`) console.log(e) logUserDecisionStatus() attempt++ + await resetCodeWhispererGlobalVariables() } } if (attempt === retries) { @@ -180,6 +216,29 @@ describe('Amazon Q Inline', async function () { assert.deepStrictEqual(vscode.window.activeTextEditor?.document.getText(), originalEditorContents) }) }) + + it(`${name} invoke on unsupported filetype`, async function () { + await setupEditor({ + name: 'test.zig', + contents: `fn doSomething() void { + + }`, + }) + + /** + * Add delay between editor loading and invoking completion + * @see beforeEach in supported filetypes for more information + */ + await sleep(1000) + await invokeCompletion() + + if (name === 'automatic') { + // It should never get triggered since its not a supported file type + assert.deepStrictEqual(RecommendationService.instance.isRunning, false) + } else { + await getTestWindow().waitForMessage('currently not supported by Amazon Q inline suggestions') + } + }) }) } }) diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/EditRendering/stringUtils.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/EditRendering/stringUtils.test.ts new file mode 100644 index 00000000000..09c33fb0c80 --- /dev/null +++ b/packages/amazonq/test/unit/amazonq/apps/inline/EditRendering/stringUtils.test.ts @@ -0,0 +1,47 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import assert from 'assert' +import { stripCommonIndentation } from '../../../../../../src/app/inline/EditRendering/stringUtils' + +describe('stripCommonIndentation', () => { + it('should strip common leading whitespace', () => { + const input = [' line1 ', ' line2 ', ' line3 '] + const expected = ['line1 ', 'line2 ', ' line3 '] + assert.deepStrictEqual(stripCommonIndentation(input), expected) + }) + + it('should handle HTML tags', () => { + const input = [ + ' line2 ', + ] + const expected = ['line2 '] + assert.deepStrictEqual(stripCommonIndentation(input), expected) + }) + + it('should handle mixed indentation', () => { + const input = [' line1', ' line2', ' line3'] + const expected = ['line1', ' line2', ' line3'] + assert.deepStrictEqual(stripCommonIndentation(input), expected) + }) + + it('should handle empty lines', () => { + const input = [' line1', '', ' line2'] + const expected = [' line1', '', ' line2'] + assert.deepStrictEqual(stripCommonIndentation(input), expected) + }) + + it('should handle no indentation', () => { + const input = ['line1', 'line2'] + const expected = ['line1', 'line2'] + assert.deepStrictEqual(stripCommonIndentation(input), expected) + }) + + it('should handle single line', () => { + const input = [' single line'] + const expected = ['single line'] + assert.deepStrictEqual(stripCommonIndentation(input), expected) + }) +}) diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts index fbc28feefbb..417c8be1426 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts @@ -26,9 +26,9 @@ import { ReferenceLogViewProvider, vsCodeState, } from 'aws-core-vscode/codewhisperer' -import { InlineGeneratingMessage } from '../../../../../src/app/inline/inlineGeneratingMessage' 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 @@ -244,17 +244,19 @@ describe('InlineCompletionManager', () => { let getAllRecommendationsStub: sinon.SinonStub let recommendationService: RecommendationService let inlineTutorialAnnotation: InlineTutorialAnnotation + let documentEventListener: DocumentEventListener beforeEach(() => { const lineTracker = new LineTracker() - const activeStateController = new InlineGeneratingMessage(lineTracker) inlineTutorialAnnotation = new InlineTutorialAnnotation(lineTracker, mockSessionManager) - recommendationService = new RecommendationService(mockSessionManager, activeStateController) + recommendationService = new RecommendationService(mockSessionManager) + documentEventListener = new DocumentEventListener() vsCodeState.isRecommendationsActive = false mockSessionManager = { getActiveSession: getActiveSessionStub, getActiveRecommendation: getActiveRecommendationStub, clear: () => {}, + updateCodeReferenceAndImports: () => {}, } as unknown as SessionManager getActiveSessionStub.returns({ @@ -273,7 +275,8 @@ describe('InlineCompletionManager', () => { languageClient, recommendationService, mockSessionManager, - inlineTutorialAnnotation + inlineTutorialAnnotation, + documentEventListener ) const items = await provider.provideInlineCompletionItems( mockDocument, @@ -289,7 +292,8 @@ describe('InlineCompletionManager', () => { languageClient, recommendationService, mockSessionManager, - inlineTutorialAnnotation + inlineTutorialAnnotation, + documentEventListener ) await provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken) }), @@ -298,7 +302,8 @@ describe('InlineCompletionManager', () => { languageClient, recommendationService, mockSessionManager, - inlineTutorialAnnotation + inlineTutorialAnnotation, + documentEventListener ) getActiveRecommendationStub.returns([ { @@ -328,7 +333,8 @@ describe('InlineCompletionManager', () => { languageClient, recommendationService, mockSessionManager, - inlineTutorialAnnotation + inlineTutorialAnnotation, + documentEventListener ) const expectedText = `${mockSuggestions[1].insertText}this is my text` getActiveRecommendationStub.returns([ @@ -354,7 +360,8 @@ describe('InlineCompletionManager', () => { languageClient, recommendationService, mockSessionManager, - inlineTutorialAnnotation + inlineTutorialAnnotation, + documentEventListener ) getActiveRecommendationStub.returns([]) const messageShown = new Promise((resolve) => @@ -371,7 +378,7 @@ describe('InlineCompletionManager', () => { ) await messageShown }) - describe('debounce behavior', function () { + describe.skip('debounce behavior', function () { let clock: ReturnType beforeEach(function () { @@ -382,12 +389,13 @@ describe('InlineCompletionManager', () => { clock.uninstall() }) - it('should only trigger once on rapid events', async () => { + it.skip('should only trigger once on rapid events', async () => { provider = new AmazonQInlineCompletionItemProvider( languageClient, recommendationService, mockSessionManager, - inlineTutorialAnnotation + inlineTutorialAnnotation, + documentEventListener ) const p1 = provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken) const p2 = provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken) 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 c143020d74d..a051ef94abb 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/recommendationService.test.ts @@ -10,20 +10,21 @@ 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 { LineTracker } from '../../../../../src/app/inline/stateTracker/lineTracker' -import { InlineGeneratingMessage } from '../../../../../src/app/inline/inlineGeneratingMessage' // 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 lineTracker: LineTracker - let activeStateController: InlineGeneratingMessage let service: RecommendationService let cursorUpdateManager: CursorUpdateManager let statusBarStub: any @@ -32,6 +33,10 @@ describe('RecommendationService', () => { const mockPosition = { line: 0, character: 0 } as Position 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 @@ -69,8 +74,6 @@ describe('RecommendationService', () => { } as unknown as LanguageClient sessionManager = new SessionManager() - lineTracker = new LineTracker() - activeStateController = new InlineGeneratingMessage(lineTracker) // Create cursor update manager mock cursorUpdateManager = { @@ -94,7 +97,7 @@ describe('RecommendationService', () => { sandbox.stub(CodeWhispererStatusBarManager, 'instance').get(() => statusBarStub) // Create the service without cursor update recorder initially - service = new RecommendationService(sessionManager, activeStateController) + service = new RecommendationService(sessionManager) }) afterEach(() => { @@ -104,11 +107,7 @@ describe('RecommendationService', () => { describe('constructor', () => { it('should initialize with optional cursorUpdateRecorder', () => { - const serviceWithRecorder = new RecommendationService( - sessionManager, - activeStateController, - cursorUpdateManager - ) + const serviceWithRecorder = new RecommendationService(sessionManager, cursorUpdateManager) // Verify the service was created with the recorder assert.strictEqual(serviceWithRecorder['cursorUpdateRecorder'], cursorUpdateManager) @@ -130,6 +129,9 @@ describe('RecommendationService', () => { 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], @@ -138,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 @@ -157,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], @@ -172,19 +193,35 @@ 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, @@ -204,55 +241,55 @@ describe('RecommendationService', () => { sendRequestStub.resolves(mockFirstResult) - await service.getAllRecommendations(languageClient, mockDocument, mockPosition, mockContext, mockToken) + 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) }) - // Helper function to setup UI test - function setupUITest() { - const mockFirstResult = { - sessionId: 'test-session', - items: [mockInlineCompletionItemOne], - partialResultToken: undefined, - } - - sendRequestStub.resolves(mockFirstResult) - - // Spy on the UI methods - const showGeneratingStub = sandbox.stub(activeStateController, 'showGenerating').resolves() - const hideGeneratingStub = sandbox.stub(activeStateController, 'hideGenerating') - - return { showGeneratingStub, hideGeneratingStub } - } - it('should not show UI indicators when showUi option is false', async () => { - const { showGeneratingStub, hideGeneratingStub } = setupUITest() - // Call with showUi: false option - await service.getAllRecommendations(languageClient, mockDocument, mockPosition, mockContext, mockToken, { - showUi: false, - emitTelemetry: true, - }) + await service.getAllRecommendations( + languageClient, + mockDocument, + mockPosition, + mockContext, + mockToken, + true, + mockDocumentEventListener, + { + showUi: false, + emitTelemetry: true, + } + ) // Verify UI methods were not called - sinon.assert.notCalled(showGeneratingStub) - sinon.assert.notCalled(hideGeneratingStub) sinon.assert.notCalled(statusBarStub.setLoading) sinon.assert.notCalled(statusBarStub.refreshStatusBar) }) it('should show UI indicators when showUi option is true (default)', async () => { - const { showGeneratingStub, hideGeneratingStub } = setupUITest() - // Call with default options (showUi: true) - await service.getAllRecommendations(languageClient, mockDocument, mockPosition, mockContext, mockToken) + await service.getAllRecommendations( + languageClient, + mockDocument, + mockPosition, + mockContext, + mockToken, + true, + mockDocumentEventListener + ) // Verify UI methods were called - sinon.assert.calledOnce(showGeneratingStub) - sinon.assert.calledOnce(hideGeneratingStub) sinon.assert.calledOnce(statusBarStub.setLoading) sinon.assert.calledOnce(statusBarStub.refreshStatusBar) }) @@ -268,10 +305,6 @@ describe('RecommendationService', () => { // Set up UI options const options = { showUi: true } - // Stub the UI methods to avoid errors - // const showGeneratingStub = sandbox.stub(activeStateController, 'showGenerating').resolves() - const hideGeneratingStub = sandbox.stub(activeStateController, 'hideGenerating') - // Temporarily replace console.error with a no-op function to prevent test failure const originalConsoleError = console.error console.error = () => {} @@ -284,6 +317,8 @@ describe('RecommendationService', () => { mockPosition, mockContext, mockToken, + true, + mockDocumentEventListener, options ) @@ -291,12 +326,75 @@ describe('RecommendationService', () => { assert.deepStrictEqual(result, []) // Verify the UI indicators were hidden even when an error occurs - sinon.assert.calledOnce(hideGeneratingStub) 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 69b15d6e311..c31e873e181 100644 --- a/packages/amazonq/test/unit/amazonq/lsp/config.test.ts +++ b/packages/amazonq/test/unit/amazonq/lsp/config.test.ts @@ -77,3 +77,151 @@ describe('getAmazonQLspConfig', () => { 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('should send customization configuration with logging', async () => { + const config = { + type: 'customization' as const, + customization: 'test-customization-arn', + } + + await pushConfigUpdate(mockClient, config) + + // 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/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 index 512092d53d3..dee096d7f57 100644 --- a/packages/amazonq/test/unit/app/inline/EditRendering/diffUtils.test.ts +++ b/packages/amazonq/test/unit/app/inline/EditRendering/diffUtils.test.ts @@ -4,7 +4,7 @@ */ import * as assert from 'assert' -import { applyUnifiedDiff, getAddedAndDeletedCharCount } from '../../../../../src/app/inline/EditRendering/diffUtils' +import { applyUnifiedDiff } from '../../../../../src/app/inline/EditRendering/diffUtils' describe('diffUtils', function () { describe('applyUnifiedDiff', function () { @@ -27,35 +27,13 @@ describe('diffUtils', function () { 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) + const appliedCode = applyUnifiedDiff(originalCode, unifiedDiff) // Verify the result assert.strictEqual(appliedCode, expectedResult) }) }) - describe('getAddedAndDeletedCharCount', function () { - it('should correctly calculate added and deleted character counts', function () { - // Unified diff with additions and deletions - 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' + - ' }' - - // Calculate character counts - const { addedCharacterCount, deletedCharacterCount } = getAddedAndDeletedCharCount(unifiedDiff) - - // Verify the counts with the actual values from the implementation - assert.strictEqual(addedCharacterCount, 20) - assert.strictEqual(deletedCharacterCount, 15) - }) - }) - describe('applyUnifiedDiff with complex changes', function () { it('should handle multiple hunks in a diff', function () { // Original code with multiple functions @@ -96,7 +74,7 @@ describe('diffUtils', function () { '}' // Apply the diff - const { appliedCode } = applyUnifiedDiff(originalCode, unifiedDiff) + 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 index df4fac09c28..0a8cde5bacf 100644 --- a/packages/amazonq/test/unit/app/inline/EditRendering/displayImage.test.ts +++ b/packages/amazonq/test/unit/app/inline/EditRendering/displayImage.test.ts @@ -6,7 +6,30 @@ import * as vscode from 'vscode' import * as sinon from 'sinon' import assert from 'assert' -import { EditDecorationManager } from '../../../../../src/app/inline/EditRendering/displayImage' +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 @@ -25,23 +48,13 @@ describe('EditDecorationManager', function () { dispose: sandbox.stub(), } as unknown as sinon.SinonStubbedInstance - documentStub = { - getText: sandbox.stub().returns('Original code content'), - lineCount: 5, - 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 - - editorStub = { - document: documentStub, - setDecorations: sandbox.stub(), - edit: sandbox.stub().resolves(true), - } 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) @@ -57,7 +70,7 @@ describe('EditDecorationManager', function () { sandbox.restore() }) - it('should display SVG decorations in the editor', function () { + 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') @@ -69,7 +82,7 @@ describe('EditDecorationManager', function () { editorStub.setDecorations.reset() // Call displayEditSuggestion - manager.displayEditSuggestion( + await manager.displayEditSuggestion( editorStub as unknown as vscode.TextEditor, svgUri, 0, @@ -94,7 +107,7 @@ describe('EditDecorationManager', function () { }) // Helper function to setup edit suggestion test - function setupEditSuggestionTest() { + async function setupEditSuggestionTest() { // Create a fake SVG image URI const svgUri = vscode.Uri.parse('file:///path/to/image.svg') @@ -103,7 +116,7 @@ describe('EditDecorationManager', function () { const rejectHandler = sandbox.stub() // Display the edit suggestion - manager.displayEditSuggestion( + await manager.displayEditSuggestion( editorStub as unknown as vscode.TextEditor, svgUri, 0, @@ -117,8 +130,8 @@ describe('EditDecorationManager', function () { return { acceptHandler, rejectHandler } } - it('should trigger accept handler when command is executed', function () { - const { acceptHandler, rejectHandler } = setupEditSuggestionTest() + 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( @@ -138,8 +151,8 @@ describe('EditDecorationManager', function () { } }) - it('should trigger reject handler when command is executed', function () { - const { acceptHandler, rejectHandler } = setupEditSuggestionTest() + 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( @@ -159,12 +172,12 @@ describe('EditDecorationManager', function () { } }) - it('should clear decorations when requested', function () { + it('should clear decorations when requested', async function () { // Reset the setDecorations stub to clear any previous calls editorStub.setDecorations.reset() // Call clearDecorations - manager.clearDecorations(editorStub as unknown as vscode.TextEditor) + await manager.clearDecorations(editorStub as unknown as vscode.TextEditor) // Verify decorations were cleared assert.strictEqual(editorStub.setDecorations.callCount, 2) @@ -174,3 +187,124 @@ describe('EditDecorationManager', function () { sinon.assert.calledWith(editorStub.setDecorations.secondCall, manager['removedCodeDecorationType'], []) }) }) + +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 () { + 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 () { + 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 () { + 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 () { + 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 () { + 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 () { + 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 index 2a3db2af650..e1c32778d83 100644 --- a/packages/amazonq/test/unit/app/inline/EditRendering/imageRenderer.test.ts +++ b/packages/amazonq/test/unit/app/inline/EditRendering/imageRenderer.test.ts @@ -30,9 +30,7 @@ describe('showEdits', function () { svgImage: vscode.Uri.file('/path/to/generated.svg'), startLine: 5, newCode: 'console.log("Hello World");', - origionalCodeHighlightRange: [{ line: 5, start: 0, end: 10 }], - addedCharacterCount: 25, - deletedCharacterCount: 0, + originalCodeHighlightRange: [{ line: 5, start: 0, end: 10 }], ...overrides, } } @@ -54,6 +52,7 @@ describe('showEdits', function () { 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) @@ -74,6 +73,7 @@ describe('showEdits', function () { } 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 @@ -167,12 +167,10 @@ describe('showEdits', function () { mockSvgResult.svgImage, mockSvgResult.startLine, mockSvgResult.newCode, - mockSvgResult.origionalCodeHighlightRange, + mockSvgResult.originalCodeHighlightRange, sessionStub, languageClientStub, - itemStub, - mockSvgResult.addedCharacterCount, - mockSvgResult.deletedCharacterCount + itemStub ) // Verify no errors were logged diff --git a/packages/amazonq/test/unit/app/inline/EditRendering/svgGenerator.test.ts b/packages/amazonq/test/unit/app/inline/EditRendering/svgGenerator.test.ts index 81ba05251e2..657ff5c2915 100644 --- a/packages/amazonq/test/unit/app/inline/EditRendering/svgGenerator.test.ts +++ b/packages/amazonq/test/unit/app/inline/EditRendering/svgGenerator.test.ts @@ -150,7 +150,7 @@ describe('SvgGenerationService', function () { }) describe('highlight ranges', function () { - it('should generate highlight ranges for character-level changes', 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;']]) @@ -174,32 +174,6 @@ describe('SvgGenerationService', function () { assert.ok(addedRange.end > addedRange.start) }) - it('should merge adjacent highlight ranges', 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) - - // Adjacent ranges should be merged - const sortedRanges = [...result.addedRanges].sort((a, b) => { - if (a.line !== b.line) { - return a.line - b.line - } - return a.start - b.start - }) - - // Check that no adjacent ranges exist - for (let i = 0; i < sortedRanges.length - 1; i++) { - const current = sortedRanges[i] - const next = sortedRanges[i + 1] - if (current.line === next.line) { - assert.ok(next.start - current.end > 1, 'Adjacent ranges should be merged') - } - } - }) - it('should handle HTML escaping in highlight edits', function () { const newLines = ['function test() {', ' return "";', '}'] const highlightRanges = [{ line: 1, start: 10, end: 35 }] diff --git a/packages/amazonq/test/unit/app/inline/completion.test.ts b/packages/amazonq/test/unit/app/inline/completion.test.ts new file mode 100644 index 00000000000..5c8673a0276 --- /dev/null +++ b/packages/amazonq/test/unit/app/inline/completion.test.ts @@ -0,0 +1,210 @@ +/*! + * 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 { AmazonQInlineCompletionItemProvider } from '../../../../src/app/inline/completion' + +describe('AmazonQInlineCompletionItemProvider', function () { + let provider: AmazonQInlineCompletionItemProvider + let mockLanguageClient: any + let sandbox: sinon.SinonSandbox + + beforeEach(function () { + sandbox = sinon.createSandbox() + mockLanguageClient = { + sendNotification: sandbox.stub(), + } + + // Create provider with minimal mocks + provider = new AmazonQInlineCompletionItemProvider( + mockLanguageClient, + {} as any, // recommendationService + {} as any, // sessionManager + {} as any, // inlineTutorialAnnotation + {} as any // documentEventListener + ) + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('batchDiscardTelemetryForEditSuggestion', function () { + it('should batch multiple completion items into single telemetry event', function () { + const items = [ + { itemId: 'item1', isInlineEdit: false }, + { itemId: 'item2', isInlineEdit: false }, + { itemId: 'item3', isInlineEdit: false }, + ] + + const session = { + sessionId: 'test-session', + firstCompletionDisplayLatency: 100, + requestStartTime: Date.now() - 1000, + } + + provider.batchDiscardTelemetryForEditSuggestion(items, session) + + // Verify single telemetry notification was sent + assert.strictEqual(mockLanguageClient.sendNotification.callCount, 1) + + // Verify the notification contains all items + const call = mockLanguageClient.sendNotification.getCall(0) + const params = call.args[1] + + assert.strictEqual(params.sessionId, 'test-session') + assert.strictEqual(Object.keys(params.completionSessionResult).length, 3) + assert.deepStrictEqual(params.completionSessionResult.item1, { + seen: false, + accepted: false, + discarded: true, + }) + assert.deepStrictEqual(params.completionSessionResult.item2, { + seen: false, + accepted: false, + discarded: true, + }) + assert.deepStrictEqual(params.completionSessionResult.item3, { + seen: false, + accepted: false, + discarded: true, + }) + }) + + it('should filter out inline edit items', function () { + const items = [ + { itemId: 'item1', isInlineEdit: false }, + { itemId: 'item2', isInlineEdit: true }, // Should be filtered out + { itemId: 'item3', isInlineEdit: false }, + ] + + const session = { + sessionId: 'test-session', + firstCompletionDisplayLatency: 100, + requestStartTime: Date.now() - 1000, + } + + provider.batchDiscardTelemetryForEditSuggestion(items, session) + + const call = mockLanguageClient.sendNotification.getCall(0) + const params = call.args[1] + + // Should only include 2 items (item2 filtered out) + assert.strictEqual(Object.keys(params.completionSessionResult).length, 2) + assert.ok(params.completionSessionResult.item1) + assert.ok(params.completionSessionResult.item3) + assert.ok(!params.completionSessionResult.item2) + }) + + it('should not send notification when no valid items', function () { + const items = [ + { itemId: 'item1', isInlineEdit: true }, // Filtered out + { itemId: undefined, isInlineEdit: false }, // No itemId + ] + + const session = { + sessionId: 'test-session', + firstCompletionDisplayLatency: 100, + requestStartTime: Date.now() - 1000, + } + + provider.batchDiscardTelemetryForEditSuggestion(items, session) + + // No notification should be sent + assert.strictEqual(mockLanguageClient.sendNotification.callCount, 0) + }) + }) + + describe('isCompletionActive', function () { + let mockSessionManager: any + let mockVscodeCommands: any + + beforeEach(function () { + mockSessionManager = { + getActiveSession: sandbox.stub(), + } + + // Mock vscode.commands.executeCommand + mockVscodeCommands = sandbox.stub(require('vscode').commands, 'executeCommand') + + // Create provider with mocked session manager + provider = new AmazonQInlineCompletionItemProvider( + mockLanguageClient, + {} as any, // recommendationService + mockSessionManager, + {} as any, // inlineTutorialAnnotation + {} as any // documentEventListener + ) + }) + + it('should return false when no active session', async function () { + mockSessionManager.getActiveSession.returns(undefined) + + const result = await provider.isCompletionActive() + + assert.strictEqual(result, false) + assert.strictEqual(mockVscodeCommands.callCount, 0) + }) + + it('should return false when session not displayed', async function () { + mockSessionManager.getActiveSession.returns({ + displayed: false, + suggestions: [{ isInlineEdit: false }], + lastVisibleTime: 0, + }) + + const result = await provider.isCompletionActive() + + assert.strictEqual(result, false) + assert.strictEqual(mockVscodeCommands.callCount, 0) + }) + + it('should return false when session has inline edit suggestions', async function () { + mockSessionManager.getActiveSession.returns({ + displayed: true, + suggestions: [{ isInlineEdit: true }], + lastVisibleTime: Date.now(), + }) + + const result = await provider.isCompletionActive() + + assert.strictEqual(result, false) + assert.strictEqual(mockVscodeCommands.callCount, 0) + }) + + it('should return true when VS Code command executes successfully', async function () { + const currentTime = Date.now() + mockSessionManager.getActiveSession.returns({ + displayed: true, + suggestions: [{ isInlineEdit: false }], + lastVisibleTime: currentTime, // Recent timestamp + }) + mockVscodeCommands.resolves() + + const result = await provider.isCompletionActive() + + assert.strictEqual(result, true) + assert.strictEqual(mockVscodeCommands.callCount, 1) + assert.strictEqual(mockVscodeCommands.getCall(0).args[0], 'aws.amazonq.checkInlineSuggestionVisibility') + }) + + it('should return false when VS Code command fails', async function () { + const oldTime = Date.now() - 100 // Old timestamp (>50ms ago) + mockSessionManager.getActiveSession.returns({ + displayed: true, + suggestions: [{ isInlineEdit: false }], + lastVisibleTime: oldTime, + }) + mockVscodeCommands.resolves() // Command doesn't fail, but timestamp is old + + const result = await provider.isCompletionActive() + + assert.strictEqual(result, false) + assert.strictEqual(mockVscodeCommands.callCount, 1) + assert.strictEqual(mockVscodeCommands.getCall(0).args[0], 'aws.amazonq.checkInlineSuggestionVisibility') + }) + }) +}) diff --git a/packages/amazonq/test/unit/app/inline/notebookUtil.test.ts b/packages/amazonq/test/unit/app/inline/notebookUtil.test.ts new file mode 100644 index 00000000000..697c88ef6ec --- /dev/null +++ b/packages/amazonq/test/unit/app/inline/notebookUtil.test.ts @@ -0,0 +1,87 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as assert from 'assert' +import { createMockDocument } from 'aws-core-vscode/test' +import { convertCellContent, getNotebookContext } from '../../../../src/app/inline/notebookUtil' +import { CodeWhispererConstants } from 'aws-core-vscode/codewhisperer' + +export function createNotebookCell( + document: vscode.TextDocument = createMockDocument('def example():\n return "test"'), + kind: vscode.NotebookCellKind = vscode.NotebookCellKind.Code, + notebook: vscode.NotebookDocument = {} as any, + index: number = 0, + outputs: vscode.NotebookCellOutput[] = [], + metadata: { readonly [key: string]: any } = {}, + executionSummary?: vscode.NotebookCellExecutionSummary +): vscode.NotebookCell { + return { + document, + kind, + notebook, + index, + outputs, + metadata, + executionSummary, + } +} + +describe('Notebook Util', function () { + describe('convertCellContent', function () { + it('should return code cell content as-is', function () { + const codeCell = createNotebookCell( + createMockDocument('def example():\n return "test"'), + vscode.NotebookCellKind.Code + ) + const result = convertCellContent(codeCell) + assert.strictEqual(result, 'def example():\n return "test"') + }) + + it('should convert markdown cell content to comments for Python', function () { + const markdownCell = createNotebookCell( + createMockDocument('# Heading\nSome text'), + vscode.NotebookCellKind.Markup + ) + const result = convertCellContent(markdownCell) + assert.strictEqual(result, '# # Heading\n# Some text') + }) + }) + + describe('getNotebookContext', function () { + it('should combine context from multiple cells', function () { + const currentDoc = createMockDocument('cell2 content', 'b.ipynb') + const notebook = { + getCells: () => [ + createNotebookCell(createMockDocument('cell1 content', 'a.ipynb'), vscode.NotebookCellKind.Code), + createNotebookCell(currentDoc, vscode.NotebookCellKind.Code), + createNotebookCell(createMockDocument('cell3 content', 'c.ipynb'), vscode.NotebookCellKind.Code), + ], + } as vscode.NotebookDocument + + const position = new vscode.Position(0, 5) + + const { caretLeftFileContext, caretRightFileContext } = getNotebookContext(notebook, currentDoc, position) + + assert.strictEqual(caretLeftFileContext, 'cell1 content\ncell2') + assert.strictEqual(caretRightFileContext, ' content\ncell3 content') + }) + + it('should respect character limits', function () { + const longContent = 'a'.repeat(10000) + const notebook = { + getCells: () => [createNotebookCell(createMockDocument(longContent), vscode.NotebookCellKind.Code)], + } as vscode.NotebookDocument + + const currentDoc = createMockDocument(longContent) + const position = new vscode.Position(0, 5000) + + const { caretLeftFileContext, caretRightFileContext } = getNotebookContext(notebook, currentDoc, position) + + assert.ok(caretLeftFileContext.length <= CodeWhispererConstants.charactersLimit) + assert.ok(caretRightFileContext.length <= CodeWhispererConstants.charactersLimit) + }) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/commands/invokeRecommendation.test.ts b/packages/amazonq/test/unit/codewhisperer/commands/invokeRecommendation.test.ts new file mode 100644 index 00000000000..68cebe37bb1 --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/commands/invokeRecommendation.test.ts @@ -0,0 +1,43 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import { resetCodeWhispererGlobalVariables, createMockTextEditor } from 'aws-core-vscode/test' +import { + ConfigurationEntry, + invokeRecommendation, + InlineCompletionService, + isInlineCompletionEnabled, + DefaultCodeWhispererClient, +} from 'aws-core-vscode/codewhisperer' + +describe('invokeRecommendation', function () { + describe('invokeRecommendation', function () { + let getRecommendationStub: sinon.SinonStub + let mockClient: DefaultCodeWhispererClient + + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + getRecommendationStub = sinon.stub(InlineCompletionService.instance, 'getPaginatedRecommendation') + }) + + afterEach(function () { + sinon.restore() + }) + + it('Should call getPaginatedRecommendation with OnDemand as trigger type when inline completion is enabled', async function () { + const mockEditor = createMockTextEditor() + const config: ConfigurationEntry = { + isShowMethodsEnabled: true, + isManualTriggerEnabled: true, + isAutomatedTriggerEnabled: true, + isSuggestionsWithCodeReferencesEnabled: true, + } + await invokeRecommendation(mockEditor, mockClient, config) + assert.strictEqual(getRecommendationStub.called, isInlineCompletionEnabled()) + }) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/commands/onAcceptance.test.ts b/packages/amazonq/test/unit/codewhisperer/commands/onAcceptance.test.ts new file mode 100644 index 00000000000..0471aaa3601 --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/commands/onAcceptance.test.ts @@ -0,0 +1,64 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as vscode from 'vscode' +import * as sinon from 'sinon' +import { onAcceptance, AcceptedSuggestionEntry, session, CodeWhispererTracker } from 'aws-core-vscode/codewhisperer' +import { resetCodeWhispererGlobalVariables, createMockTextEditor } from 'aws-core-vscode/test' + +describe('onAcceptance', function () { + describe('onAcceptance', function () { + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + session.reset() + }) + + afterEach(function () { + sinon.restore() + session.reset() + }) + + it('Should enqueue an event object to tracker', async function () { + const mockEditor = createMockTextEditor() + const trackerSpy = sinon.spy(CodeWhispererTracker.prototype, 'enqueue') + const fakeReferences = [ + { + message: '', + licenseName: 'MIT', + repository: 'http://github.com/fake', + recommendationContentSpan: { + start: 0, + end: 10, + }, + }, + ] + await onAcceptance({ + editor: mockEditor, + range: new vscode.Range(new vscode.Position(1, 0), new vscode.Position(1, 26)), + effectiveRange: new vscode.Range(new vscode.Position(1, 0), new vscode.Position(1, 26)), + acceptIndex: 0, + recommendation: "print('Hello World!')", + requestId: '', + sessionId: '', + triggerType: 'OnDemand', + completionType: 'Line', + language: 'python', + references: fakeReferences, + }) + const actualArg = trackerSpy.getCall(0).args[0] as AcceptedSuggestionEntry + assert.ok(trackerSpy.calledOnce) + assert.strictEqual(actualArg.originalString, 'def two_sum(nums, target):') + assert.strictEqual(actualArg.requestId, '') + assert.strictEqual(actualArg.sessionId, '') + assert.strictEqual(actualArg.triggerType, 'OnDemand') + assert.strictEqual(actualArg.completionType, 'Line') + assert.strictEqual(actualArg.language, 'python') + assert.deepStrictEqual(actualArg.startPosition, new vscode.Position(1, 0)) + assert.deepStrictEqual(actualArg.endPosition, new vscode.Position(1, 26)) + assert.strictEqual(actualArg.index, 0) + }) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/commands/onInlineAcceptance.test.ts b/packages/amazonq/test/unit/codewhisperer/commands/onInlineAcceptance.test.ts new file mode 100644 index 00000000000..ed3bc99fa34 --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/commands/onInlineAcceptance.test.ts @@ -0,0 +1,43 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as vscode from 'vscode' +import * as sinon from 'sinon' +import { resetCodeWhispererGlobalVariables, createMockTextEditor } from 'aws-core-vscode/test' +import { onInlineAcceptance, RecommendationHandler, session } from 'aws-core-vscode/codewhisperer' + +describe('onInlineAcceptance', function () { + describe('onInlineAcceptance', function () { + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + session.reset() + }) + + afterEach(function () { + sinon.restore() + session.reset() + }) + + it('Should dispose inline completion provider', async function () { + const mockEditor = createMockTextEditor() + const spy = sinon.spy(RecommendationHandler.instance, 'disposeInlineCompletion') + await onInlineAcceptance({ + editor: mockEditor, + range: new vscode.Range(new vscode.Position(1, 0), new vscode.Position(1, 21)), + effectiveRange: new vscode.Range(new vscode.Position(1, 0), new vscode.Position(1, 21)), + acceptIndex: 0, + recommendation: "print('Hello World!')", + requestId: '', + sessionId: '', + triggerType: 'OnDemand', + completionType: 'Line', + language: 'python', + references: undefined, + }) + assert.ok(spy.calledWith()) + }) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/service/inlineCompletionService.test.ts b/packages/amazonq/test/unit/codewhisperer/service/inlineCompletionService.test.ts new file mode 100644 index 00000000000..a35677408c4 --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/service/inlineCompletionService.test.ts @@ -0,0 +1,173 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import assert from 'assert' +import * as sinon from 'sinon' +import { + InlineCompletionService, + ReferenceInlineProvider, + RecommendationHandler, + ConfigurationEntry, + CWInlineCompletionItemProvider, + session, + DefaultCodeWhispererClient, +} from 'aws-core-vscode/codewhisperer' +import { createMockTextEditor, resetCodeWhispererGlobalVariables, createMockDocument } from 'aws-core-vscode/test' + +describe('inlineCompletionService', function () { + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + }) + + describe('getPaginatedRecommendation', function () { + const config: ConfigurationEntry = { + isShowMethodsEnabled: true, + isManualTriggerEnabled: true, + isAutomatedTriggerEnabled: true, + isSuggestionsWithCodeReferencesEnabled: true, + } + + let mockClient: DefaultCodeWhispererClient + + beforeEach(async function () { + mockClient = new DefaultCodeWhispererClient() + await resetCodeWhispererGlobalVariables() + }) + + afterEach(function () { + sinon.restore() + }) + + it('should call checkAndResetCancellationTokens before showing inline and next token to be null', async function () { + const mockEditor = createMockTextEditor() + sinon.stub(RecommendationHandler.instance, 'getRecommendations').resolves({ + result: 'Succeeded', + errorMessage: undefined, + recommendationCount: 1, + }) + const checkAndResetCancellationTokensStub = sinon.stub( + RecommendationHandler.instance, + 'checkAndResetCancellationTokens' + ) + session.recommendations = [{ content: "\n\t\tconsole.log('Hello world!');\n\t}" }, { content: '' }] + await InlineCompletionService.instance.getPaginatedRecommendation( + mockClient, + mockEditor, + 'OnDemand', + config + ) + assert.ok(checkAndResetCancellationTokensStub.called) + assert.strictEqual(RecommendationHandler.instance.hasNextToken(), false) + }) + }) + + describe('clearInlineCompletionStates', function () { + it('should remove inline reference and recommendations', async function () { + const fakeReferences = [ + { + message: '', + licenseName: 'MIT', + repository: 'http://github.com/fake', + recommendationContentSpan: { + start: 0, + end: 10, + }, + }, + ] + ReferenceInlineProvider.instance.setInlineReference(1, 'test', fakeReferences) + session.recommendations = [{ content: "\n\t\tconsole.log('Hello world!');\n\t}" }, { content: '' }] + session.language = 'python' + + assert.ok(session.recommendations.length > 0) + await RecommendationHandler.instance.clearInlineCompletionStates() + assert.strictEqual(ReferenceInlineProvider.instance.refs.length, 0) + assert.strictEqual(session.recommendations.length, 0) + }) + }) + + describe('truncateOverlapWithRightContext', function () { + const fileName = 'test.py' + const language = 'python' + const rightContext = 'return target\n' + const doc = `import math\ndef two_sum(nums, target):\n` + const provider = new CWInlineCompletionItemProvider(0, 0, [], '', new vscode.Position(0, 0), '') + + it('removes overlap with right context from suggestion', async function () { + const mockSuggestion = 'return target\n' + const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) + const cursorPosition = new vscode.Position(2, 0) + const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) + assert.strictEqual(result, '') + }) + + it('only removes the overlap part from suggestion', async function () { + const mockSuggestion = 'print(nums)\nreturn target\n' + const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) + const cursorPosition = new vscode.Position(2, 0) + const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) + assert.strictEqual(result, 'print(nums)\n') + }) + + it('only removes the last overlap pattern from suggestion', async function () { + const mockSuggestion = 'return target\nprint(nums)\nreturn target\n' + const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) + const cursorPosition = new vscode.Position(2, 0) + const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) + assert.strictEqual(result, 'return target\nprint(nums)\n') + }) + + it('returns empty string if the remaining suggestion only contains white space', async function () { + const mockSuggestion = 'return target\n ' + const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) + const cursorPosition = new vscode.Position(2, 0) + const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) + assert.strictEqual(result, '') + }) + + it('returns the original suggestion if no match found', async function () { + const mockSuggestion = 'import numpy\n' + const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) + const cursorPosition = new vscode.Position(2, 0) + const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) + assert.strictEqual(result, 'import numpy\n') + }) + + it('ignores the space at the end of recommendation', async function () { + const mockSuggestion = 'return target\n\n\n\n\n' + const mockEditor = createMockTextEditor(`${doc}${rightContext}`, fileName, language) + const cursorPosition = new vscode.Position(2, 0) + const result = provider.truncateOverlapWithRightContext(mockEditor.document, mockSuggestion, cursorPosition) + assert.strictEqual(result, '') + }) + }) +}) + +describe('CWInlineCompletionProvider', function () { + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + }) + + describe('provideInlineCompletionItems', function () { + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + }) + + afterEach(function () { + sinon.restore() + }) + + it('should return undefined if position is before RecommendationHandler start pos', async function () { + const position = new vscode.Position(0, 0) + const document = createMockDocument() + const fakeContext = { triggerKind: 0, selectedCompletionInfo: undefined } + const token = new vscode.CancellationTokenSource().token + const provider = new CWInlineCompletionItemProvider(0, 0, [], '', new vscode.Position(1, 1), '') + const result = await provider.provideInlineCompletionItems(document, position, fakeContext, token) + + assert.ok(result === undefined) + }) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/service/keyStrokeHandler.test.ts b/packages/amazonq/test/unit/codewhisperer/service/keyStrokeHandler.test.ts new file mode 100644 index 00000000000..4b6a5291f22 --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/service/keyStrokeHandler.test.ts @@ -0,0 +1,237 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as vscode from 'vscode' +import * as sinon from 'sinon' +import * as codewhispererSdkClient from 'aws-core-vscode/codewhisperer' +import { + createMockTextEditor, + createTextDocumentChangeEvent, + resetCodeWhispererGlobalVariables, +} from 'aws-core-vscode/test' +import * as EditorContext from 'aws-core-vscode/codewhisperer' +import { + ConfigurationEntry, + DocumentChangedSource, + KeyStrokeHandler, + DefaultDocumentChangedType, + RecommendationService, + ClassifierTrigger, + isInlineCompletionEnabled, + RecommendationHandler, + InlineCompletionService, +} from 'aws-core-vscode/codewhisperer' + +describe('keyStrokeHandler', function () { + const config: ConfigurationEntry = { + isShowMethodsEnabled: true, + isManualTriggerEnabled: true, + isAutomatedTriggerEnabled: true, + isSuggestionsWithCodeReferencesEnabled: true, + } + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + }) + describe('processKeyStroke', async function () { + let invokeSpy: sinon.SinonStub + let startTimerSpy: sinon.SinonStub + let mockClient: codewhispererSdkClient.DefaultCodeWhispererClient + beforeEach(async function () { + invokeSpy = sinon.stub(KeyStrokeHandler.instance, 'invokeAutomatedTrigger') + startTimerSpy = sinon.stub(KeyStrokeHandler.instance, 'startIdleTimeTriggerTimer') + sinon.spy(RecommendationHandler.instance, 'getRecommendations') + mockClient = new codewhispererSdkClient.DefaultCodeWhispererClient() + await resetCodeWhispererGlobalVariables() + sinon.stub(mockClient, 'listRecommendations') + sinon.stub(mockClient, 'generateRecommendations') + }) + afterEach(function () { + sinon.restore() + }) + + it('Whatever the input is, should skip when automatic trigger is turned off, should not call invokeAutomatedTrigger', async function () { + const mockEditor = createMockTextEditor() + const mockEvent: vscode.TextDocumentChangeEvent = createTextDocumentChangeEvent( + mockEditor.document, + new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)), + ' ' + ) + const cfg: ConfigurationEntry = { + isShowMethodsEnabled: true, + isManualTriggerEnabled: true, + isAutomatedTriggerEnabled: false, + isSuggestionsWithCodeReferencesEnabled: true, + } + const keyStrokeHandler = new KeyStrokeHandler() + await keyStrokeHandler.processKeyStroke(mockEvent, mockEditor, mockClient, cfg) + assert.ok(!invokeSpy.called) + assert.ok(!startTimerSpy.called) + }) + + it('Should not call invokeAutomatedTrigger when changed text across multiple lines', async function () { + await testShouldInvoke('\nprint(n', false) + }) + + it('Should not call invokeAutomatedTrigger when doing delete or undo (empty changed text)', async function () { + await testShouldInvoke('', false) + }) + + it('Should call invokeAutomatedTrigger with Enter when inputing \n', async function () { + await testShouldInvoke('\n', true) + }) + + it('Should call invokeAutomatedTrigger with Enter when inputing \r\n', async function () { + await testShouldInvoke('\r\n', true) + }) + + it('Should call invokeAutomatedTrigger with SpecialCharacter when inputing {', async function () { + await testShouldInvoke('{', true) + }) + + it('Should not call invokeAutomatedTrigger for non-special characters for classifier language if classifier says no', async function () { + sinon.stub(ClassifierTrigger.instance, 'shouldTriggerFromClassifier').returns(false) + await testShouldInvoke('a', false) + }) + + it('Should call invokeAutomatedTrigger for non-special characters for classifier language if classifier says yes', async function () { + sinon.stub(ClassifierTrigger.instance, 'shouldTriggerFromClassifier').returns(true) + await testShouldInvoke('a', true) + }) + + it('Should skip invoking if there is immediate right context on the same line and not a single }', async function () { + const casesForSuppressTokenFilling = [ + { + rightContext: 'add', + shouldInvoke: false, + }, + { + rightContext: '}', + shouldInvoke: true, + }, + { + rightContext: '} ', + shouldInvoke: true, + }, + { + rightContext: ')', + shouldInvoke: true, + }, + { + rightContext: ') ', + shouldInvoke: true, + }, + { + rightContext: ' add', + shouldInvoke: true, + }, + { + rightContext: ' ', + shouldInvoke: true, + }, + { + rightContext: '\naddTwo', + shouldInvoke: true, + }, + ] + + for (const o of casesForSuppressTokenFilling) { + await testShouldInvoke('{', o.shouldInvoke, o.rightContext) + } + }) + + async function testShouldInvoke(input: string, shouldTrigger: boolean, rightContext: string = '') { + const mockEditor = createMockTextEditor(rightContext, 'test.js', 'javascript', 0, 0) + const mockEvent: vscode.TextDocumentChangeEvent = createTextDocumentChangeEvent( + mockEditor.document, + new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)), + input + ) + await KeyStrokeHandler.instance.processKeyStroke(mockEvent, mockEditor, mockClient, config) + assert.strictEqual( + invokeSpy.called, + shouldTrigger, + `invokeAutomatedTrigger ${shouldTrigger ? 'NOT' : 'WAS'} called for rightContext: "${rightContext}"` + ) + } + }) + + describe('invokeAutomatedTrigger', function () { + let mockClient: codewhispererSdkClient.DefaultCodeWhispererClient + beforeEach(async function () { + sinon.restore() + mockClient = new codewhispererSdkClient.DefaultCodeWhispererClient() + await resetCodeWhispererGlobalVariables() + sinon.stub(mockClient, 'listRecommendations') + sinon.stub(mockClient, 'generateRecommendations') + }) + afterEach(function () { + sinon.restore() + }) + + it('should call getPaginatedRecommendation when inline completion is enabled', async function () { + const mockEditor = createMockTextEditor() + const keyStrokeHandler = new KeyStrokeHandler() + const mockEvent: vscode.TextDocumentChangeEvent = createTextDocumentChangeEvent( + mockEditor.document, + new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)), + ' ' + ) + const getRecommendationsStub = sinon.stub(InlineCompletionService.instance, 'getPaginatedRecommendation') + await keyStrokeHandler.invokeAutomatedTrigger('Enter', mockEditor, mockClient, config, mockEvent) + assert.strictEqual(getRecommendationsStub.called, isInlineCompletionEnabled()) + }) + }) + + describe('shouldTriggerIdleTime', function () { + it('should return false when inline is enabled and inline completion is in progress ', function () { + const keyStrokeHandler = new KeyStrokeHandler() + sinon.stub(RecommendationService.instance, 'isRunning').get(() => true) + const result = keyStrokeHandler.shouldTriggerIdleTime() + assert.strictEqual(result, !isInlineCompletionEnabled()) + }) + }) + + describe('test checkChangeSource', function () { + const tabStr = ' '.repeat(EditorContext.getTabSize()) + + const cases: [string, DocumentChangedSource][] = [ + ['\n ', DocumentChangedSource.EnterKey], + ['\n', DocumentChangedSource.EnterKey], + ['(', DocumentChangedSource.SpecialCharsKey], + ['()', DocumentChangedSource.SpecialCharsKey], + ['{}', DocumentChangedSource.SpecialCharsKey], + ['(a, b):', DocumentChangedSource.Unknown], + [':', DocumentChangedSource.SpecialCharsKey], + ['a', DocumentChangedSource.RegularKey], + [tabStr, DocumentChangedSource.TabKey], + [' ', DocumentChangedSource.Reformatting], + ['def add(a,b):\n return a + b\n', DocumentChangedSource.Unknown], + ['function suggestedByIntelliSense():', DocumentChangedSource.Unknown], + ] + + for (const tuple of cases) { + const input = tuple[0] + const expected = tuple[1] + it(`test input ${input} should return ${expected}`, function () { + const actual = new DefaultDocumentChangedType( + createFakeDocumentChangeEvent(tuple[0]) + ).checkChangeSource() + assert.strictEqual(actual, expected) + }) + } + + function createFakeDocumentChangeEvent(str: string): ReadonlyArray { + return [ + { + range: new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 5)), + rangeOffset: 0, + rangeLength: 0, + text: str, + }, + ] + } + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts b/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts new file mode 100644 index 00000000000..08c1b3a7cca --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/service/recommendationHandler.test.ts @@ -0,0 +1,269 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as vscode from 'vscode' +import * as sinon from 'sinon' +import { + ReferenceInlineProvider, + session, + AuthUtil, + DefaultCodeWhispererClient, + ConfigurationEntry, + RecommendationHandler, + supplementalContextUtil, +} from 'aws-core-vscode/codewhisperer' +import { + assertTelemetryCurried, + stub, + createMockTextEditor, + resetCodeWhispererGlobalVariables, +} from 'aws-core-vscode/test' +// import * as supplementalContextUtil from 'aws-core-vscode/codewhisperer' + +describe('recommendationHandler', function () { + const config: ConfigurationEntry = { + isShowMethodsEnabled: true, + isManualTriggerEnabled: true, + isAutomatedTriggerEnabled: true, + isSuggestionsWithCodeReferencesEnabled: true, + } + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + }) + + describe('getRecommendations', async function () { + const mockClient = stub(DefaultCodeWhispererClient) + const mockEditor = createMockTextEditor() + const testStartUrl = 'testStartUrl' + + beforeEach(async function () { + sinon.restore() + await resetCodeWhispererGlobalVariables() + mockClient.listRecommendations.resolves({}) + mockClient.generateRecommendations.resolves({}) + RecommendationHandler.instance.clearRecommendations() + sinon.stub(AuthUtil.instance, 'startUrl').value(testStartUrl) + }) + + afterEach(function () { + sinon.restore() + }) + + // it('should assign correct recommendations given input', async function () { + // assert.strictEqual(CodeWhispererCodeCoverageTracker.instances.size, 0) + // assert.strictEqual( + // CodeWhispererCodeCoverageTracker.getTracker(mockEditor.document.languageId)?.serviceInvocationCount, + // 0 + // ) + + // const mockServerResult = { + // recommendations: [{ content: "print('Hello World!')" }, { content: '' }], + // $response: { + // requestId: 'test_request', + // httpResponse: { + // headers: { + // 'x-amzn-sessionid': 'test_request', + // }, + // }, + // }, + // } + // const handler = new RecommendationHandler() + // sinon.stub(handler, 'getServerResponse').resolves(mockServerResult) + // await handler.getRecommendations(mockClient, mockEditor, 'AutoTrigger', config, 'Enter', false) + // const actual = session.recommendations + // const expected: RecommendationsList = [{ content: "print('Hello World!')" }, { content: '' }] + // assert.deepStrictEqual(actual, expected) + // assert.strictEqual( + // CodeWhispererCodeCoverageTracker.getTracker(mockEditor.document.languageId)?.serviceInvocationCount, + // 1 + // ) + // }) + + it('should assign request id correctly', async function () { + const mockServerResult = { + recommendations: [{ content: "print('Hello World!')" }, { content: '' }], + $response: { + requestId: 'test_request', + httpResponse: { + headers: { + 'x-amzn-sessionid': 'test_request', + }, + }, + }, + } + const handler = new RecommendationHandler() + sinon.stub(handler, 'getServerResponse').resolves(mockServerResult) + sinon.stub(handler, 'isCancellationRequested').returns(false) + await handler.getRecommendations(mockClient, mockEditor, 'AutoTrigger', config, 'Enter', false) + assert.strictEqual(handler.requestId, 'test_request') + assert.strictEqual(session.sessionId, 'test_request') + assert.strictEqual(session.triggerType, 'AutoTrigger') + }) + + it('should call telemetry function that records a CodeWhisperer service invocation', async function () { + const mockServerResult = { + recommendations: [{ content: "print('Hello World!')" }, { content: '' }], + $response: { + requestId: 'test_request', + httpResponse: { + headers: { + 'x-amzn-sessionid': 'test_request', + }, + }, + }, + } + const handler = new RecommendationHandler() + sinon.stub(handler, 'getServerResponse').resolves(mockServerResult) + sinon.stub(supplementalContextUtil, 'fetchSupplementalContext').resolves({ + isUtg: false, + isProcessTimeout: false, + supplementalContextItems: [], + contentsLength: 100, + latency: 0, + strategy: 'empty', + }) + sinon.stub(performance, 'now').returns(0.0) + session.startPos = new vscode.Position(1, 0) + session.startCursorOffset = 2 + await handler.getRecommendations(mockClient, mockEditor, 'AutoTrigger', config, 'Enter') + const assertTelemetry = assertTelemetryCurried('codewhisperer_serviceInvocation') + assertTelemetry({ + codewhispererRequestId: 'test_request', + codewhispererSessionId: 'test_request', + codewhispererLastSuggestionIndex: 1, + codewhispererTriggerType: 'AutoTrigger', + codewhispererAutomatedTriggerType: 'Enter', + codewhispererImportRecommendationEnabled: true, + result: 'Succeeded', + codewhispererLineNumber: 1, + codewhispererCursorOffset: 38, + codewhispererLanguage: 'python', + credentialStartUrl: testStartUrl, + codewhispererSupplementalContextIsUtg: false, + codewhispererSupplementalContextTimeout: false, + codewhispererSupplementalContextLatency: 0, + codewhispererSupplementalContextLength: 100, + }) + }) + }) + + describe('isValidResponse', function () { + afterEach(function () { + sinon.restore() + }) + it('should return true if any response is not empty', function () { + const handler = new RecommendationHandler() + session.recommendations = [ + { + content: + '\n // Use the console to output debug info…n of the command with the "command" variable', + }, + { content: '' }, + ] + assert.ok(handler.isValidResponse()) + }) + + it('should return false if response is empty', function () { + const handler = new RecommendationHandler() + session.recommendations = [] + assert.ok(!handler.isValidResponse()) + }) + + it('should return false if all response has no string length', function () { + const handler = new RecommendationHandler() + session.recommendations = [{ content: '' }, { content: '' }] + assert.ok(!handler.isValidResponse()) + }) + }) + + describe('setCompletionType/getCompletionType', function () { + beforeEach(function () { + sinon.restore() + }) + + it('should set the completion type to block given a multi-line suggestion', function () { + session.setCompletionType(0, { content: 'test\n\n \t\r\nanother test' }) + assert.strictEqual(session.getCompletionType(0), 'Block') + + session.setCompletionType(0, { content: 'test\ntest\n' }) + assert.strictEqual(session.getCompletionType(0), 'Block') + + session.setCompletionType(0, { content: '\n \t\r\ntest\ntest' }) + assert.strictEqual(session.getCompletionType(0), 'Block') + }) + + it('should set the completion type to line given a single-line suggestion', function () { + session.setCompletionType(0, { content: 'test' }) + assert.strictEqual(session.getCompletionType(0), 'Line') + + session.setCompletionType(0, { content: 'test\r\t ' }) + assert.strictEqual(session.getCompletionType(0), 'Line') + }) + + it('should set the completion type to line given a multi-line completion but only one-lien of non-blank sequence', function () { + session.setCompletionType(0, { content: 'test\n\t' }) + assert.strictEqual(session.getCompletionType(0), 'Line') + + session.setCompletionType(0, { content: 'test\n ' }) + assert.strictEqual(session.getCompletionType(0), 'Line') + + session.setCompletionType(0, { content: 'test\n\r' }) + assert.strictEqual(session.getCompletionType(0), 'Line') + + session.setCompletionType(0, { content: '\n\n\n\ntest' }) + assert.strictEqual(session.getCompletionType(0), 'Line') + }) + }) + + describe('on event change', async function () { + beforeEach(function () { + const fakeReferences = [ + { + message: '', + licenseName: 'MIT', + repository: 'http://github.com/fake', + recommendationContentSpan: { + start: 0, + end: 10, + }, + }, + ] + ReferenceInlineProvider.instance.setInlineReference(1, 'test', fakeReferences) + session.sessionId = '' + RecommendationHandler.instance.requestId = '' + }) + + it('should remove inline reference onEditorChange', async function () { + session.sessionId = 'aSessionId' + RecommendationHandler.instance.requestId = 'aRequestId' + await RecommendationHandler.instance.onEditorChange() + assert.strictEqual(ReferenceInlineProvider.instance.refs.length, 0) + }) + it('should remove inline reference onFocusChange', async function () { + session.sessionId = 'aSessionId' + RecommendationHandler.instance.requestId = 'aRequestId' + await RecommendationHandler.instance.onFocusChange() + assert.strictEqual(ReferenceInlineProvider.instance.refs.length, 0) + }) + it('should not remove inline reference on cursor change from typing', async function () { + await RecommendationHandler.instance.onCursorChange({ + textEditor: createMockTextEditor(), + selections: [], + kind: vscode.TextEditorSelectionChangeKind.Keyboard, + }) + assert.strictEqual(ReferenceInlineProvider.instance.refs.length, 1) + }) + + it('should remove inline reference on cursor change from mouse movement', async function () { + await RecommendationHandler.instance.onCursorChange({ + textEditor: vscode.window.activeTextEditor!, + selections: [], + kind: vscode.TextEditorSelectionChangeKind.Mouse, + }) + assert.strictEqual(ReferenceInlineProvider.instance.refs.length, 0) + }) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/service/securityIssueHoverProvider.test.ts b/packages/amazonq/test/unit/codewhisperer/service/securityIssueHoverProvider.test.ts index 956c3b43d73..7709eed10fe 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/securityIssueHoverProvider.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/securityIssueHoverProvider.test.ts @@ -21,6 +21,49 @@ describe('securityIssueHoverProvider', () => { token = new vscode.CancellationTokenSource() }) + function buildCommandLink( + command: string, + commandIcon: string, + args: any[], + label: string, + tooltip: string + ): string { + return `[$(${commandIcon}) ${label}](command:${command}?${encodeURIComponent(JSON.stringify(args))} '${tooltip}')` + } + + function buildExpectedContent(issue: any, fileName: string, description: string, severity?: string): string { + const severityBadge = severity ? ` ![${severity}](severity-${severity.toLowerCase()}.svg)` : ' ' + const commands = [ + buildCommandLink( + 'aws.amazonq.explainIssue', + 'comment', + [issue, fileName], + 'Explain', + 'Explain with Amazon Q' + ), + buildCommandLink('aws.amazonq.generateFix', 'wrench', [issue, fileName], 'Fix', 'Fix with Amazon Q'), + buildCommandLink( + 'aws.amazonq.security.ignore', + 'error', + [issue, fileName, 'hover'], + 'Ignore', + 'Ignore Issue' + ), + buildCommandLink( + 'aws.amazonq.security.ignoreAll', + 'error', + [issue, 'hover'], + 'Ignore All', + 'Ignore Similar Issues' + ), + ] + return `## title${severityBadge}\n${description}\n\n${commands.join('\n | ')}\n` + } + + function setupIssues(issues: any[]): void { + securityIssueProvider.issues = [{ filePath: mockDocument.fileName, issues }] + } + it('should return hover for each issue for the current position', () => { const issues = [ createCodeScanIssue({ findingId: 'finding-1', detectorId: 'language/detector-1', ruleId: 'Rule-123' }), @@ -32,82 +75,17 @@ describe('securityIssueHoverProvider', () => { }), ] - securityIssueProvider.issues = [ - { - filePath: mockDocument.fileName, - issues, - }, - ] - + setupIssues(issues) const actual = securityIssueHoverProvider.provideHover(mockDocument, new vscode.Position(0, 0), token.token) assert.strictEqual(actual.contents.length, 2) assert.strictEqual( (actual.contents[0] as vscode.MarkdownString).value, - '## title ![High](severity-high.svg)\n' + - 'fix\n\n' + - `[$(eye) View Details](command:aws.amazonq.openSecurityIssuePanel?${encodeURIComponent( - JSON.stringify([issues[0], mockDocument.fileName]) - )} 'Open "Code Issue Details"')\n` + - ` | [$(comment) Explain](command:aws.amazonq.explainIssue?${encodeURIComponent( - JSON.stringify([issues[0]]) - )} 'Explain with Amazon Q')\n` + - ` | [$(error) Ignore](command:aws.amazonq.security.ignore?${encodeURIComponent( - JSON.stringify([issues[0], mockDocument.fileName, 'hover']) - )} 'Ignore Issue')\n` + - ` | [$(error) Ignore All](command:aws.amazonq.security.ignoreAll?${encodeURIComponent( - JSON.stringify([issues[0], 'hover']) - )} 'Ignore Similar Issues')\n` + - ` | [$(wrench) Fix](command:aws.amazonq.applySecurityFix?${encodeURIComponent( - JSON.stringify([issues[0], mockDocument.fileName, 'hover']) - )} 'Fix with Amazon Q')\n` + - '### Suggested Fix Preview\n\n' + - '\n\n' + - '```undefined\n' + - '@@ -1,1 +1,1 @@ \n' + - '```\n\n' + - '\n' + - '
\n' + - '\n\n' + - '```language\n' + - 'first line \n' + - '```\n\n' + - '\n' + - '
\n' + - '\n\n' + - '```diff\n' + - '-second line \n' + - '```\n\n' + - '\n' + - '
\n' + - '\n\n' + - '```diff\n' + - '+third line \n' + - '```\n\n' + - '\n' + - '
\n' + - '\n\n' + - '```language\n' + - 'fourth line \n' + - '```\n\n' + - '\n\n' + buildExpectedContent(issues[0], mockDocument.fileName, 'fix', 'High') ) assert.strictEqual( (actual.contents[1] as vscode.MarkdownString).value, - '## title ![High](severity-high.svg)\n' + - 'recommendationText\n\n' + - `[$(eye) View Details](command:aws.amazonq.openSecurityIssuePanel?${encodeURIComponent( - JSON.stringify([issues[1], mockDocument.fileName]) - )} 'Open "Code Issue Details"')\n` + - ` | [$(comment) Explain](command:aws.amazonq.explainIssue?${encodeURIComponent( - JSON.stringify([issues[1]]) - )} 'Explain with Amazon Q')\n` + - ` | [$(error) Ignore](command:aws.amazonq.security.ignore?${encodeURIComponent( - JSON.stringify([issues[1], mockDocument.fileName, 'hover']) - )} 'Ignore Issue')\n` + - ` | [$(error) Ignore All](command:aws.amazonq.security.ignoreAll?${encodeURIComponent( - JSON.stringify([issues[1], 'hover']) - )} 'Ignore Similar Issues')\n` + buildExpectedContent(issues[1], mockDocument.fileName, 'recommendationText', 'High') ) assertTelemetry('codewhisperer_codeScanIssueHover', [ { findingId: 'finding-1', detectorId: 'language/detector-1', ruleId: 'Rule-123', includesFix: true }, @@ -116,27 +94,15 @@ describe('securityIssueHoverProvider', () => { }) it('should return empty contents if there is no issue on the current position', () => { - securityIssueProvider.issues = [ - { - filePath: mockDocument.fileName, - issues: [createCodeScanIssue()], - }, - ] - + setupIssues([createCodeScanIssue()]) const actual = securityIssueHoverProvider.provideHover(mockDocument, new vscode.Position(2, 0), token.token) assert.strictEqual(actual.contents.length, 0) }) it('should skip issues not in the current file', () => { securityIssueProvider.issues = [ - { - filePath: 'some/path', - issues: [createCodeScanIssue()], - }, - { - filePath: mockDocument.fileName, - issues: [createCodeScanIssue()], - }, + { filePath: 'some/path', issues: [createCodeScanIssue()] }, + { filePath: mockDocument.fileName, issues: [createCodeScanIssue()] }, ] const actual = securityIssueHoverProvider.provideHover(mockDocument, new vscode.Position(0, 0), token.token) assert.strictEqual(actual.contents.length, 1) @@ -144,30 +110,12 @@ describe('securityIssueHoverProvider', () => { it('should not show severity badge if undefined', () => { const issues = [createCodeScanIssue({ severity: undefined, suggestedFixes: [] })] - securityIssueProvider.issues = [ - { - filePath: mockDocument.fileName, - issues, - }, - ] + setupIssues(issues) const actual = securityIssueHoverProvider.provideHover(mockDocument, new vscode.Position(0, 0), token.token) assert.strictEqual(actual.contents.length, 1) assert.strictEqual( (actual.contents[0] as vscode.MarkdownString).value, - '## title \n' + - 'recommendationText\n\n' + - `[$(eye) View Details](command:aws.amazonq.openSecurityIssuePanel?${encodeURIComponent( - JSON.stringify([issues[0], mockDocument.fileName]) - )} 'Open "Code Issue Details"')\n` + - ` | [$(comment) Explain](command:aws.amazonq.explainIssue?${encodeURIComponent( - JSON.stringify([issues[0]]) - )} 'Explain with Amazon Q')\n` + - ` | [$(error) Ignore](command:aws.amazonq.security.ignore?${encodeURIComponent( - JSON.stringify([issues[0], mockDocument.fileName, 'hover']) - )} 'Ignore Issue')\n` + - ` | [$(error) Ignore All](command:aws.amazonq.security.ignoreAll?${encodeURIComponent( - JSON.stringify([issues[0], 'hover']) - )} 'Ignore Similar Issues')\n` + buildExpectedContent(issues[0], mockDocument.fileName, 'recommendationText') ) }) @@ -182,75 +130,17 @@ describe('securityIssueHoverProvider', () => { ], }), ] - securityIssueProvider.issues = [ - { - filePath: mockDocument.fileName, - issues, - }, - ] + setupIssues(issues) const actual = securityIssueHoverProvider.provideHover(mockDocument, new vscode.Position(0, 0), token.token) assert.strictEqual(actual.contents.length, 1) assert.strictEqual( (actual.contents[0] as vscode.MarkdownString).value, - '## title ![High](severity-high.svg)\n' + - 'fix\n\n' + - `[$(eye) View Details](command:aws.amazonq.openSecurityIssuePanel?${encodeURIComponent( - JSON.stringify([issues[0], mockDocument.fileName]) - )} 'Open "Code Issue Details"')\n` + - ` | [$(comment) Explain](command:aws.amazonq.explainIssue?${encodeURIComponent( - JSON.stringify([issues[0]]) - )} 'Explain with Amazon Q')\n` + - ` | [$(error) Ignore](command:aws.amazonq.security.ignore?${encodeURIComponent( - JSON.stringify([issues[0], mockDocument.fileName, 'hover']) - )} 'Ignore Issue')\n` + - ` | [$(error) Ignore All](command:aws.amazonq.security.ignoreAll?${encodeURIComponent( - JSON.stringify([issues[0], 'hover']) - )} 'Ignore Similar Issues')\n` + - ` | [$(wrench) Fix](command:aws.amazonq.applySecurityFix?${encodeURIComponent( - JSON.stringify([issues[0], mockDocument.fileName, 'hover']) - )} 'Fix with Amazon Q')\n` + - '### Suggested Fix Preview\n\n' + - '\n\n' + - '```undefined\n' + - '@@ -1,1 +1,1 @@ \n' + - '```\n\n' + - '\n' + - '
\n' + - '\n\n' + - '```language\n' + - 'first line \n' + - '```\n\n' + - '\n' + - '
\n' + - '\n\n' + - '```diff\n' + - '-second line \n' + - '-third line \n' + - '```\n\n' + - '\n' + - '
\n' + - '\n\n' + - '```diff\n' + - '+fourth line \n' + - '```\n\n' + - '\n' + - '
\n' + - '\n\n' + - '```language\n' + - 'fifth line \n' + - '```\n\n' + - '\n\n' + buildExpectedContent(issues[0], mockDocument.fileName, 'fix', 'High') ) }) it('should not show issues that are not visible', () => { - const issues = [createCodeScanIssue({ visible: false })] - securityIssueProvider.issues = [ - { - filePath: mockDocument.fileName, - issues, - }, - ] + setupIssues([createCodeScanIssue({ visible: false })]) const actual = securityIssueHoverProvider.provideHover(mockDocument, new vscode.Position(0, 0), token.token) assert.strictEqual(actual.contents.length, 0) }) diff --git a/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererCodeCoverageTracker.test.ts b/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererCodeCoverageTracker.test.ts new file mode 100644 index 00000000000..ee001b3328d --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererCodeCoverageTracker.test.ts @@ -0,0 +1,560 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import * as vscode from 'vscode' +import { + CodeWhispererCodeCoverageTracker, + vsCodeState, + TelemetryHelper, + AuthUtil, + getUnmodifiedAcceptedTokens, +} from 'aws-core-vscode/codewhisperer' +import { createMockDocument, createMockTextEditor, resetCodeWhispererGlobalVariables } from 'aws-core-vscode/test' +import { globals } from 'aws-core-vscode/shared' +import { assertTelemetryCurried } from 'aws-core-vscode/test' + +describe('codewhispererCodecoverageTracker', function () { + const language = 'python' + + describe('test getTracker', function () { + afterEach(async function () { + await resetCodeWhispererGlobalVariables() + CodeWhispererCodeCoverageTracker.instances.clear() + }) + + it('unsupported language', function () { + assert.strictEqual(CodeWhispererCodeCoverageTracker.getTracker('vb'), undefined) + assert.strictEqual(CodeWhispererCodeCoverageTracker.getTracker('ipynb'), undefined) + }) + + it('supported language', function () { + assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('python'), undefined) + assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('javascriptreact'), undefined) + assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('java'), undefined) + assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('javascript'), undefined) + assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('cpp'), undefined) + assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('ruby'), undefined) + assert.notStrictEqual(CodeWhispererCodeCoverageTracker.getTracker('go'), undefined) + }) + + it('supported language and should return singleton object per language', function () { + let instance1: CodeWhispererCodeCoverageTracker | undefined + let instance2: CodeWhispererCodeCoverageTracker | undefined + instance1 = CodeWhispererCodeCoverageTracker.getTracker('java') + instance2 = CodeWhispererCodeCoverageTracker.getTracker('java') + assert.notStrictEqual(instance1, undefined) + assert.strictEqual(Object.is(instance1, instance2), true) + + instance1 = CodeWhispererCodeCoverageTracker.getTracker('python') + instance2 = CodeWhispererCodeCoverageTracker.getTracker('python') + assert.notStrictEqual(instance1, undefined) + assert.strictEqual(Object.is(instance1, instance2), true) + + instance1 = CodeWhispererCodeCoverageTracker.getTracker('javascriptreact') + instance2 = CodeWhispererCodeCoverageTracker.getTracker('javascriptreact') + assert.notStrictEqual(instance1, undefined) + assert.strictEqual(Object.is(instance1, instance2), true) + }) + }) + + describe('test isActive', function () { + let tracker: CodeWhispererCodeCoverageTracker | undefined + + afterEach(async function () { + await resetCodeWhispererGlobalVariables() + CodeWhispererCodeCoverageTracker.instances.clear() + sinon.restore() + }) + + it('inactive case: telemetryEnable = true, isConnected = false', function () { + sinon.stub(TelemetryHelper.instance, 'isTelemetryEnabled').returns(true) + sinon.stub(AuthUtil.instance, 'isConnected').returns(false) + + tracker = CodeWhispererCodeCoverageTracker.getTracker('python') + if (!tracker) { + assert.fail() + } + + assert.strictEqual(tracker.isActive(), false) + }) + + it('inactive case: telemetryEnabled = false, isConnected = false', function () { + sinon.stub(TelemetryHelper.instance, 'isTelemetryEnabled').returns(false) + sinon.stub(AuthUtil.instance, 'isConnected').returns(false) + + tracker = CodeWhispererCodeCoverageTracker.getTracker('java') + if (!tracker) { + assert.fail() + } + + assert.strictEqual(tracker.isActive(), false) + }) + + it('active case: telemetryEnabled = true, isConnected = true', function () { + sinon.stub(TelemetryHelper.instance, 'isTelemetryEnabled').returns(true) + sinon.stub(AuthUtil.instance, 'isConnected').returns(true) + + tracker = CodeWhispererCodeCoverageTracker.getTracker('javascript') + if (!tracker) { + assert.fail() + } + assert.strictEqual(tracker.isActive(), true) + }) + }) + + describe('updateAcceptedTokensCount', function () { + let tracker: CodeWhispererCodeCoverageTracker | undefined + + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + tracker = CodeWhispererCodeCoverageTracker.getTracker(language) + if (tracker) { + sinon.stub(tracker, 'isActive').returns(true) + } + }) + + afterEach(function () { + sinon.restore() + CodeWhispererCodeCoverageTracker.instances.clear() + }) + + it('Should compute edit distance to update the accepted tokens', function () { + if (!tracker) { + assert.fail() + } + const editor = createMockTextEditor('def addTwoNumbers(a, b):\n') + + tracker.addAcceptedTokens(editor.document.fileName, { + range: new vscode.Range(0, 0, 0, 25), + text: `def addTwoNumbers(x, y):\n`, + accepted: 25, + }) + tracker.addTotalTokens(editor.document.fileName, 100) + tracker.updateAcceptedTokensCount(editor) + assert.strictEqual(tracker?.acceptedTokens[editor.document.fileName][0].accepted, 23) + }) + }) + + describe('getUnmodifiedAcceptedTokens', function () { + let tracker: CodeWhispererCodeCoverageTracker | undefined + + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + tracker = CodeWhispererCodeCoverageTracker.getTracker(language) + if (tracker) { + sinon.stub(tracker, 'isActive').returns(true) + } + }) + + afterEach(function () { + sinon.restore() + CodeWhispererCodeCoverageTracker.instances.clear() + }) + + it('Should return correct unmodified accepted tokens count', function () { + assert.strictEqual(getUnmodifiedAcceptedTokens('foo', 'fou'), 2) + assert.strictEqual(getUnmodifiedAcceptedTokens('foo', 'f11111oo'), 3) + assert.strictEqual(getUnmodifiedAcceptedTokens('foo', 'fo'), 2) + assert.strictEqual(getUnmodifiedAcceptedTokens('helloworld', 'HelloWorld'), 8) + assert.strictEqual(getUnmodifiedAcceptedTokens('helloworld', 'World'), 4) + assert.strictEqual(getUnmodifiedAcceptedTokens('CodeWhisperer', 'CODE'), 1) + assert.strictEqual(getUnmodifiedAcceptedTokens('CodeWhisperer', 'CodeWhispererGood'), 13) + }) + }) + + describe('countAcceptedTokens', function () { + let tracker: CodeWhispererCodeCoverageTracker | undefined + + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + tracker = CodeWhispererCodeCoverageTracker.getTracker(language) + if (tracker) { + sinon.stub(tracker, 'isActive').returns(true) + } + }) + + afterEach(function () { + sinon.restore() + CodeWhispererCodeCoverageTracker.instances.clear() + }) + + it('Should skip when tracker is not active', function () { + if (!tracker) { + assert.fail() + } + tracker.countAcceptedTokens(new vscode.Range(0, 0, 0, 1), 'a', 'test.py') + const spy = sinon.spy(CodeWhispererCodeCoverageTracker.prototype, 'addAcceptedTokens') + assert.ok(!spy.called) + }) + + it('Should increase AcceptedTokens', function () { + if (!tracker) { + assert.fail() + } + tracker.countAcceptedTokens(new vscode.Range(0, 0, 0, 1), 'a', 'test.py') + assert.deepStrictEqual(tracker.acceptedTokens['test.py'][0], { + range: new vscode.Range(0, 0, 0, 1), + text: 'a', + accepted: 1, + }) + }) + it('Should increase TotalTokens', function () { + if (!tracker) { + assert.fail() + } + tracker.countAcceptedTokens(new vscode.Range(0, 0, 0, 1), 'a', 'test.py') + tracker.countAcceptedTokens(new vscode.Range(0, 0, 0, 1), 'b', 'test.py') + assert.deepStrictEqual(tracker.totalTokens['test.py'], 2) + }) + }) + + describe('countTotalTokens', function () { + let tracker: CodeWhispererCodeCoverageTracker | undefined + + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + tracker = CodeWhispererCodeCoverageTracker.getTracker(language) + if (tracker) { + sinon.stub(tracker, 'isActive').returns(true) + } + }) + + afterEach(function () { + sinon.restore() + CodeWhispererCodeCoverageTracker.instances.clear() + }) + + it('Should skip when content change size is more than 50', function () { + if (!tracker) { + assert.fail() + } + tracker.countTotalTokens({ + reason: undefined, + document: createMockDocument(), + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 600), + rangeOffset: 0, + rangeLength: 600, + text: 'def twoSum(nums, target):\nfor '.repeat(20), + }, + ], + }) + assert.strictEqual(Object.keys(tracker.totalTokens).length, 0) + }) + + it('Should not skip when content change size is less than 50', function () { + if (!tracker) { + assert.fail() + } + tracker.countTotalTokens({ + reason: undefined, + document: createMockDocument(), + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 49), + rangeOffset: 0, + rangeLength: 49, + text: 'a = 123'.repeat(7), + }, + ], + }) + assert.strictEqual(Object.keys(tracker.totalTokens).length, 1) + assert.strictEqual(Object.values(tracker.totalTokens)[0], 49) + }) + + it('Should skip when CodeWhisperer is editing', function () { + if (!tracker) { + assert.fail() + } + vsCodeState.isCodeWhispererEditing = true + tracker.countTotalTokens({ + reason: undefined, + document: createMockDocument(), + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 30), + rangeOffset: 0, + rangeLength: 30, + text: 'def twoSum(nums, target):\nfor', + }, + ], + }) + const startedSpy = sinon.spy(CodeWhispererCodeCoverageTracker.prototype, 'addTotalTokens') + assert.ok(!startedSpy.called) + }) + + it('Should not reduce tokens when delete', function () { + if (!tracker) { + assert.fail() + } + const doc = createMockDocument('import math', 'test.py', 'python') + tracker.countTotalTokens({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 1), + rangeOffset: 0, + rangeLength: 0, + text: 'a', + }, + ], + }) + tracker.countTotalTokens({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 1), + rangeOffset: 0, + rangeLength: 0, + text: 'b', + }, + ], + }) + assert.strictEqual(tracker?.totalTokens[doc.fileName], 2) + tracker.countTotalTokens({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 1), + rangeOffset: 1, + rangeLength: 1, + text: '', + }, + ], + }) + assert.strictEqual(tracker?.totalTokens[doc.fileName], 2) + }) + + it('Should add tokens when type', function () { + if (!tracker) { + assert.fail() + } + const doc = createMockDocument('import math', 'test.py', 'python') + tracker.countTotalTokens({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 1), + rangeOffset: 0, + rangeLength: 0, + text: 'a', + }, + ], + }) + assert.strictEqual(tracker?.totalTokens[doc.fileName], 1) + }) + + it('Should add tokens when hitting enter with indentation', function () { + if (!tracker) { + assert.fail() + } + const doc = createMockDocument('def h():', 'test.py', 'python') + tracker.countTotalTokens({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 8), + rangeOffset: 0, + rangeLength: 0, + text: '\n ', + }, + ], + }) + assert.strictEqual(tracker?.totalTokens[doc.fileName], 1) + }) + + it('Should add tokens when hitting enter with indentation in Windows', function () { + if (!tracker) { + assert.fail() + } + const doc = createMockDocument('def h():', 'test.py', 'python') + tracker.countTotalTokens({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 8), + rangeOffset: 0, + rangeLength: 0, + text: '\r\n ', + }, + ], + }) + assert.strictEqual(tracker?.totalTokens[doc.fileName], 1) + }) + + it('Should add tokens when hitting enter with indentation in Java', function () { + if (!tracker) { + assert.fail() + } + const doc = createMockDocument('class A() {', 'test.java', 'java') + tracker.countTotalTokens({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 11), + rangeOffset: 0, + rangeLength: 0, + text: '', + }, + { + range: new vscode.Range(0, 0, 0, 11), + rangeOffset: 0, + rangeLength: 0, + text: '\n\t\t', + }, + ], + }) + assert.strictEqual(tracker?.totalTokens[doc.fileName], 1) + }) + + it('Should add tokens when inserting closing brackets', function () { + if (!tracker) { + assert.fail() + } + const doc = createMockDocument('a=', 'test.py', 'python') + tracker.countTotalTokens({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 3), + rangeOffset: 0, + rangeLength: 0, + text: '[]', + }, + ], + }) + assert.strictEqual(tracker?.totalTokens[doc.fileName], 2) + }) + + it('Should add tokens when inserting closing brackets in Java', function () { + if (!tracker) { + assert.fail() + } + const doc = createMockDocument('class A ', 'test.java', 'java') + tracker.countTotalTokens({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 8), + rangeOffset: 0, + rangeLength: 0, + text: '{}', + }, + { + range: new vscode.Range(0, 0, 0, 8), + rangeOffset: 0, + rangeLength: 0, + text: '', + }, + ], + }) + assert.strictEqual(tracker?.totalTokens[doc.fileName], 2) + }) + }) + + describe('flush', function () { + let tracker: CodeWhispererCodeCoverageTracker | undefined + + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + tracker = CodeWhispererCodeCoverageTracker.getTracker(language) + if (tracker) { + sinon.stub(tracker, 'isActive').returns(true) + } + }) + + afterEach(function () { + sinon.restore() + CodeWhispererCodeCoverageTracker.instances.clear() + }) + + it('Should not send codecoverage telemetry if tracker is not active', function () { + if (!tracker) { + assert.fail() + } + sinon.restore() + sinon.stub(tracker, 'isActive').returns(false) + + tracker.addAcceptedTokens(`test.py`, { range: new vscode.Range(0, 0, 0, 7), text: `print()`, accepted: 7 }) + tracker.addTotalTokens(`test.py`, 100) + tracker.flush() + const data = globals.telemetry.logger.query({ + metricName: 'codewhisperer_codePercentage', + excludeKeys: ['awsAccount'], + }) + assert.strictEqual(data.length, 0) + }) + }) + + describe('emitCodeWhispererCodeContribution', function () { + let tracker: CodeWhispererCodeCoverageTracker | undefined + + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + tracker = CodeWhispererCodeCoverageTracker.getTracker(language) + if (tracker) { + sinon.stub(tracker, 'isActive').returns(true) + } + }) + + afterEach(function () { + sinon.restore() + CodeWhispererCodeCoverageTracker.instances.clear() + }) + + it('should emit correct code coverage telemetry in python file', async function () { + const tracker = CodeWhispererCodeCoverageTracker.getTracker(language) + + const assertTelemetry = assertTelemetryCurried('codewhisperer_codePercentage') + tracker?.incrementServiceInvocationCount() + tracker?.addAcceptedTokens(`test.py`, { range: new vscode.Range(0, 0, 0, 7), text: `print()`, accepted: 7 }) + tracker?.addTotalTokens(`test.py`, 100) + tracker?.emitCodeWhispererCodeContribution() + assertTelemetry({ + codewhispererTotalTokens: 100, + codewhispererLanguage: language, + codewhispererAcceptedTokens: 7, + codewhispererSuggestedTokens: 7, + codewhispererPercentage: 7, + successCount: 1, + }) + }) + + it('should emit correct code coverage telemetry when success count = 0', async function () { + const tracker = CodeWhispererCodeCoverageTracker.getTracker('java') + + const assertTelemetry = assertTelemetryCurried('codewhisperer_codePercentage') + tracker?.addAcceptedTokens(`test.java`, { + range: new vscode.Range(0, 0, 0, 18), + text: `public static main`, + accepted: 18, + }) + tracker?.incrementServiceInvocationCount() + tracker?.incrementServiceInvocationCount() + tracker?.addTotalTokens(`test.java`, 30) + tracker?.emitCodeWhispererCodeContribution() + assertTelemetry({ + codewhispererTotalTokens: 30, + codewhispererLanguage: 'java', + codewhispererAcceptedTokens: 18, + codewhispererSuggestedTokens: 18, + codewhispererPercentage: 60, + successCount: 2, + }) + }) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/bm25.test.ts b/packages/amazonq/test/unit/codewhisperer/util/bm25.test.ts new file mode 100644 index 00000000000..0a3c4b17d60 --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/util/bm25.test.ts @@ -0,0 +1,117 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { BM25Okapi } from 'aws-core-vscode/codewhisperer' + +describe('bm25', function () { + it('simple case 1', function () { + const query = 'windy London' + const corpus = ['Hello there good man!', 'It is quite windy in London', 'How is the weather today?'] + + const sut = new BM25Okapi(corpus) + const actual = sut.score(query) + + assert.deepStrictEqual(actual, [ + { + content: 'Hello there good man!', + index: 0, + score: 0, + }, + { + content: 'It is quite windy in London', + index: 1, + score: 0.937294722506405, + }, + { + content: 'How is the weather today?', + index: 2, + score: 0, + }, + ]) + + assert.deepStrictEqual(sut.topN(query, 1), [ + { + content: 'It is quite windy in London', + index: 1, + score: 0.937294722506405, + }, + ]) + }) + + it('simple case 2', function () { + const query = 'codewhisperer is a machine learning powered code generator' + const corpus = [ + 'codewhisperer goes GA at April 2023', + 'machine learning tool is the trending topic!!! :)', + 'codewhisperer is good =))))', + 'codewhisperer vs. copilot, which code generator better?', + 'copilot is a AI code generator too', + 'it is so amazing!!', + ] + + const sut = new BM25Okapi(corpus) + const actual = sut.score(query) + + assert.deepStrictEqual(actual, [ + { + content: 'codewhisperer goes GA at April 2023', + index: 0, + score: 0, + }, + { + content: 'machine learning tool is the trending topic!!! :)', + index: 1, + score: 2.597224531416621, + }, + { + content: 'codewhisperer is good =))))', + index: 2, + score: 0.3471790843435529, + }, + { + content: 'codewhisperer vs. copilot, which code generator better?', + index: 3, + score: 1.063018436525109, + }, + { + content: 'copilot is a AI code generator too', + index: 4, + score: 2.485359418462239, + }, + { + content: 'it is so amazing!!', + index: 5, + score: 0.3154033715392277, + }, + ]) + + assert.deepStrictEqual(sut.topN(query, 1), [ + { + content: 'machine learning tool is the trending topic!!! :)', + index: 1, + score: 2.597224531416621, + }, + ]) + + assert.deepStrictEqual(sut.topN(query, 3), [ + { + content: 'machine learning tool is the trending topic!!! :)', + index: 1, + score: 2.597224531416621, + }, + { + content: 'copilot is a AI code generator too', + index: 4, + score: 2.485359418462239, + }, + { + content: 'codewhisperer vs. copilot, which code generator better?', + index: 3, + score: 1.063018436525109, + }, + ]) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/codeParsingUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/codeParsingUtil.test.ts new file mode 100644 index 00000000000..2a2ad8bb34e --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/util/codeParsingUtil.test.ts @@ -0,0 +1,327 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + PlatformLanguageId, + extractClasses, + extractFunctions, + isTestFile, + utgLanguageConfigs, +} from 'aws-core-vscode/codewhisperer' +import assert from 'assert' +import { createTestWorkspaceFolder, toTextDocument } from 'aws-core-vscode/test' + +describe('RegexValidationForPython', () => { + it('should extract all function names from a python file content', () => { + // TODO: Replace this variable based testing to read content from File. + // const filePath = vscode.Uri.file('./testData/samplePython.py').fsPath; + // const fileContent = fs.readFileSync('./testData/samplePython.py' , 'utf-8'); + // const regex = /function\s+(\w+)/g; + + const result = extractFunctions(pythonFileContent, utgLanguageConfigs['python'].functionExtractionPattern) + assert.strictEqual(result.length, 13) + assert.deepStrictEqual(result, [ + 'hello_world', + 'add_numbers', + 'multiply_numbers', + 'sum_numbers', + 'divide_numbers', + '__init__', + 'add', + 'multiply', + 'square', + 'from_sum', + '__init__', + 'triple', + 'main', + ]) + }) + + it('should extract all class names from a file content', () => { + const result = extractClasses(pythonFileContent, utgLanguageConfigs['python'].classExtractionPattern) + assert.deepStrictEqual(result, ['Calculator']) + }) +}) + +describe('RegexValidationForJava', () => { + it('should extract all function names from a java file content', () => { + // TODO: Replace this variable based testing to read content from File. + // const filePath = vscode.Uri.file('./testData/samplePython.py').fsPath; + // const fileContent = fs.readFileSync('./testData/samplePython.py' , 'utf-8'); + // const regex = /function\s+(\w+)/g; + + const result = extractFunctions(javaFileContent, utgLanguageConfigs['java'].functionExtractionPattern) + assert.strictEqual(result.length, 5) + assert.deepStrictEqual(result, ['sayHello', 'doSomething', 'square', 'manager', 'ABCFUNCTION']) + }) + + it('should extract all class names from a java file content', () => { + const result = extractClasses(javaFileContent, utgLanguageConfigs['java'].classExtractionPattern) + assert.deepStrictEqual(result, ['Test']) + }) +}) + +describe('isTestFile', () => { + let testWsFolder: string + beforeEach(async function () { + testWsFolder = (await createTestWorkspaceFolder()).uri.fsPath + }) + + it('validate by file path', async function () { + const langs = new Map([ + ['java', '.java'], + ['python', '.py'], + ['typescript', '.ts'], + ['javascript', '.js'], + ['typescriptreact', '.tsx'], + ['javascriptreact', '.jsx'], + ]) + const testFilePathsWithoutExt = [ + '/test/MyClass', + '/test/my_class', + '/tst/MyClass', + '/tst/my_class', + '/tests/MyClass', + '/tests/my_class', + ] + + const srcFilePathsWithoutExt = [ + '/src/MyClass', + 'MyClass', + 'foo/bar/MyClass', + 'foo/my_class', + 'my_class', + 'anyFolderOtherThanTest/foo/myClass', + ] + + for (const [languageId, ext] of langs) { + const testFilePaths = testFilePathsWithoutExt.map((it) => it + ext) + for (const testFilePath of testFilePaths) { + const actual = await isTestFile(testFilePath, { languageId: languageId }) + assert.strictEqual(actual, true) + } + + const srcFilePaths = srcFilePathsWithoutExt.map((it) => it + ext) + for (const srcFilePath of srcFilePaths) { + const actual = await isTestFile(srcFilePath, { languageId: languageId }) + assert.strictEqual(actual, false) + } + } + }) + + async function assertIsTestFile( + fileNames: string[], + config: { languageId: PlatformLanguageId }, + expected: boolean + ) { + for (const fileName of fileNames) { + const document = await toTextDocument('', fileName, testWsFolder) + const actual = await isTestFile(document.uri.fsPath, { languageId: config.languageId }) + assert.strictEqual(actual, expected) + } + } + + it('validate by file name', async function () { + const camelCaseSrc = ['Foo.java', 'Bar.java', 'Baz.java'] + await assertIsTestFile(camelCaseSrc, { languageId: 'java' }, false) + + const camelCaseTst = ['FooTest.java', 'BarTests.java'] + await assertIsTestFile(camelCaseTst, { languageId: 'java' }, true) + + const snakeCaseSrc = ['foo.py', 'bar.py'] + await assertIsTestFile(snakeCaseSrc, { languageId: 'python' }, false) + + const snakeCaseTst = ['test_foo.py', 'bar_test.py'] + await assertIsTestFile(snakeCaseTst, { languageId: 'python' }, true) + + const javascriptSrc = ['Foo.js', 'bar.js'] + await assertIsTestFile(javascriptSrc, { languageId: 'javascript' }, false) + + const javascriptTst = ['Foo.test.js', 'Bar.spec.js'] + await assertIsTestFile(javascriptTst, { languageId: 'javascript' }, true) + + const typescriptSrc = ['Foo.ts', 'bar.ts'] + await assertIsTestFile(typescriptSrc, { languageId: 'typescript' }, false) + + const typescriptTst = ['Foo.test.ts', 'Bar.spec.ts'] + await assertIsTestFile(typescriptTst, { languageId: 'typescript' }, true) + + const jsxSrc = ['Foo.jsx', 'Bar.jsx'] + await assertIsTestFile(jsxSrc, { languageId: 'javascriptreact' }, false) + + const jsxTst = ['Foo.test.jsx', 'Bar.spec.jsx'] + await assertIsTestFile(jsxTst, { languageId: 'javascriptreact' }, true) + }) + + it('should return true if the file name matches the test filename pattern - Java', async () => { + const filePaths = ['/path/to/MyClassTest.java', '/path/to/TestMyClass.java', '/path/to/MyClassTests.java'] + const language = 'java' + + for (const filePath of filePaths) { + const result = await isTestFile(filePath, { languageId: language }) + assert.strictEqual(result, true) + } + }) + + it('should return false if the file name does not match the test filename pattern - Java', async () => { + const filePaths = ['/path/to/MyClass.java', '/path/to/MyClass_test.java', '/path/to/test_MyClass.java'] + const language = 'java' + + for (const filePath of filePaths) { + const result = await isTestFile(filePath, { languageId: language }) + assert.strictEqual(result, false) + } + }) + + it('should return true if the file name does not match the test filename pattern - Python', async () => { + const filePaths = ['/path/to/util_test.py', '/path/to/test_util.py'] + const language = 'python' + + for (const filePath of filePaths) { + const result = await isTestFile(filePath, { languageId: language }) + assert.strictEqual(result, true) + } + }) + + it('should return false if the file name does not match the test filename pattern - Python', async () => { + const filePaths = ['/path/to/util.py', '/path/to/utilTest.java', '/path/to/Testutil.java'] + const language = 'python' + + for (const filePath of filePaths) { + const result = await isTestFile(filePath, { languageId: language }) + assert.strictEqual(result, false) + } + }) + + it('should return false if the language is not supported', async () => { + const filePath = '/path/to/MyClass.cpp' + const language = 'c++' + const result = await isTestFile(filePath, { languageId: language }) + assert.strictEqual(result, false) + }) +}) + +const pythonFileContent = ` +# Single-line import statements +import os +import numpy as np +from typing import List, Tuple + +# Multi-line import statements +from collections import ( + defaultdict, + Counter +) + +# Relative imports +from . import module1 +from ..subpackage import module2 + +# Wildcard imports +from mypackage import * +from mypackage.module import * + +# Aliased imports +import pandas as pd +from mypackage import module1 as m1, module2 as m2 + +def hello_world(): + print("Hello, world!") + +def add_numbers(x, y): + return x + y + +def multiply_numbers(x=1, y=1): + return x * y + +def sum_numbers(*args): + total = 0 + for num in args: + total += num + return total + +def divide_numbers(x, y=1, *args, **kwargs): + result = x / y + for arg in args: + result /= arg + for _, value in kwargs.items(): + result /= value + return result + +class Calculator: + def __init__(self, x, y): + self.x = x + self.y = y + + def add(self): + return self.x + self.y + + def multiply(self): + return self.x * self.y + + @staticmethod + def square(x): + return x ** 2 + + @classmethod + def from_sum(cls, x, y): + return cls(x+y, 0) + + class InnerClass: + def __init__(self, z): + self.z = z + + def triple(self): + return self.z * 3 + +def main(): + print(hello_world()) + print(add_numbers(3, 5)) + print(multiply_numbers(3, 5)) + print(sum_numbers(1, 2, 3, 4, 5)) + print(divide_numbers(10, 2, 5, 2, a=2, b=3)) + + calc = Calculator(3, 5) + print(calc.add()) + print(calc.multiply()) + print(Calculator.square(3)) + print(Calculator.from_sum(2, 3).add()) + + inner = Calculator.InnerClass(5) + print(inner.triple()) + +if __name__ == "__main__": + main() +` + +const javaFileContent = ` +@Annotation +public class Test { + Test() { + // Do something here + } + + //Additional commenting + public static void sayHello() { + System.out.println("Hello, World!"); + } + + private void doSomething(int x, int y) throws Exception { + int z = x + y; + System.out.println("The sum of " + x + " and " + y + " is " + z); + } + + protected static int square(int x) { + return x * x; + } + + private static void manager(int a, int b) { + return a+b; + } + + public int ABCFUNCTION( int ABC, int PQR) { + return ABC + PQR; + } +}` diff --git a/packages/amazonq/test/unit/codewhisperer/util/commonUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/commonUtil.test.ts new file mode 100644 index 00000000000..5694b33365d --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/util/commonUtil.test.ts @@ -0,0 +1,81 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { + JsonConfigFileNamingConvention, + checkLeftContextKeywordsForJson, + getPrefixSuffixOverlap, +} from 'aws-core-vscode/codewhisperer' + +describe('commonUtil', function () { + describe('getPrefixSuffixOverlap', function () { + it('Should return correct overlap', async function () { + assert.strictEqual(getPrefixSuffixOverlap('32rasdgvdsg', 'sg462ydfgbs'), `sg`) + assert.strictEqual(getPrefixSuffixOverlap('32rasdgbreh', 'brehsega'), `breh`) + assert.strictEqual(getPrefixSuffixOverlap('42y24hsd', '42y24hsdzqq23'), `42y24hsd`) + assert.strictEqual(getPrefixSuffixOverlap('ge23yt1', 'ge23yt1'), `ge23yt1`) + assert.strictEqual(getPrefixSuffixOverlap('1sgdbsfbwsergsa', 'a1sgdbsfbwsergs'), `a`) + assert.strictEqual(getPrefixSuffixOverlap('xxa', 'xa'), `xa`) + }) + + it('Should return empty overlap for prefix suffix not matching cases', async function () { + assert.strictEqual(getPrefixSuffixOverlap('1sgdbsfbwsergsa', '1sgdbsfbwsergs'), ``) + assert.strictEqual(getPrefixSuffixOverlap('1sgdbsfbwsergsab', '1sgdbsfbwsergs'), ``) + assert.strictEqual(getPrefixSuffixOverlap('2135t12', 'v2135t12'), ``) + assert.strictEqual(getPrefixSuffixOverlap('2135t12', 'zv2135t12'), ``) + assert.strictEqual(getPrefixSuffixOverlap('xa', 'xxa'), ``) + }) + + it('Should return empty overlap for empty string input', async function () { + assert.strictEqual(getPrefixSuffixOverlap('ergwsghws', ''), ``) + assert.strictEqual(getPrefixSuffixOverlap('', 'asfegw4eh'), ``) + }) + }) + + describe('checkLeftContextKeywordsForJson', function () { + it('Should return true for valid left context keywords', async function () { + assert.strictEqual( + checkLeftContextKeywordsForJson('foo.json', 'Create an S3 Bucket named CodeWhisperer', 'json'), + true + ) + }) + it('Should return false for invalid left context keywords', async function () { + assert.strictEqual( + checkLeftContextKeywordsForJson( + 'foo.json', + 'Create an S3 Bucket named CodeWhisperer in Cloudformation', + 'json' + ), + false + ) + }) + + for (const jsonConfigFile of JsonConfigFileNamingConvention) { + it(`should evalute by filename ${jsonConfigFile}`, function () { + assert.strictEqual(checkLeftContextKeywordsForJson(jsonConfigFile, 'foo', 'json'), false) + + assert.strictEqual(checkLeftContextKeywordsForJson(jsonConfigFile.toUpperCase(), 'bar', 'json'), false) + + assert.strictEqual(checkLeftContextKeywordsForJson(jsonConfigFile.toUpperCase(), 'baz', 'json'), false) + }) + + const upperCaseFilename = jsonConfigFile.toUpperCase() + it(`should evalute by filename and case insensitive ${upperCaseFilename}`, function () { + assert.strictEqual(checkLeftContextKeywordsForJson(upperCaseFilename, 'foo', 'json'), false) + + assert.strictEqual( + checkLeftContextKeywordsForJson(upperCaseFilename.toUpperCase(), 'bar', 'json'), + false + ) + + assert.strictEqual( + checkLeftContextKeywordsForJson(upperCaseFilename.toUpperCase(), 'baz', 'json'), + false + ) + }) + } + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/crossFileContextUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/crossFileContextUtil.test.ts new file mode 100644 index 00000000000..4c2ca1190ca --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/util/crossFileContextUtil.test.ts @@ -0,0 +1,417 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as FakeTimers from '@sinonjs/fake-timers' +import * as vscode from 'vscode' +import * as sinon from 'sinon' +import * as crossFile from 'aws-core-vscode/codewhisperer' +import { + aLongStringWithLineCount, + aStringWithLineCount, + createMockTextEditor, + installFakeClock, +} from 'aws-core-vscode/test' +import { FeatureConfigProvider, crossFileContextConfig } from 'aws-core-vscode/codewhisperer' +import { + assertTabCount, + closeAllEditors, + createTestWorkspaceFolder, + toTextEditor, + shuffleList, + toFile, +} from 'aws-core-vscode/test' +import { areEqual, normalize } from 'aws-core-vscode/shared' +import * as path from 'path' + +let tempFolder: string + +describe('crossFileContextUtil', function () { + const fakeCancellationToken: vscode.CancellationToken = { + isCancellationRequested: false, + onCancellationRequested: sinon.spy(), + } + + let mockEditor: vscode.TextEditor + let clock: FakeTimers.InstalledClock + + before(function () { + clock = installFakeClock() + }) + + after(function () { + clock.uninstall() + }) + + afterEach(function () { + sinon.restore() + }) + + describe('fetchSupplementalContextForSrc', function () { + beforeEach(async function () { + tempFolder = (await createTestWorkspaceFolder()).uri.fsPath + }) + + afterEach(async function () { + sinon.restore() + }) + + it.skip('for control group, should return opentabs context where there will be 3 chunks and each chunk should contains 50 lines', async function () { + sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').returns('control') + await toTextEditor(aStringWithLineCount(200), 'CrossFile.java', tempFolder, { preview: false }) + const myCurrentEditor = await toTextEditor('', 'TargetFile.java', tempFolder, { + preview: false, + }) + + await assertTabCount(2) + + const actual = await crossFile.fetchSupplementalContextForSrc(myCurrentEditor, fakeCancellationToken) + assert.ok(actual) + assert.strictEqual(actual.supplementalContextItems.length, 3) + assert.strictEqual(actual.supplementalContextItems[0].content.split('\n').length, 50) + assert.strictEqual(actual.supplementalContextItems[1].content.split('\n').length, 50) + assert.strictEqual(actual.supplementalContextItems[2].content.split('\n').length, 50) + }) + + it('for t1 group, should return repomap + opentabs context, should not exceed 20k total length', async function () { + await toTextEditor(aLongStringWithLineCount(200), 'CrossFile.java', tempFolder, { preview: false }) + const myCurrentEditor = await toTextEditor('', 'TargetFile.java', tempFolder, { + preview: false, + }) + + await assertTabCount(2) + + sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').returns('t1') + + const mockLanguageClient = { + sendRequest: sinon.stub().resolves([ + { + content: 'foo'.repeat(3000), + score: 0, + filePath: 'q-inline', + }, + ]), + } as any + + const actual = await crossFile.fetchSupplementalContextForSrc( + myCurrentEditor, + fakeCancellationToken, + mockLanguageClient + ) + assert.ok(actual) + assert.strictEqual(actual.supplementalContextItems.length, 3) + assert.strictEqual(actual?.strategy, 'codemap') + assert.deepEqual(actual?.supplementalContextItems[0], { + content: 'foo'.repeat(3000), + score: 0, + filePath: 'q-inline', + }) + assert.strictEqual(actual.supplementalContextItems[1].content.split('\n').length, 50) + assert.strictEqual(actual.supplementalContextItems[2].content.split('\n').length, 50) + }) + + it.skip('for t2 group, should return global bm25 context and no repomap', async function () { + await toTextEditor(aStringWithLineCount(200), 'CrossFile.java', tempFolder, { preview: false }) + const myCurrentEditor = await toTextEditor('', 'TargetFile.java', tempFolder, { + preview: false, + }) + + await assertTabCount(2) + + sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').returns('t2') + + const actual = await crossFile.fetchSupplementalContextForSrc(myCurrentEditor, fakeCancellationToken) + assert.ok(actual) + assert.strictEqual(actual.supplementalContextItems.length, 5) + assert.strictEqual(actual?.strategy, 'bm25') + + assert.deepEqual(actual?.supplementalContextItems[0], { + content: 'foo', + score: 5, + filePath: 'foo.java', + }) + + assert.deepEqual(actual?.supplementalContextItems[1], { + content: 'bar', + score: 4, + filePath: 'bar.java', + }) + assert.deepEqual(actual?.supplementalContextItems[2], { + content: 'baz', + score: 3, + filePath: 'baz.java', + }) + + assert.deepEqual(actual?.supplementalContextItems[3], { + content: 'qux', + score: 2, + filePath: 'qux.java', + }) + + assert.deepEqual(actual?.supplementalContextItems[4], { + content: 'quux', + score: 1, + filePath: 'quux.java', + }) + }) + }) + + describe('non supported language should return undefined', function () { + it('c++', async function () { + mockEditor = createMockTextEditor('content', 'fileName', 'cpp') + const actual = await crossFile.fetchSupplementalContextForSrc(mockEditor, fakeCancellationToken) + assert.strictEqual(actual, undefined) + }) + + it('ruby', async function () { + mockEditor = createMockTextEditor('content', 'fileName', 'ruby') + + const actual = await crossFile.fetchSupplementalContextForSrc(mockEditor, fakeCancellationToken) + + assert.strictEqual(actual, undefined) + }) + }) + + describe('getCrossFileCandidate', function () { + before(async function () { + this.timeout(60000) + }) + + beforeEach(async function () { + tempFolder = (await createTestWorkspaceFolder()).uri.fsPath + }) + + afterEach(async function () { + await closeAllEditors() + }) + + it('should return opened files, exclude test files and sorted ascendingly by file distance', async function () { + const targetFile = path.join('src', 'service', 'microService', 'CodeWhispererFileContextProvider.java') + const fileWithDistance3 = path.join('src', 'service', 'CodewhispererRecommendationService.java') + const fileWithDistance5 = path.join('src', 'util', 'CodeWhispererConstants.java') + const fileWithDistance6 = path.join('src', 'ui', 'popup', 'CodeWhispererPopupManager.java') + const fileWithDistance7 = path.join('src', 'ui', 'popup', 'components', 'CodeWhispererPopup.java') + const fileWithDistance8 = path.join( + 'src', + 'ui', + 'popup', + 'components', + 'actions', + 'AcceptRecommendationAction.java' + ) + const testFile1 = path.join('test', 'service', 'CodeWhispererFileContextProviderTest.java') + const testFile2 = path.join('test', 'ui', 'CodeWhispererPopupManagerTest.java') + + const expectedFilePaths = [ + fileWithDistance3, + fileWithDistance5, + fileWithDistance6, + fileWithDistance7, + fileWithDistance8, + ] + + const shuffledFilePaths = shuffleList(expectedFilePaths) + + for (const filePath of shuffledFilePaths) { + await toTextEditor('', filePath, tempFolder, { preview: false }) + } + + await toTextEditor('', testFile1, tempFolder, { preview: false }) + await toTextEditor('', testFile2, tempFolder, { preview: false }) + const editor = await toTextEditor('', targetFile, tempFolder, { preview: false }) + + await assertTabCount(shuffledFilePaths.length + 3) + + const actual = await crossFile.getCrossFileCandidates(editor) + + assert.ok(actual.length === 5) + for (const [index, actualFile] of actual.entries()) { + const expectedFile = path.join(tempFolder, expectedFilePaths[index]) + assert.strictEqual(normalize(expectedFile), normalize(actualFile)) + assert.ok(areEqual(tempFolder, actualFile, expectedFile)) + } + }) + }) + + describe.skip('partial support - control group', function () { + const fileExtLists: string[] = [] + + before(async function () { + this.timeout(60000) + }) + + beforeEach(async function () { + tempFolder = (await createTestWorkspaceFolder()).uri.fsPath + }) + + afterEach(async function () { + await closeAllEditors() + }) + + for (const fileExt of fileExtLists) { + it('should be empty if userGroup is control', async function () { + const editor = await toTextEditor('content-1', `file-1.${fileExt}`, tempFolder) + await toTextEditor('content-2', `file-2.${fileExt}`, tempFolder, { preview: false }) + await toTextEditor('content-3', `file-3.${fileExt}`, tempFolder, { preview: false }) + await toTextEditor('content-4', `file-4.${fileExt}`, tempFolder, { preview: false }) + + await assertTabCount(4) + + const actual = await crossFile.fetchSupplementalContextForSrc(editor, fakeCancellationToken) + + assert.ok(actual && actual.supplementalContextItems.length === 0) + }) + } + }) + + describe.skip('partial support - crossfile group', function () { + const fileExtLists: string[] = [] + + before(async function () { + this.timeout(60000) + }) + + beforeEach(async function () { + tempFolder = (await createTestWorkspaceFolder()).uri.fsPath + }) + + afterEach(async function () { + await closeAllEditors() + }) + + for (const fileExt of fileExtLists) { + it('should be non empty if usergroup is Crossfile', async function () { + const editor = await toTextEditor('content-1', `file-1.${fileExt}`, tempFolder) + await toTextEditor('content-2', `file-2.${fileExt}`, tempFolder, { preview: false }) + await toTextEditor('content-3', `file-3.${fileExt}`, tempFolder, { preview: false }) + await toTextEditor('content-4', `file-4.${fileExt}`, tempFolder, { preview: false }) + + await assertTabCount(4) + + const actual = await crossFile.fetchSupplementalContextForSrc(editor, fakeCancellationToken) + + assert.ok(actual && actual.supplementalContextItems.length !== 0) + }) + } + }) + + describe('full support', function () { + const fileExtLists = ['java', 'js', 'ts', 'py', 'tsx', 'jsx'] + + before(async function () { + this.timeout(60000) + }) + + beforeEach(async function () { + tempFolder = (await createTestWorkspaceFolder()).uri.fsPath + }) + + afterEach(async function () { + sinon.restore() + await closeAllEditors() + }) + + for (const fileExt of fileExtLists) { + it(`supplemental context for file ${fileExt} should be non empty`, async function () { + sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').returns('control') + const editor = await toTextEditor('content-1', `file-1.${fileExt}`, tempFolder) + await toTextEditor('content-2', `file-2.${fileExt}`, tempFolder, { preview: false }) + await toTextEditor('content-3', `file-3.${fileExt}`, tempFolder, { preview: false }) + await toTextEditor('content-4', `file-4.${fileExt}`, tempFolder, { preview: false }) + + await assertTabCount(4) + + const actual = await crossFile.fetchSupplementalContextForSrc(editor, fakeCancellationToken) + + assert.ok(actual && actual.supplementalContextItems.length !== 0) + }) + } + }) + + describe('splitFileToChunks', function () { + beforeEach(async function () { + tempFolder = (await createTestWorkspaceFolder()).uri.fsPath + }) + + it('should split file to a chunk of 2 lines', async function () { + const filePath = path.join(tempFolder, 'file.txt') + await toFile('line_1\nline_2\nline_3\nline_4\nline_5\nline_6\nline_7', filePath) + + const chunks = await crossFile.splitFileToChunks(filePath, 2) + + assert.strictEqual(chunks.length, 4) + assert.strictEqual(chunks[0].content, 'line_1\nline_2') + assert.strictEqual(chunks[1].content, 'line_3\nline_4') + assert.strictEqual(chunks[2].content, 'line_5\nline_6') + assert.strictEqual(chunks[3].content, 'line_7') + }) + + it('should split file to a chunk of 5 lines', async function () { + const filePath = path.join(tempFolder, 'file.txt') + await toFile('line_1\nline_2\nline_3\nline_4\nline_5\nline_6\nline_7', filePath) + + const chunks = await crossFile.splitFileToChunks(filePath, 5) + + assert.strictEqual(chunks.length, 2) + assert.strictEqual(chunks[0].content, 'line_1\nline_2\nline_3\nline_4\nline_5') + assert.strictEqual(chunks[1].content, 'line_6\nline_7') + }) + + it('codewhisperer crossfile config should use 50 lines', async function () { + const filePath = path.join(tempFolder, 'file.txt') + await toFile(aStringWithLineCount(210), filePath) + + const chunks = await crossFile.splitFileToChunks(filePath, crossFileContextConfig.numberOfLinesEachChunk) + + // (210 / 50) + 1 + assert.strictEqual(chunks.length, 5) + // line0 -> line49 + assert.strictEqual(chunks[0].content, aStringWithLineCount(50, 0)) + // line50 -> line99 + assert.strictEqual(chunks[1].content, aStringWithLineCount(50, 50)) + // line100 -> line149 + assert.strictEqual(chunks[2].content, aStringWithLineCount(50, 100)) + // line150 -> line199 + assert.strictEqual(chunks[3].content, aStringWithLineCount(50, 150)) + // line 200 -> line209 + assert.strictEqual(chunks[4].content, aStringWithLineCount(10, 200)) + }) + + it('linkChunks should add another chunk which will link to the first chunk and chunk.nextContent should reflect correct value', async function () { + const filePath = path.join(tempFolder, 'file.txt') + await toFile(aStringWithLineCount(210), filePath) + + const chunks = await crossFile.splitFileToChunks(filePath, crossFileContextConfig.numberOfLinesEachChunk) + const linkedChunks = crossFile.linkChunks(chunks) + + // 210 / 50 + 2 + assert.strictEqual(linkedChunks.length, 6) + + // 0th + assert.strictEqual(linkedChunks[0].content, aStringWithLineCount(3, 0)) + assert.strictEqual(linkedChunks[0].nextContent, aStringWithLineCount(50, 0)) + + // 1st + assert.strictEqual(linkedChunks[1].content, aStringWithLineCount(50, 0)) + assert.strictEqual(linkedChunks[1].nextContent, aStringWithLineCount(50, 50)) + + // 2nd + assert.strictEqual(linkedChunks[2].content, aStringWithLineCount(50, 50)) + assert.strictEqual(linkedChunks[2].nextContent, aStringWithLineCount(50, 100)) + + // 3rd + assert.strictEqual(linkedChunks[3].content, aStringWithLineCount(50, 100)) + assert.strictEqual(linkedChunks[3].nextContent, aStringWithLineCount(50, 150)) + + // 4th + assert.strictEqual(linkedChunks[4].content, aStringWithLineCount(50, 150)) + assert.strictEqual(linkedChunks[4].nextContent, aStringWithLineCount(10, 200)) + + // 5th + assert.strictEqual(linkedChunks[5].content, aStringWithLineCount(10, 200)) + assert.strictEqual(linkedChunks[5].nextContent, aStringWithLineCount(10, 200)) + }) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/editorContext.test.ts b/packages/amazonq/test/unit/codewhisperer/util/editorContext.test.ts new file mode 100644 index 00000000000..3875dbbd0f2 --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/util/editorContext.test.ts @@ -0,0 +1,392 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import assert from 'assert' +import * as codewhispererClient from 'aws-core-vscode/codewhisperer' +import * as EditorContext from 'aws-core-vscode/codewhisperer' +import { + createMockDocument, + createMockTextEditor, + createMockClientRequest, + resetCodeWhispererGlobalVariables, + toTextEditor, + createTestWorkspaceFolder, + closeAllEditors, +} from 'aws-core-vscode/test' +import { globals } from 'aws-core-vscode/shared' +import { GenerateCompletionsRequest } from 'aws-core-vscode/codewhisperer' +import * as vscode from 'vscode' + +export function createNotebookCell( + document: vscode.TextDocument = createMockDocument('def example():\n return "test"'), + kind: vscode.NotebookCellKind = vscode.NotebookCellKind.Code, + notebook: vscode.NotebookDocument = {} as any, + index: number = 0, + outputs: vscode.NotebookCellOutput[] = [], + metadata: { readonly [key: string]: any } = {}, + executionSummary?: vscode.NotebookCellExecutionSummary +): vscode.NotebookCell { + return { + document, + kind, + notebook, + index, + outputs, + metadata, + executionSummary, + } +} + +describe('editorContext', function () { + let telemetryEnabledDefault: boolean + let tempFolder: string + + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + telemetryEnabledDefault = globals.telemetry.telemetryEnabled + }) + + afterEach(async function () { + await globals.telemetry.setTelemetryEnabled(telemetryEnabledDefault) + }) + + describe('extractContextForCodeWhisperer', function () { + it('Should return expected context', function () { + const editor = createMockTextEditor('import math\ndef two_sum(nums, target):\n', 'test.py', 'python', 1, 17) + const actual = EditorContext.extractContextForCodeWhisperer(editor) + const expected: codewhispererClient.FileContext = { + fileUri: 'file:///test.py', + filename: 'test.py', + programmingLanguage: { + languageName: 'python', + }, + leftFileContent: 'import math\ndef two_sum(nums,', + rightFileContent: ' target):\n', + } + assert.deepStrictEqual(actual, expected) + }) + + it('Should return expected context within max char limit', function () { + const editor = createMockTextEditor( + 'import math\ndef ' + 'a'.repeat(10340) + 'two_sum(nums, target):\n', + 'test.py', + 'python', + 1, + 17 + ) + const actual = EditorContext.extractContextForCodeWhisperer(editor) + const expected: codewhispererClient.FileContext = { + fileUri: 'file:///test.py', + filename: 'test.py', + programmingLanguage: { + languageName: 'python', + }, + leftFileContent: 'import math\ndef aaaaaaaaaaaaa', + rightFileContent: 'a'.repeat(10240), + } + assert.deepStrictEqual(actual, expected) + }) + + it('in a notebook, includes context from other cells', async function () { + const cells: vscode.NotebookCellData[] = [ + new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, 'Previous cell', 'python'), + new vscode.NotebookCellData( + vscode.NotebookCellKind.Code, + 'import numpy as np\nimport pandas as pd\n\ndef analyze_data(df):\n # Current cell with cursor here', + 'python' + ), + new vscode.NotebookCellData( + vscode.NotebookCellKind.Code, + '# Process the data\nresult = analyze_data(df)\nprint(result)', + 'python' + ), + ] + + const document = await vscode.workspace.openNotebookDocument( + 'jupyter-notebook', + new vscode.NotebookData(cells) + ) + const editor: any = { + document: document.cellAt(1).document, + selection: { active: new vscode.Position(4, 13) }, + } + + const actual = EditorContext.extractContextForCodeWhisperer(editor) + const expected: codewhispererClient.FileContext = { + fileUri: editor.document.uri.toString(), + filename: 'Untitled-1.py', + programmingLanguage: { + languageName: 'python', + }, + leftFileContent: + '# Previous cell\nimport numpy as np\nimport pandas as pd\n\ndef analyze_data(df):\n # Current', + rightFileContent: + ' cell with cursor here\n# Process the data\nresult = analyze_data(df)\nprint(result)\n', + } + assert.deepStrictEqual(actual, expected) + }) + }) + + describe('getFileName', function () { + it('Should return expected filename given a document reading test.py', function () { + const editor = createMockTextEditor('', 'test.py', 'python', 1, 17) + const actual = EditorContext.getFileName(editor) + const expected = 'test.py' + assert.strictEqual(actual, expected) + }) + + it('Should return expected filename for a long filename', async function () { + const editor = createMockTextEditor('', 'a'.repeat(1500), 'python', 1, 17) + const actual = EditorContext.getFileName(editor) + const expected = 'a'.repeat(1024) + assert.strictEqual(actual, expected) + }) + }) + + describe('getFileRelativePath', function () { + this.beforeEach(async function () { + tempFolder = (await createTestWorkspaceFolder()).uri.fsPath + }) + + it('Should return a new filename with correct extension given a .ipynb file', function () { + const languageToExtension = new Map([ + ['python', 'py'], + ['rust', 'rs'], + ['javascript', 'js'], + ['typescript', 'ts'], + ['c', 'c'], + ]) + + for (const [language, extension] of languageToExtension.entries()) { + const editor = createMockTextEditor('', 'test.ipynb', language, 1, 17) + const actual = EditorContext.getFileRelativePath(editor) + const expected = 'test.' + extension + assert.strictEqual(actual, expected) + } + }) + + it('Should return relative path', async function () { + const editor = await toTextEditor('tttt', 'test.py', tempFolder) + const actual = EditorContext.getFileRelativePath(editor) + const expected = 'test.py' + assert.strictEqual(actual, expected) + }) + + afterEach(async function () { + await closeAllEditors() + }) + }) + + describe('getNotebookCellContext', function () { + it('Should return cell text for python code cells when language is python', function () { + const mockCodeCell = createNotebookCell(createMockDocument('def example():\n return "test"')) + const result = EditorContext.getNotebookCellContext(mockCodeCell, 'python') + assert.strictEqual(result, 'def example():\n return "test"') + }) + + it('Should return java comments for python code cells when language is java', function () { + const mockCodeCell = createNotebookCell(createMockDocument('def example():\n return "test"')) + const result = EditorContext.getNotebookCellContext(mockCodeCell, 'java') + assert.strictEqual(result, '// def example():\n// return "test"') + }) + + it('Should return python comments for java code cells when language is python', function () { + const mockCodeCell = createNotebookCell(createMockDocument('println(1 + 1);', 'somefile.ipynb', 'java')) + const result = EditorContext.getNotebookCellContext(mockCodeCell, 'python') + assert.strictEqual(result, '# println(1 + 1);') + }) + + it('Should add python comment prefixes for markdown cells when language is python', function () { + const mockMarkdownCell = createNotebookCell( + createMockDocument('# Heading\nThis is a markdown cell'), + vscode.NotebookCellKind.Markup + ) + const result = EditorContext.getNotebookCellContext(mockMarkdownCell, 'python') + assert.strictEqual(result, '# # Heading\n# This is a markdown cell') + }) + + it('Should add java comment prefixes for markdown cells when language is java', function () { + const mockMarkdownCell = createNotebookCell( + createMockDocument('# Heading\nThis is a markdown cell'), + vscode.NotebookCellKind.Markup + ) + const result = EditorContext.getNotebookCellContext(mockMarkdownCell, 'java') + assert.strictEqual(result, '// # Heading\n// This is a markdown cell') + }) + }) + + describe('getNotebookCellsSliceContext', function () { + it('Should extract content from cells in reverse order up to maxLength from prefix cells', function () { + const mockCells = [ + createNotebookCell(createMockDocument('First cell content')), + createNotebookCell(createMockDocument('Second cell content')), + createNotebookCell(createMockDocument('Third cell content')), + ] + + const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'python', false) + assert.strictEqual(result, 'First cell content\nSecond cell content\nThird cell content\n') + }) + + it('Should extract content from cells in reverse order up to maxLength from suffix cells', function () { + const mockCells = [ + createNotebookCell(createMockDocument('First cell content')), + createNotebookCell(createMockDocument('Second cell content')), + createNotebookCell(createMockDocument('Third cell content')), + ] + + const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'python', true) + assert.strictEqual(result, 'First cell content\nSecond cell content\nThird cell content\n') + }) + + it('Should respect maxLength parameter from prefix cells', function () { + const mockCells = [ + createNotebookCell(createMockDocument('First')), + createNotebookCell(createMockDocument('Second')), + createNotebookCell(createMockDocument('Third')), + createNotebookCell(createMockDocument('Fourth')), + ] + // Should only include part of second cell and the last two cells + const result = EditorContext.getNotebookCellsSliceContext(mockCells, 15, 'python', false) + assert.strictEqual(result, 'd\nThird\nFourth\n') + }) + + it('Should respect maxLength parameter from suffix cells', function () { + const mockCells = [ + createNotebookCell(createMockDocument('First')), + createNotebookCell(createMockDocument('Second')), + createNotebookCell(createMockDocument('Third')), + createNotebookCell(createMockDocument('Fourth')), + ] + + // Should only include first cell and part of second cell + const result = EditorContext.getNotebookCellsSliceContext(mockCells, 15, 'python', true) + assert.strictEqual(result, 'First\nSecond\nTh') + }) + + it('Should handle empty cells array from prefix cells', function () { + const result = EditorContext.getNotebookCellsSliceContext([], 100, 'python', false) + assert.strictEqual(result, '') + }) + + it('Should handle empty cells array from suffix cells', function () { + const result = EditorContext.getNotebookCellsSliceContext([], 100, 'python', true) + assert.strictEqual(result, '') + }) + + it('Should add python comments to markdown prefix cells', function () { + const mockCells = [ + createNotebookCell(createMockDocument('# Heading\nThis is markdown'), vscode.NotebookCellKind.Markup), + createNotebookCell(createMockDocument('def example():\n return "test"')), + ] + const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'python', false) + assert.strictEqual(result, '# # Heading\n# This is markdown\ndef example():\n return "test"\n') + }) + + it('Should add python comments to markdown suffix cells', function () { + const mockCells = [ + createNotebookCell(createMockDocument('# Heading\nThis is markdown'), vscode.NotebookCellKind.Markup), + createNotebookCell(createMockDocument('def example():\n return "test"')), + ] + + const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'python', true) + assert.strictEqual(result, '# # Heading\n# This is markdown\ndef example():\n return "test"\n') + }) + + it('Should add java comments to markdown and python prefix cells when language is java', function () { + const mockCells = [ + createNotebookCell(createMockDocument('# Heading\nThis is markdown'), vscode.NotebookCellKind.Markup), + createNotebookCell(createMockDocument('def example():\n return "test"')), + ] + const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'java', false) + assert.strictEqual(result, '// # Heading\n// This is markdown\n// def example():\n// return "test"\n') + }) + + it('Should add java comments to markdown and python suffix cells when language is java', function () { + const mockCells = [ + createNotebookCell(createMockDocument('# Heading\nThis is markdown'), vscode.NotebookCellKind.Markup), + createNotebookCell(createMockDocument('println(1 + 1);', 'somefile.ipynb', 'java')), + ] + + const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'java', true) + assert.strictEqual(result, '// # Heading\n// This is markdown\nprintln(1 + 1);\n') + }) + + it('Should handle code prefix cells with different languages', function () { + const mockCells = [ + createNotebookCell( + createMockDocument('println(1 + 1);', 'somefile.ipynb', 'java'), + vscode.NotebookCellKind.Code + ), + createNotebookCell(createMockDocument('def example():\n return "test"')), + ] + const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'python', false) + assert.strictEqual(result, '# println(1 + 1);\ndef example():\n return "test"\n') + }) + + it('Should handle code suffix cells with different languages', function () { + const mockCells = [ + createNotebookCell( + createMockDocument('println(1 + 1);', 'somefile.ipynb', 'java'), + vscode.NotebookCellKind.Code + ), + createNotebookCell(createMockDocument('def example():\n return "test"')), + ] + const result = EditorContext.getNotebookCellsSliceContext(mockCells, 100, 'python', true) + assert.strictEqual(result, '# println(1 + 1);\ndef example():\n return "test"\n') + }) + }) + + describe('validateRequest', function () { + it('Should return false if request filename.length is invalid', function () { + const req = createMockClientRequest() + req.fileContext.filename = '' + assert.ok(!EditorContext.validateRequest(req)) + }) + + it('Should return false if request programming language is invalid', function () { + const req = createMockClientRequest() + req.fileContext.programmingLanguage.languageName = '' + assert.ok(!EditorContext.validateRequest(req)) + req.fileContext.programmingLanguage.languageName = 'a'.repeat(200) + assert.ok(!EditorContext.validateRequest(req)) + }) + + it('Should return false if request left or right context exceeds max length', function () { + const req = createMockClientRequest() + req.fileContext.leftFileContent = 'a'.repeat(256000) + assert.ok(!EditorContext.validateRequest(req)) + req.fileContext.leftFileContent = 'a' + req.fileContext.rightFileContent = 'a'.repeat(256000) + assert.ok(!EditorContext.validateRequest(req)) + }) + + it('Should return true if above conditions are not met', function () { + const req = createMockClientRequest() + assert.ok(EditorContext.validateRequest(req)) + }) + }) + + describe('getLeftContext', function () { + it('Should return expected left context', function () { + const editor = createMockTextEditor('import math\ndef two_sum(nums, target):\n', 'test.py', 'python', 1, 17) + const actual = EditorContext.getLeftContext(editor, 1) + const expected = '...wo_sum(nums, target)' + assert.strictEqual(actual, expected) + }) + }) + + describe('buildListRecommendationRequest', function () { + it('Should return expected fields for optOut, nextToken and reference config', async function () { + const nextToken = 'testToken' + const optOutPreference = false + await globals.telemetry.setTelemetryEnabled(false) + const editor = createMockTextEditor('import math\ndef two_sum(nums, target):\n', 'test.py', 'python', 1, 17) + const actual = await EditorContext.buildListRecommendationRequest(editor, nextToken, optOutPreference) + + assert.strictEqual(actual.request.nextToken, nextToken) + assert.strictEqual((actual.request as GenerateCompletionsRequest).optOutPreference, 'OPTOUT') + assert.strictEqual(actual.request.referenceTrackerConfiguration?.recommendationsWithReferences, 'BLOCK') + }) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/globalStateUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/globalStateUtil.test.ts new file mode 100644 index 00000000000..24062a81b7c --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/util/globalStateUtil.test.ts @@ -0,0 +1,42 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import { resetCodeWhispererGlobalVariables } from 'aws-core-vscode/test' +import { getLogger } from 'aws-core-vscode/shared' +import { resetIntelliSenseState, vsCodeState } from 'aws-core-vscode/codewhisperer' + +describe('globalStateUtil', function () { + let loggerSpy: sinon.SinonSpy + + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + vsCodeState.isIntelliSenseActive = true + loggerSpy = sinon.spy(getLogger(), 'info') + }) + + this.afterEach(function () { + sinon.restore() + }) + + it('Should skip when CodeWhisperer is turned off', async function () { + const isManualTriggerEnabled = false + const isAutomatedTriggerEnabled = false + resetIntelliSenseState(isManualTriggerEnabled, isAutomatedTriggerEnabled, true) + assert.ok(!loggerSpy.called) + }) + + it('Should skip when invocationContext is not active', async function () { + vsCodeState.isIntelliSenseActive = false + resetIntelliSenseState(false, false, true) + assert.ok(!loggerSpy.called) + }) + + it('Should skip when no valid recommendations', async function () { + resetIntelliSenseState(true, true, false) + assert.ok(!loggerSpy.called) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/util/supplemetalContextUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/supplemetalContextUtil.test.ts new file mode 100644 index 00000000000..cf2fd151262 --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/util/supplemetalContextUtil.test.ts @@ -0,0 +1,254 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as FakeTimers from '@sinonjs/fake-timers' +import * as vscode from 'vscode' +import * as sinon from 'sinon' +import * as os from 'os' +import * as crossFile from 'aws-core-vscode/codewhisperer' +import { TestFolder, assertTabCount, installFakeClock } from 'aws-core-vscode/test' +import { CodeWhispererSupplementalContext, FeatureConfigProvider } from 'aws-core-vscode/codewhisperer' +import { toTextEditor } from 'aws-core-vscode/test' + +const newLine = os.EOL + +describe('supplementalContextUtil', function () { + let testFolder: TestFolder + let clock: FakeTimers.InstalledClock + + const fakeCancellationToken: vscode.CancellationToken = { + isCancellationRequested: false, + onCancellationRequested: sinon.spy(), + } + + before(function () { + clock = installFakeClock() + }) + + after(function () { + clock.uninstall() + }) + + beforeEach(async function () { + testFolder = await TestFolder.create() + sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').returns('control') + }) + + afterEach(function () { + sinon.restore() + }) + + describe('fetchSupplementalContext', function () { + describe('openTabsContext', function () { + it('opentabContext should include chunks if non empty', async function () { + await toTextEditor('class Foo', 'Foo.java', testFolder.path, { preview: false }) + await toTextEditor('class Bar', 'Bar.java', testFolder.path, { preview: false }) + await toTextEditor('class Baz', 'Baz.java', testFolder.path, { preview: false }) + + const editor = await toTextEditor('public class Foo {}', 'Query.java', testFolder.path, { + preview: false, + }) + + await assertTabCount(4) + + const actual = await crossFile.fetchSupplementalContext(editor, fakeCancellationToken) + assert.ok(actual?.supplementalContextItems.length === 3) + }) + + it('opentabsContext should filter out empty chunks', async function () { + // open 3 files as supplemental context candidate files but none of them have contents + await toTextEditor('', 'Foo.java', testFolder.path, { preview: false }) + await toTextEditor('', 'Bar.java', testFolder.path, { preview: false }) + await toTextEditor('', 'Baz.java', testFolder.path, { preview: false }) + + const editor = await toTextEditor('public class Foo {}', 'Query.java', testFolder.path, { + preview: false, + }) + + await assertTabCount(4) + + const actual = await crossFile.fetchSupplementalContext(editor, fakeCancellationToken) + assert.ok(actual?.supplementalContextItems.length === 0) + }) + }) + }) + + describe('truncation', function () { + it('truncate context should do nothing if everything fits in constraint', function () { + const chunkA: crossFile.CodeWhispererSupplementalContextItem = { + content: 'a', + filePath: 'a.java', + score: 0, + } + const chunkB: crossFile.CodeWhispererSupplementalContextItem = { + content: 'b', + filePath: 'b.java', + score: 1, + } + const chunks = [chunkA, chunkB] + + const supplementalContext: CodeWhispererSupplementalContext = { + isUtg: false, + isProcessTimeout: false, + supplementalContextItems: chunks, + contentsLength: 25000, + latency: 0, + strategy: 'codemap', + } + + const actual = crossFile.truncateSuppelementalContext(supplementalContext) + assert.strictEqual(actual.supplementalContextItems.length, 2) + assert.strictEqual(actual.supplementalContextItems[0].content, 'a') + assert.strictEqual(actual.supplementalContextItems[1].content, 'b') + }) + + it('truncateLineByLine should drop the last line if max length is greater than threshold', function () { + const input = + repeatString('a', 11) + + newLine + + repeatString('b', 11) + + newLine + + repeatString('c', 11) + + newLine + + repeatString('d', 11) + + newLine + + repeatString('e', 11) + + assert.ok(input.length > 50) + const actual = crossFile.truncateLineByLine(input, 50) + assert.ok(actual.length <= 50) + + const input2 = repeatString(`b${newLine}`, 10) + const actual2 = crossFile.truncateLineByLine(input2, 8) + assert.ok(actual2.length <= 8) + }) + + it('truncation context should make context length per item lte 10240 cap', function () { + const chunkA: crossFile.CodeWhispererSupplementalContextItem = { + content: repeatString(`a${newLine}`, 4000), + filePath: 'a.java', + score: 0, + } + const chunkB: crossFile.CodeWhispererSupplementalContextItem = { + content: repeatString(`b${newLine}`, 6000), + filePath: 'b.java', + score: 1, + } + const chunkC: crossFile.CodeWhispererSupplementalContextItem = { + content: repeatString(`c${newLine}`, 1000), + filePath: 'c.java', + score: 2, + } + const chunkD: crossFile.CodeWhispererSupplementalContextItem = { + content: repeatString(`d${newLine}`, 1500), + filePath: 'd.java', + score: 3, + } + + assert.ok( + chunkA.content.length + chunkB.content.length + chunkC.content.length + chunkD.content.length > 20480 + ) + + const supplementalContext: CodeWhispererSupplementalContext = { + isUtg: false, + isProcessTimeout: false, + supplementalContextItems: [chunkA, chunkB, chunkC, chunkD], + contentsLength: 25000, + latency: 0, + strategy: 'codemap', + } + + const actual = crossFile.truncateSuppelementalContext(supplementalContext) + assert.strictEqual(actual.supplementalContextItems.length, 3) + assert.ok(actual.contentsLength <= 20480) + assert.strictEqual(actual.strategy, 'codemap') + }) + + it('truncate context should make context items lte 5', function () { + const chunkA: crossFile.CodeWhispererSupplementalContextItem = { + content: 'a', + filePath: 'a.java', + score: 0, + } + const chunkB: crossFile.CodeWhispererSupplementalContextItem = { + content: 'b', + filePath: 'b.java', + score: 1, + } + const chunkC: crossFile.CodeWhispererSupplementalContextItem = { + content: 'c', + filePath: 'c.java', + score: 2, + } + const chunkD: crossFile.CodeWhispererSupplementalContextItem = { + content: 'd', + filePath: 'd.java', + score: 3, + } + const chunkE: crossFile.CodeWhispererSupplementalContextItem = { + content: 'e', + filePath: 'e.java', + score: 4, + } + const chunkF: crossFile.CodeWhispererSupplementalContextItem = { + content: 'f', + filePath: 'f.java', + score: 5, + } + const chunkG: crossFile.CodeWhispererSupplementalContextItem = { + content: 'g', + filePath: 'g.java', + score: 6, + } + const chunks = [chunkA, chunkB, chunkC, chunkD, chunkE, chunkF, chunkG] + + assert.strictEqual(chunks.length, 7) + + const supplementalContext: CodeWhispererSupplementalContext = { + isUtg: false, + isProcessTimeout: false, + supplementalContextItems: chunks, + contentsLength: 25000, + latency: 0, + strategy: 'codemap', + } + + const actual = crossFile.truncateSuppelementalContext(supplementalContext) + assert.strictEqual(actual.supplementalContextItems.length, 5) + }) + + describe('truncate line by line', function () { + it('should return empty if empty string is provided', function () { + const input = '' + const actual = crossFile.truncateLineByLine(input, 50) + assert.strictEqual(actual, '') + }) + + it('should return empty if 0 max length is provided', function () { + const input = 'aaaaa' + const actual = crossFile.truncateLineByLine(input, 0) + assert.strictEqual(actual, '') + }) + + it('should flip the value if negative max length is provided', function () { + const input = `aaaaa${newLine}bbbbb` + const actual = crossFile.truncateLineByLine(input, -6) + const expected = crossFile.truncateLineByLine(input, 6) + assert.strictEqual(actual, expected) + assert.strictEqual(actual, 'aaaaa') + }) + }) + }) +}) + +function repeatString(s: string, n: number): string { + let output = '' + for (let i = 0; i < n; i++) { + output += s + } + + return output +} diff --git a/packages/amazonq/test/unit/codewhisperer/util/utgUtils.test.ts b/packages/amazonq/test/unit/codewhisperer/util/utgUtils.test.ts new file mode 100644 index 00000000000..67359b8a6fc --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/util/utgUtils.test.ts @@ -0,0 +1,63 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as utgUtils from 'aws-core-vscode/codewhisperer' + +describe('shouldFetchUtgContext', () => { + it('fully supported language', function () { + assert.ok(utgUtils.shouldFetchUtgContext('java')) + }) + + it('partially supported language', () => { + assert.strictEqual(utgUtils.shouldFetchUtgContext('python'), false) + }) + + it('not supported language', () => { + assert.strictEqual(utgUtils.shouldFetchUtgContext('typescript'), undefined) + + assert.strictEqual(utgUtils.shouldFetchUtgContext('javascript'), undefined) + + assert.strictEqual(utgUtils.shouldFetchUtgContext('javascriptreact'), undefined) + + assert.strictEqual(utgUtils.shouldFetchUtgContext('typescriptreact'), undefined) + + assert.strictEqual(utgUtils.shouldFetchUtgContext('scala'), undefined) + + assert.strictEqual(utgUtils.shouldFetchUtgContext('shellscript'), undefined) + + assert.strictEqual(utgUtils.shouldFetchUtgContext('csharp'), undefined) + + assert.strictEqual(utgUtils.shouldFetchUtgContext('c'), undefined) + }) +}) + +describe('guessSrcFileName', function () { + it('should return undefined if no matching regex', function () { + assert.strictEqual(utgUtils.guessSrcFileName('Foo.java', 'java'), undefined) + assert.strictEqual(utgUtils.guessSrcFileName('folder1/foo.py', 'python'), undefined) + assert.strictEqual(utgUtils.guessSrcFileName('Bar.js', 'javascript'), undefined) + }) + + it('java', function () { + assert.strictEqual(utgUtils.guessSrcFileName('FooTest.java', 'java'), 'Foo.java') + assert.strictEqual(utgUtils.guessSrcFileName('FooTests.java', 'java'), 'Foo.java') + }) + + it('python', function () { + assert.strictEqual(utgUtils.guessSrcFileName('test_foo.py', 'python'), 'foo.py') + assert.strictEqual(utgUtils.guessSrcFileName('foo_test.py', 'python'), 'foo.py') + }) + + it('typescript', function () { + assert.strictEqual(utgUtils.guessSrcFileName('Foo.test.ts', 'typescript'), 'Foo.ts') + assert.strictEqual(utgUtils.guessSrcFileName('Foo.spec.ts', 'typescript'), 'Foo.ts') + }) + + it('javascript', function () { + assert.strictEqual(utgUtils.guessSrcFileName('Foo.test.js', 'javascript'), 'Foo.js') + assert.strictEqual(utgUtils.guessSrcFileName('Foo.spec.js', 'javascript'), 'Foo.js') + }) +}) diff --git a/packages/core/package.json b/packages/core/package.json index 05b73b2acd7..a4d7d0bb96b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -309,110 +309,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" + } } } }, @@ -427,7 +504,7 @@ "webpackDev": "webpack --mode development", "serveVue": "ts-node ./scripts/build/checkServerPort.ts && webpack serve --port 8080 --config-name vue --mode development", "watch": "npm run clean && npm run buildScripts && npm run compileOnly -- --watch", - "lint": "ts-node ./scripts/lint/testLint.ts", + "lint": "node --max-old-space-size=8192 -r ts-node/register ./scripts/lint/testLint.ts", "generateClients": "ts-node ./scripts/build/generateServiceClient.ts ", "generateIcons": "ts-node ../../scripts/generateIcons.ts", "generateTelemetry": "node ../../node_modules/@aws-toolkits/telemetry/lib/generateTelemetry.js --extraInput=src/shared/telemetry/vscodeTelemetry.json --output=src/shared/telemetry/telemetry.gen.ts" @@ -442,9 +519,9 @@ "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.97", - "@aws/language-server-runtimes-types": "^0.1.39", + "@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", @@ -502,6 +579,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/credential-providers": "<3.731.0", "@aws-sdk/client-api-gateway": "<3.731.0", "@aws-sdk/client-apprunner": "<3.731.0", "@aws-sdk/client-cloudcontrol": "<3.731.0", @@ -509,12 +588,16 @@ "@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-glue": "^3.852.0", "@aws-sdk/client-iam": "<3.731.0", "@aws-sdk/client-lambda": "<3.731.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-ssm": "<3.731.0", "@aws-sdk/client-sso": "<3.731.0", "@aws-sdk/client-sso-oidc": "<3.731.0", @@ -578,8 +661,10 @@ "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", + "protobufjs": "^7.2.6", "@svgdotjs/svg.js": "^3.0.16", "svgdom": "^0.1.0", "jaro-winkler": "^0.2.8" diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json index 75bb7fe5672..a31376db0af 100644 --- a/packages/core/package.nls.json +++ b/packages/core/package.nls.json @@ -107,7 +107,8 @@ "AWS.configuration.description.amazonq.workspaceIndexCacheDirPath": "The path to the directory that contains the cache of the index of your workspace files", "AWS.configuration.description.amazonq.ignoredSecurityIssues": "Specifies a list of code issue identifiers that Amazon Q should ignore when reviewing your workspace. Each item in the array should be a unique string identifier for a specific code issue. This allows you to suppress notifications for known issues that you've assessed and determined to be false positives or not applicable to your project. Use this setting with caution, as it may cause you to miss important security alerts.", "AWS.configuration.description.amazonq.proxy.certificateAuthority": "Path to a Certificate Authority (PEM file) for SSL/TLS verification when using a proxy.", - "AWS.command.apig.invokeRemoteRestApi": "Invoke in the cloud", + "AWS.configuration.description.amazonq.proxy.enableProxyAndCertificateAutoDiscovery": "Automatically detect system proxy settings and SSL certificates.", + "AWS.command.apig.invokeRemoteRestApi": "Invoke remotely", "AWS.command.apig.invokeRemoteRestApi.cn": "Invoke on Amazon", "AWS.appBuilder.explorerTitle": "Application Builder", "AWS.appBuilder.explorerNode.noApps": "[This resource is not yet supported.]", @@ -152,9 +153,7 @@ "AWS.command.amazonq.optimizeCode": "Optimize", "AWS.command.amazonq.sendToPrompt": "Send to prompt", "AWS.command.amazonq.generateUnitTests": "Generate Tests", - "AWS.command.amazonq.security.scan": "Run Project Review", - "AWS.command.amazonq.security.fileScan": "Run File Review", - "AWS.command.amazonq.generateFix": "Generate Fix", + "AWS.command.amazonq.generateFix": "Fix", "AWS.command.amazonq.viewDetails": "View Details", "AWS.command.amazonq.explainIssue": "Explain", "AWS.command.amazonq.ignoreIssue": "Ignore Issue", @@ -167,8 +166,13 @@ "AWS.command.aboutToolkit": "About", "AWS.command.downloadLambda": "Download...", "AWS.command.uploadLambda": "Upload Lambda...", - "AWS.command.invokeLambda": "Invoke in the cloud", + "AWS.command.invokeLambda": "Invoke Remotely", + "AWS.command.openLambdaFile": "Open your Lambda code", + "AWS.command.quickDeployLambda": "Save and deploy your code", + "AWS.command.openLambdaWorkspace": "Open in a workspace", "AWS.command.invokeLambda.cn": "Invoke on Amazon", + "AWS.command.remoteDebugging.clearSnapshot": "Reset Lambda Remote Debugging Snapshot", + "AWS.command.lambda.convertToSam": "Convert to SAM Application", "AWS.command.refreshAwsExplorer": "Refresh Explorer", "AWS.command.refreshCdkExplorer": "Refresh CDK Explorer", "AWS.command.cdk.help": "View CDK Documentation", @@ -233,6 +237,10 @@ "AWS.command.s3.createFolder": "Create Folder...", "AWS.command.s3.uploadFile": "Upload Files...", "AWS.command.s3.uploadFileToParent": "Upload to Parent...", + "AWS.command.smus.switchProject": "Switch Project", + "AWS.command.smus.refreshProject": "Refresh Project", + "AWS.command.smus.signOut": "Sign Out", + "AWS.command.sagemaker.filterSpaces": "Filter Sagemaker Spaces", "AWS.command.stepFunctions.createStateMachineFromTemplate": "Create a new Step Functions state machine", "AWS.command.stepFunctions.publishStateMachine": "Publish state machine to Step Functions", "AWS.command.stepFunctions.openWithWorkflowStudio": "Open with Workflow Studio", @@ -250,7 +258,7 @@ "AWS.command.ssmDocument.openLocalDocumentJson": "Download as JSON", "AWS.command.ssmDocument.openLocalDocumentYaml": "Download as YAML", "AWS.command.ssmDocument.publishDocument": "Publish a Systems Manager Document", - "AWS.command.launchConfigForm.title": "Local Invoke and Debug Configuration", + "AWS.command.launchConfigForm.title": "Invoke Locally", "AWS.command.addSamDebugConfig": "Add Local Invoke and Debug Configuration", "AWS.command.toggleSamCodeLenses": "Toggle SAM hints in source files", "AWS.command.apprunner.createService": "Create Service", @@ -278,12 +286,13 @@ "AWS.command.codewhisperer.signout": "Sign Out", "AWS.command.codewhisperer.reconnect": "Reconnect", "AWS.command.codewhisperer.openReferencePanel": "Open Code Reference Log", + "AWS.command.codewhisperer.showLogs": "Show Logs", "AWS.command.q.selectRegionProfile": "Select Profile", "AWS.command.q.transform.acceptChanges": "Accept", "AWS.command.q.transform.rejectChanges": "Reject", "AWS.command.q.transform.stopJobInHub": "Stop job", "AWS.command.q.transform.viewJobProgress": "View job progress", - "AWS.command.q.transform.viewJobStatus": "View job status", + "AWS.command.q.transform.viewJobHistory": "View job history", "AWS.command.q.transform.showTransformationPlan": "View plan", "AWS.command.q.transform.showChangeSummary": "View summary", "AWS.command.threatComposer.createNew": "Create New Threat Composer File", @@ -298,6 +307,7 @@ "AWS.appcomposer.explorerTitle": "Infrastructure Composer", "AWS.cdk.explorerTitle": "CDK", "AWS.codecatalyst.explorerTitle": "CodeCatalyst", + "AWS.sagemakerunifiedstudio.explorerTitle": "SageMaker Unified Studio", "AWS.cwl.limit.desc": "Maximum amount of log entries pulled per request from CloudWatch Logs. For LiveTail, when the limit is reached, the oldest events will be removed to accomodate new events. (max 10000)", "AWS.samcli.deploy.bucket.recentlyUsed": "Buckets recently used for SAM deployments", "AWS.submenu.amazonqEditorContextSubmenu.title": "Amazon Q", @@ -356,11 +366,11 @@ "AWS.amazonq.security": "Code Issues", "AWS.amazonq.login": "Login", "AWS.amazonq.learnMore": "Learn More About Amazon Q", - "AWS.amazonq.exploreAgents": "Explore Agent Capabilities", "AWS.amazonq.welcomeWalkthrough": "Welcome Walkthrough", "AWS.amazonq.codewhisperer.title": "Amazon Q", "AWS.amazonq.toggleCodeSuggestion": "Toggle Auto-Suggestions", "AWS.amazonq.toggleCodeScan": "Toggle Auto-Scans", + "AWS.amazonq.toggleNextEditPredictionPanel": "Toggle next edit suggestion", "AWS.amazonq.scans.scanProgress": "Sure. This may take a few minutes. I will send a notification when it’s complete if you navigate away from this panel.", "AWS.amazonq.scans.waitingForInput": "Waiting on your inputs...", "AWS.amazonq.scans.chooseScan.description": "Would you like to review your active file or the workspace you have open?", @@ -468,12 +478,14 @@ "AWS.amazonq.doc.pillText.reject": "Reject", "AWS.amazonq.doc.pillText.makeChanges": "Make changes", "AWS.amazonq.inline.invokeChat": "Inline chat", + "AWS.amazonq.inline.acceptEdit": "Accept edit suggestion", + "AWS.amazonq.inline.rejectEdit": "Reject edit suggestion", "AWS.amazonq.opensettings:": "Open settings", "AWS.toolkit.lambda.walkthrough.quickpickTitle": "Application Builder Walkthrough", "AWS.toolkit.lambda.walkthrough.title": "Get started building your application", "AWS.toolkit.lambda.walkthrough.description": "Your quick guide to build an application visually, iterate locally, and deploy to the cloud!", "AWS.toolkit.lambda.walkthrough.toolInstall.title": "Complete installation", - "AWS.toolkit.lambda.walkthrough.toolInstall.description": "The AWS Command Line Interface (AWS CLI) is an open source tool that enables you to interact with AWS services using commands in your command-line shell. It is required to create and interact with AWS resources. \n\n[Install AWS CLI](command:aws.toolkit.installAWSCLI)\n\n Use the Serverless Application Model (SAM) CLI to locally build, invoke, and deploy your functions. Version 1.98+ is required. \n\n[Install SAM CLI](command:aws.toolkit.installSAMCLI)\n\n Use Docker to locally emulate a Lambda environment. Docker is optional. However, if you want to invoke locally, Docker is required so Lambda can locally emulate the execution environment. \n\n[Install Docker (optional)](command:aws.toolkit.installDocker)", + "AWS.toolkit.lambda.walkthrough.toolInstall.description": "Manage your AWS services and resources with the AWS Command Line Interface (AWS CLI). \n\n[Install AWS CLI](command:aws.toolkit.installAWSCLI)\n\nBuild locally, invoke, and deploy your functions with the Serverless Application Model (SAM) CLI. \n\n[Install SAM CLI](command:aws.toolkit.installSAMCLI)\n\nDocker is an optional, third party tool that assists with local AWS Lambda runtime emulation. Docker is required to invoke Lambda functions on your local machine. \n\n[Install Docker (optional)](command:aws.toolkit.installDocker)\n\nEmulate your AWS cloud services locally with LocalStack to streamline testing in VS Code and CI environments. [Learn more](https://docs.localstack.cloud/aws/). \n\n[Install LocalStack (optional)](command:aws.toolkit.installLocalStack)", "AWS.toolkit.lambda.walkthrough.chooseTemplate.title": "Choose your application template", "AWS.toolkit.lambda.walkthrough.chooseTemplate.description": "Select a starter application, visually compose an application from scratch, open an existing application, or browse more application examples. \n\nInfrastructure Composer allows you to visually compose modern applications in the cloud. It will define the necessary permissions between resources when you drag a connection between them. \n\n[Initialize your project](command:aws.toolkit.lambda.initializeWalkthroughProject)", "AWS.toolkit.lambda.walkthrough.step1.title": "Iterate locally", diff --git a/packages/core/resources/amazonQCT/QCT-Maven-1-0-156-0.jar b/packages/core/resources/amazonQCT/QCT-Maven-1-0-156-0.jar new file mode 100644 index 00000000000..8530e54fd5d Binary files /dev/null and b/packages/core/resources/amazonQCT/QCT-Maven-1-0-156-0.jar differ diff --git a/packages/core/resources/icons/aws/lambda/create-stack-light.svg b/packages/core/resources/icons/aws/lambda/create-stack-light.svg new file mode 100644 index 00000000000..3dd66689af0 --- /dev/null +++ b/packages/core/resources/icons/aws/lambda/create-stack-light.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/core/resources/icons/aws/lambda/create-stack.svg b/packages/core/resources/icons/aws/lambda/create-stack.svg new file mode 100644 index 00000000000..b8a08164556 --- /dev/null +++ b/packages/core/resources/icons/aws/lambda/create-stack.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/core/resources/icons/aws/lambda/deployed-function.svg b/packages/core/resources/icons/aws/lambda/deployed-function.svg new file mode 100644 index 00000000000..5d4e1c89298 --- /dev/null +++ b/packages/core/resources/icons/aws/lambda/deployed-function.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/core/resources/icons/aws/lambda/invoke-remotely.svg b/packages/core/resources/icons/aws/lambda/invoke-remotely.svg new file mode 100644 index 00000000000..b6071674e0c --- /dev/null +++ b/packages/core/resources/icons/aws/lambda/invoke-remotely.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/core/resources/icons/aws/sagemaker/code-editor.svg b/packages/core/resources/icons/aws/sagemaker/code-editor.svg new file mode 100644 index 00000000000..03e2f35f05c --- /dev/null +++ b/packages/core/resources/icons/aws/sagemaker/code-editor.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/core/resources/icons/aws/sagemaker/jupyter-lab.svg b/packages/core/resources/icons/aws/sagemaker/jupyter-lab.svg new file mode 100644 index 00000000000..c2a31bc0363 --- /dev/null +++ b/packages/core/resources/icons/aws/sagemaker/jupyter-lab.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/core/resources/icons/aws/sagemakerunifiedstudio/catalog.svg b/packages/core/resources/icons/aws/sagemakerunifiedstudio/catalog.svg new file mode 100644 index 00000000000..4bd5988c386 --- /dev/null +++ b/packages/core/resources/icons/aws/sagemakerunifiedstudio/catalog.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/resources/icons/aws/sagemakerunifiedstudio/spaces-dark.svg b/packages/core/resources/icons/aws/sagemakerunifiedstudio/spaces-dark.svg new file mode 100644 index 00000000000..3d3950ef9be --- /dev/null +++ b/packages/core/resources/icons/aws/sagemakerunifiedstudio/spaces-dark.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/packages/core/resources/icons/aws/sagemakerunifiedstudio/spaces.svg b/packages/core/resources/icons/aws/sagemakerunifiedstudio/spaces.svg new file mode 100644 index 00000000000..e559fa399c7 --- /dev/null +++ b/packages/core/resources/icons/aws/sagemakerunifiedstudio/spaces.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/packages/core/resources/icons/aws/sagemakerunifiedstudio/symbol-int.svg b/packages/core/resources/icons/aws/sagemakerunifiedstudio/symbol-int.svg new file mode 100644 index 00000000000..18aa022e10f --- /dev/null +++ b/packages/core/resources/icons/aws/sagemakerunifiedstudio/symbol-int.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/core/resources/icons/aws/sagemakerunifiedstudio/table.svg b/packages/core/resources/icons/aws/sagemakerunifiedstudio/table.svg new file mode 100644 index 00000000000..a8ac2aac05d --- /dev/null +++ b/packages/core/resources/icons/aws/sagemakerunifiedstudio/table.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/resources/icons/fonts/aws-toolkit-icons.woff b/packages/core/resources/icons/fonts/aws-toolkit-icons.woff new file mode 100644 index 00000000000..f17f02c9974 Binary files /dev/null and b/packages/core/resources/icons/fonts/aws-toolkit-icons.woff differ diff --git a/packages/core/resources/icons/vscode/light/cloud-upload.svg b/packages/core/resources/icons/vscode/light/cloud-upload.svg new file mode 100644 index 00000000000..8d4bc7722a8 --- /dev/null +++ b/packages/core/resources/icons/vscode/light/cloud-upload.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/resources/icons/vscode/light/run.svg b/packages/core/resources/icons/vscode/light/run.svg new file mode 100644 index 00000000000..8b0a58eca9b --- /dev/null +++ b/packages/core/resources/icons/vscode/light/run.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/resources/markdown/lambda2sam.md b/packages/core/resources/markdown/lambda2sam.md new file mode 100644 index 00000000000..ab43a89729a --- /dev/null +++ b/packages/core/resources/markdown/lambda2sam.md @@ -0,0 +1,42 @@ +# Welcome to Lambda Development with AWS SAM + +This project was generated from existing ${sourceType} to ${stackName} stack using the AWS Toolkit for VS Code. Your Lambda functions are now a project in AWS Serverless Application Model (SAM). Here, you can manage your functions as infrastructure as code using the AWS SAM template. This eliminates the need for manual changes in the AWS Console, provides better version control, and allows automated deployments of your serverless resources. + +${warning} + +## Prerequisites + +Confirm you have installed the following tools: + +- **The AWS CLI**: Needed to interact with AWS services from the command line. +- **The AWS SAM CLI:** Needed to locally build, invoke, and deploy your functions. Version 1.98+ is required. +- **Docker**: Optional, but required if you want to invoke locally, Docker is required. + +**Note:** For help on installing these tools, choose the **Application Builder** panel in **EXPLORER** or the AWS Toolkit Extension, and select **Walkthrough of Application Builder**. + +## What you can do with AWS SAM + +Your functions are ready for local development. You can either use the **AWS Application Builder** or the **SAM CLI** to edit and manage your functions. + +To get started using Application Builder, choose the **Application Builder** panel in **EXPLORER** or the AWS Toolkit Extension, and select **Walkthrough of Application Builder**. + +Use the following SAM CLI commands to manage your functions: + +- **Build Your Code:** Run [`sam build`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-build.html) in the terminal to compile your code and install dependencies. +- **Test Locally:** Run the [`sam local invoke`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-local-invoke.html) and [`sam local start-api`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-local-start-api.html)commands in the terminal. +- **Deploy Your Changes:** Run [`sam deploy --guided`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-deploy.html) in the terminal to deploy your updated function to AWS. +- **Verify Deployment:** Run [`sam remote invoke`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-remote-invoke.html) or go to the Lambda Console + +## Quick Reference + +- **SAM Template**: [template.yaml](./template.yaml) - Contains your infrastructure as code +- **SAM Configuration**: [samconfig.toml](./samconfig.toml) - Contains deployment configuration + +## Advanced features + +You can also debug your functions locally with breakpoints, manage environment variables, work with layers and dependencies, and configure function triggers and permissions through the AWS Toolkit interface. For more details, refer to the following resources + +- [AWS toolkit for Visual Studio Code User Guide](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/welcome.html) +- [Working with Application Builder](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/appbuilder-overview-overview.html) +- [AWS SAM Developer Guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) +- [AWS SAM command line reference](http://https//docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-command-reference.html) diff --git a/packages/core/resources/markdown/lambdaEdit.md b/packages/core/resources/markdown/lambdaEdit.md new file mode 100644 index 00000000000..31733fb441e --- /dev/null +++ b/packages/core/resources/markdown/lambdaEdit.md @@ -0,0 +1,19 @@ +# Welcome to Lambda local development + +Learn how to view your Lambda function locally, iterate, and deploy changes to the AWS Cloud. + +## Edit your Lambda function + +- Make changes to your function code and save it. You will be prompted to deploy if you're done editing. You can come back later if you have more changes to make. +- Using the terminal and your favorite package manager, add dependencies for your project. + +## Manage your Lambda functions + +- Select the AWS Toolkit icon in the left sidebar and select **EXPLORER** +- In your desired region, select the Lambda dropdown menu: + - To save and deploy a previously edited Lambda function, select the ![deploy](./deploy.svg) icon next to your Lambda function. + - To remotely invoke a function, select the ![invoke](./invoke.svg) icon next to your Lambda function. + +## Advanced features + +- To convert to a Lambda function to an AWS SAM application, select the ![createStack](./create-stack.svg) icon next to your Lambda function. For details on what AWS SAM is and how it can help you, see the [AWS Serverless Application Model Developer Guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html). diff --git a/packages/core/resources/markdown/samReadme.md b/packages/core/resources/markdown/samReadme.md index 14022174844..8b6a08eed57 100644 --- a/packages/core/resources/markdown/samReadme.md +++ b/packages/core/resources/markdown/samReadme.md @@ -13,7 +13,7 @@ ${LISTOFCONFIGURATIONS} You can debug the Lambda handlers locally by adding a breakpoint to the source file, then running the launch configuration. This works by using Docker on your local machine. -Invocation parameters, including payloads and request parameters, can be edited either by the `Local Invoke and Debug Configuration` command (through the ${COMMANDPALETTE} or ${CODELENS}) or by editing the `launch.json` file. +Invocation parameters, including payloads and request parameters, can be edited either by the `Invoke Locally` command (through the ${COMMANDPALETTE} or ${CODELENS}) or by editing the `launch.json` file. ${COMPANYNAME} Lambda functions not defined in the [`template.yaml`](./template.yaml) file can be invoked and debugged by creating a launch configuration through the ${CODELENS} over the function declaration, or with the `Add Local Invoke and Debug Configuration` command. diff --git a/packages/core/resources/sagemaker_connect b/packages/core/resources/sagemaker_connect new file mode 100755 index 00000000000..ede46c1c4b3 --- /dev/null +++ b/packages/core/resources/sagemaker_connect @@ -0,0 +1,145 @@ +#!/bin/bash + +set -x + +_get_ssm_session_info() { + local credentials_type="$1" + local aws_resource_arn="$2" + local local_endpoint_port="$3" + + local url_to_get_session_info="http://localhost:${local_endpoint_port}/get_session?connection_identifier=${aws_resource_arn}&credentials_type=${credentials_type}" + + # Generate unique temporary file name to avoid conflicts + local temp_file="/tmp/ssm_session_response_$$_$(date +%s%N).json" + + # Use curl with --write-out to capture HTTP status + response=$(curl -s -w "%{http_code}" -o "$temp_file" "$url_to_get_session_info") + http_status="${response: -3}" + session_json=$(cat "$temp_file") + + # Clean up temporary file + rm -f "$temp_file" + + if [[ "$http_status" -ne 200 ]]; then + echo "Error: Failed to get SSM session info. HTTP status: $http_status" + echo "Response: $session_json" + exit 1 + fi + + if [ -z "$session_json" ]; then + echo "Error: SSM connection info is empty." + exit 1 + fi + + export SSM_SESSION_JSON="$session_json" +} + +_get_ssm_session_info_async() { + local credentials_type="$1" + local aws_resource_arn="$2" + local local_endpoint_port="$3" + + local request_id=$(date +%s%3N) + local url_base="http://localhost:${local_endpoint_port}/get_session_async" + local url_to_get_session_info="${url_base}?connection_identifier=${aws_resource_arn}&credentials_type=${credentials_type}&request_id=${request_id}" + + # Generate unique temporary file name to avoid conflicts + local temp_file="/tmp/ssm_session_response_$$_$(date +%s%N).json" + + local max_retries=8 + local retry_interval=5 + local attempt=1 + + while (( attempt <= max_retries )); do + response=$(curl -s -w "%{http_code}" -o "$temp_file" "$url_to_get_session_info") + http_status="${response: -3}" + session_json=$(cat "$temp_file") + + if [[ "$http_status" -eq 200 ]]; then + # Clean up temporary file on success + rm -f "$temp_file" + export SSM_SESSION_JSON="$session_json" + return 0 + elif [[ "$http_status" -eq 202 || "$http_status" -eq 204 ]]; then + echo "Info: Session not ready (HTTP $http_status). Retrying in $retry_interval seconds... [$attempt/$max_retries]" + sleep $retry_interval + ((attempt++)) + else + echo "Error: Failed to get SSM session info. HTTP status: $http_status" + echo "Response: $session_json" + # Clean up temporary file on error + rm -f "$temp_file" + exit 1 + fi + done + + # Clean up temporary file on timeout + rm -f "$temp_file" + echo "Error: Timed out after $max_retries attempts waiting for session to be ready." + exit 1 +} + + +# Validate input +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +HOSTNAME="$1" + +# Parse creds_type and AWS resource ARN from HOSTNAME +if [[ "$HOSTNAME" =~ ^sm_([^_]+)_(arn_._aws.*)$ ]]; then + CREDS_TYPE="${BASH_REMATCH[1]}" + AWS_RESOURCE_ARN="${BASH_REMATCH[2]}" +else + echo "Hostname: $HOSTNAME" + echo "Invalid hostname format. Expected format: sm__" + exit 1 +fi + +# Workaround: Replace "__" with "/" in ARN +AWS_RESOURCE_ARN=$(echo "${AWS_RESOURCE_ARN}" | sed 's|__|/|g; s|_._|:|g; s|jupyterlab/default|JupyterLab/default|g') +REGION=$(echo "$AWS_RESOURCE_ARN" | cut -d: -f4) + +# Validate credentials type +if [[ "$CREDS_TYPE" != "lc" && "$CREDS_TYPE" != "dl" ]]; then + echo "Invalid creds_type. Must be 'lc' or 'dl'." + exit 1 +fi + +# Validate required env var and file +if [ -z "$SAGEMAKER_LOCAL_SERVER_FILE_PATH" ]; then + echo "[Error] SAGEMAKER_LOCAL_SERVER_FILE_PATH is not set" + exit 1 +fi + +if [ ! -f "$SAGEMAKER_LOCAL_SERVER_FILE_PATH" ]; then + echo "[Error] File not found at SAGEMAKER_LOCAL_SERVER_FILE_PATH: $SAGEMAKER_LOCAL_SERVER_FILE_PATH" + exit 1 +fi + +# Extract port from file +LOCAL_ENDPOINT_PORT=$(jq -r '.port' "$SAGEMAKER_LOCAL_SERVER_FILE_PATH") +if [ -z "$LOCAL_ENDPOINT_PORT" ] || [ "$LOCAL_ENDPOINT_PORT" == "null" ]; then + echo "[Error] 'port' field is missing or invalid in $SAGEMAKER_LOCAL_SERVER_FILE_PATH" + exit 1 +fi + +# Determine region from ARN +if [ "$CREDS_TYPE" == "lc" ]; then + credentials_type="local" + _get_ssm_session_info "$credentials_type" "$AWS_RESOURCE_ARN" "$LOCAL_ENDPOINT_PORT" +elif [ "$CREDS_TYPE" == "dl" ]; then + credentials_type="deeplink" + _get_ssm_session_info_async "$credentials_type" "$AWS_RESOURCE_ARN" "$LOCAL_ENDPOINT_PORT" +else + echo "[Error] Invalid creds_type. Must be 'lc' or 'dl'." + exit 1 +fi + +echo "Extracting AWS_SSM_CLI: $AWS_SSM_CLI" + +# Execute the session +AWS_SSM_CLI="${AWS_SSM_CLI:=session-manager-plugin}" +exec "${AWS_SSM_CLI}" "$SSM_SESSION_JSON" "$REGION" StartSession diff --git a/packages/core/resources/sagemaker_connect.ps1 b/packages/core/resources/sagemaker_connect.ps1 new file mode 100644 index 00000000000..0e593d65b85 --- /dev/null +++ b/packages/core/resources/sagemaker_connect.ps1 @@ -0,0 +1,148 @@ +param ( + [Parameter(Mandatory = $true)][string]$Hostname +) + +Write-Host "`n--- Script Start ---" +Write-Host "Start Time: $(Get-Date -Format o)" +Write-Host "Hostname argument received: $Hostname" + +Set-PSDebug -Trace 1 + +function Get-SSMSessionInfo { + param ( + [string]$CredentialsType, + [string]$AwsResourceArn, + [int]$LocalEndpointPort + ) + + Write-Host "Calling Get-SSMSessionInfo with credsType=${CredentialsType}, arn=${AwsResourceArn}, port=${LocalEndpointPort}" + + $url = "http://127.0.0.1:$LocalEndpointPort/get_session?connection_identifier=$AwsResourceArn&credentials_type=$CredentialsType" + Write-Host "Request URL: $url" + + try { + $response = Invoke-WebRequest -Uri $url -UseBasicParsing -ErrorAction Stop + Write-Host "Received response with status: $($response.StatusCode)" + + if ($response.StatusCode -ne 200) { + Write-Error "Failed to get SSM session info. HTTP status: $($response.StatusCode)" + Write-Error "Response: $($response.Content)" + exit 1 + } + + if (-not $response.Content) { + Write-Error "SSM connection info is empty." + exit 1 + } + + $script:SSM_SESSION_JSON = $response.Content + Write-Host "Session JSON successfully retrieved" + } catch { + Write-Error "Exception in Get-SSMSessionInfo: $_" + exit 1 + } +} + +function Get-SSMSessionInfoAsync { + param ( + [string]$CredentialsType, + [string]$AwsResourceArn, + [int]$LocalEndpointPort + ) + + $requestId = [string][DateTimeOffset]::Now.ToUnixTimeMilliseconds() + $url = "http://localhost:$LocalEndpointPort/get_session_async?connection_identifier=$AwsResourceArn&credentials_type=$CredentialsType&request_id=$requestId" + Write-Host "Calling Get-SSMSessionInfoAsync with URL: $url" + + $maxRetries = 8 + $retryInterval = 5 + + for ($attempt = 1; $attempt -le $maxRetries; $attempt++) { + try { + $response = Invoke-WebRequest -Uri $url -UseBasicParsing -ErrorAction Stop + $statusCode = $response.StatusCode + Write-Host "Attempt ${attempt}: HTTP ${statusCode}" + + if ($statusCode -eq 200) { + $script:SSM_SESSION_JSON = $response.Content + Write-Host "Session JSON successfully retrieved" + return + } elseif ($statusCode -eq 202 -or $statusCode -eq 204) { + Write-Host "Session not ready. Retrying in ${retryInterval} seconds... [${attempt}/${maxRetries}]" + Start-Sleep -Seconds $retryInterval + } else { + Write-Error "Failed to get SSM session info. HTTP status: ${statusCode}" + Write-Error "Response: $($response.Content)" + exit 1 + } + } catch { + Write-Error "Exception in Get-SSMSessionInfoAsync: $_" + exit 1 + } + } + + Write-Error "Timed out after ${maxRetries} attempts waiting for session to be ready." + exit 1 +} + +# Parse creds_type and AWS resource ARN from HOSTNAME +Write-Host "`nParsing hostname..." +if ($Hostname -match "^sm_([^_]+)_(arn_._aws.*)$") { + $CREDS_TYPE = $matches[1] + $AWS_RESOURCE_ARN = $matches[2] -replace '_._', ':' -replace '__', '/' +} else { + Write-Error "Invalid hostname format. Expected format: sm__" + exit 1 +} + +$REGION = ($AWS_RESOURCE_ARN -split ':')[3] +Write-Host "Parsed values:" +Write-Host " CREDS_TYPE: ${CREDS_TYPE}" +Write-Host " AWS_RESOURCE_ARN: ${AWS_RESOURCE_ARN}" +Write-Host " REGION: ${REGION}" + +# Validate credentials type +if ($CREDS_TYPE -ne "lc" -and $CREDS_TYPE -ne "dl") { + Write-Error "Invalid creds_type. Must be 'lc' or 'dl'." + exit 1 +} + +# Read port from local info JSON +Write-Host "`nReading SAGEMAKER_LOCAL_SERVER_FILE_PATH: $env:SAGEMAKER_LOCAL_SERVER_FILE_PATH" +try { + $jsonContent = Get-Content $env:SAGEMAKER_LOCAL_SERVER_FILE_PATH -Raw | ConvertFrom-Json + $LOCAL_ENDPOINT_PORT = $jsonContent.port + Write-Host "Extracted port: $LOCAL_ENDPOINT_PORT" +} catch { + Write-Error "Failed to read or parse JSON file at $env:SAGEMAKER_LOCAL_SERVER_FILE_PATH" + exit 1 +} + +if (-not $LOCAL_ENDPOINT_PORT -or $LOCAL_ENDPOINT_PORT -eq "null") { + Write-Error "'port' field is missing or invalid in $env:SAGEMAKER_LOCAL_SERVER_FILE_PATH" + exit 1 +} + +# Retrieve SSM session +Write-Host "`nStarting session retrieval..." +if ($CREDS_TYPE -eq "lc") { + Get-SSMSessionInfo -CredentialsType "local" -AwsResourceArn $AWS_RESOURCE_ARN -LocalEndpointPort $LOCAL_ENDPOINT_PORT +} elseif ($CREDS_TYPE -eq "dl") { + Get-SSMSessionInfoAsync -CredentialsType "deeplink" -AwsResourceArn $AWS_RESOURCE_ARN -LocalEndpointPort $LOCAL_ENDPOINT_PORT +} + +# Execute the session +Write-Host "`nLaunching session-manager-plugin..." +$sessionPlugin = if ($env:AWS_SSM_CLI) { $env:AWS_SSM_CLI } else { "session-manager-plugin" } + +$jsonObj = $script:SSM_SESSION_JSON | ConvertFrom-Json +$streamUrl = $jsonObj.StreamUrl +$tokenValue = $jsonObj.TokenValue +$sessionId = $jsonObj.SessionId + +Write-Host "Session Values:" +Write-Host " Stream URL: ${streamUrl}" +Write-Host " Token Value: ${tokenValue}" +Write-Host " Session ID: ${sessionId}" + +& $sessionPlugin "{\`"streamUrl\`":\`"${streamUrl}\`",\`"tokenValue\`":\`"${tokenValue}\`",\`"sessionId\`":\`"${sessionId}\`"}" "$REGION" "StartSession" \ No newline at end of file diff --git a/packages/core/scripts/build/copyFiles.ts b/packages/core/scripts/build/copyFiles.ts index e926a6d9963..6a4131172ba 100644 --- a/packages/core/scripts/build/copyFiles.ts +++ b/packages/core/scripts/build/copyFiles.ts @@ -31,6 +31,11 @@ const tasks: CopyTask[] = [ { target: path.join('src', 'testFixtures') }, { target: 'src/auth/sso/vue' }, + // Vue.js for webviews + { + target: path.join('../../node_modules', 'vue', 'dist', 'vue.global.prod.js'), + destination: path.join('libs', 'vue.min.js'), + }, // SSM { target: path.join('../../node_modules', 'aws-ssm-document-language-service', 'dist', 'server.js'), diff --git a/packages/core/scripts/build/generateServiceClient.ts b/packages/core/scripts/build/generateServiceClient.ts index 7ef217be21b..de601e6ee44 100644 --- a/packages/core/scripts/build/generateServiceClient.ts +++ b/packages/core/scripts/build/generateServiceClient.ts @@ -242,8 +242,12 @@ void (async () => { serviceName: 'CodeWhispererUserClient', }, { - serviceJsonPath: 'src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json', - serviceName: 'FeatureDevProxyClient', + serviceJsonPath: 'src/sagemakerunifiedstudio/shared/client/gluecatalogapi.json', + serviceName: 'GlueCatalogApi', + }, + { + serviceJsonPath: 'src/sagemakerunifiedstudio/shared/client/sqlworkbench.json', + serviceName: 'SQLWorkbench', }, ] await generateServiceClients(serviceClientDefinitions) diff --git a/packages/core/scripts/lint/testLint.ts b/packages/core/scripts/lint/testLint.ts index d215e57f675..52b22d12fd6 100644 --- a/packages/core/scripts/lint/testLint.ts +++ b/packages/core/scripts/lint/testLint.ts @@ -9,7 +9,9 @@ void (async () => { try { console.log('Running linting tests...') - const mocha = new Mocha() + const mocha = new Mocha({ + timeout: 5000, + }) const testFiles = await glob('dist/src/testLint/**/*.test.js') for (const file of testFiles) { diff --git a/packages/core/src/amazonq/commons/connector/baseMessenger.ts b/packages/core/src/amazonq/commons/connector/baseMessenger.ts deleted file mode 100644 index c26834c6fff..00000000000 --- a/packages/core/src/amazonq/commons/connector/baseMessenger.ts +++ /dev/null @@ -1,219 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ChatItemAction, ProgressField } from '@aws/mynah-ui' -import { AuthFollowUpType, AuthMessageDataMap } from '../../../amazonq/auth/model' -import { i18n } from '../../../shared/i18n-helper' -import { CodeReference } from '../../../amazonq/webview/ui/connector' - -import { MessengerTypes } from '../../../amazonqFeatureDev/controllers/chat/messenger/constants' -import { - AppToWebViewMessageDispatcher, - AsyncEventProgressMessage, - AuthenticationUpdateMessage, - AuthNeededException, - ChatInputEnabledMessage, - ChatMessage, - CodeResultMessage, - FileComponent, - FolderConfirmationMessage, - OpenNewTabMessage, - UpdateAnswerMessage, - UpdatePlaceholderMessage, - UpdatePromptProgressMessage, -} from './connectorMessages' -import { DeletedFileInfo, FollowUpTypes, NewFileInfo } from '../types' -import { messageWithConversationId } from '../../../amazonqFeatureDev/userFacingText' -import { FeatureAuthState } from '../../../codewhisperer/util/authUtil' - -export class Messenger { - public constructor( - private readonly dispatcher: AppToWebViewMessageDispatcher, - private readonly sender: string - ) {} - - public sendAnswer(params: { - message?: string - type: MessengerTypes - followUps?: ChatItemAction[] - tabID: string - canBeVoted?: boolean - snapToTop?: boolean - messageId?: string - disableChatInput?: boolean - }) { - this.dispatcher.sendChatMessage( - new ChatMessage( - { - message: params.message, - messageType: params.type, - followUps: params.followUps, - relatedSuggestions: undefined, - canBeVoted: params.canBeVoted ?? false, - snapToTop: params.snapToTop ?? false, - messageId: params.messageId, - }, - params.tabID, - this.sender - ) - ) - if (params.disableChatInput) { - this.sendChatInputEnabled(params.tabID, false) - } - } - - public sendFeedback(tabID: string) { - this.sendAnswer({ - message: undefined, - type: 'system-prompt', - followUps: [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.sendFeedback'), - type: FollowUpTypes.SendFeedback, - status: 'info', - }, - ], - tabID, - }) - } - - public sendMonthlyLimitError(tabID: string) { - this.sendAnswer({ - type: 'answer', - tabID: tabID, - message: i18n('AWS.amazonq.featureDev.error.monthlyLimitReached'), - disableChatInput: true, - }) - this.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.featureDev.placeholder.chatInputDisabled')) - } - - public sendUpdatePromptProgress(tabID: string, progressField: ProgressField | null) { - this.dispatcher.sendUpdatePromptProgress(new UpdatePromptProgressMessage(tabID, this.sender, progressField)) - } - - public sendFolderConfirmationMessage( - tabID: string, - message: string, - folderPath: string, - followUps?: ChatItemAction[] - ) { - this.dispatcher.sendFolderConfirmationMessage( - new FolderConfirmationMessage(tabID, this.sender, message, folderPath, followUps) - ) - - this.sendChatInputEnabled(tabID, false) - } - - public sendErrorMessage( - errorMessage: string, - tabID: string, - retries: number, - conversationId?: string, - showDefaultMessage?: boolean - ) { - if (retries === 0) { - this.sendAnswer({ - type: 'answer', - tabID: tabID, - message: showDefaultMessage ? errorMessage : i18n('AWS.amazonq.featureDev.error.technicalDifficulties'), - canBeVoted: true, - }) - this.sendFeedback(tabID) - return - } - - this.sendAnswer({ - type: 'answer', - tabID: tabID, - message: errorMessage + messageWithConversationId(conversationId), - }) - - this.sendAnswer({ - message: undefined, - type: 'system-prompt', - followUps: [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.retry'), - type: FollowUpTypes.Retry, - status: 'warning', - }, - ], - tabID, - }) - } - - public sendCodeResult( - filePaths: NewFileInfo[], - deletedFiles: DeletedFileInfo[], - references: CodeReference[], - tabID: string, - uploadId: string, - codeGenerationId: string - ) { - this.dispatcher.sendCodeResult( - new CodeResultMessage(filePaths, deletedFiles, references, tabID, this.sender, uploadId, codeGenerationId) - ) - } - - public sendAsyncEventProgress(tabID: string, inProgress: boolean, message: string | undefined) { - this.dispatcher.sendAsyncEventProgress(new AsyncEventProgressMessage(tabID, this.sender, inProgress, message)) - } - - public updateFileComponent( - tabID: string, - filePaths: NewFileInfo[], - deletedFiles: DeletedFileInfo[], - messageId: string, - disableFileActions: boolean - ) { - this.dispatcher.updateFileComponent( - new FileComponent(tabID, this.sender, filePaths, deletedFiles, messageId, disableFileActions) - ) - } - - public updateChatAnswer(message: UpdateAnswerMessage) { - this.dispatcher.updateChatAnswer(message) - } - - public sendUpdatePlaceholder(tabID: string, newPlaceholder: string) { - this.dispatcher.sendPlaceholder(new UpdatePlaceholderMessage(tabID, this.sender, newPlaceholder)) - } - - public sendChatInputEnabled(tabID: string, enabled: boolean) { - this.dispatcher.sendChatInputEnabled(new ChatInputEnabledMessage(tabID, this.sender, enabled)) - } - - public sendAuthenticationUpdate(enabled: boolean, authenticatingTabIDs: string[]) { - this.dispatcher.sendAuthenticationUpdate( - new AuthenticationUpdateMessage(this.sender, enabled, authenticatingTabIDs) - ) - } - - public async sendAuthNeededExceptionMessage(credentialState: FeatureAuthState, tabID: string) { - let authType: AuthFollowUpType = 'full-auth' - let message = AuthMessageDataMap[authType].message - - switch (credentialState.amazonQ) { - case 'disconnected': - authType = 'full-auth' - message = AuthMessageDataMap[authType].message - break - case 'unsupported': - authType = 'use-supported-auth' - message = AuthMessageDataMap[authType].message - break - case 'expired': - authType = 're-auth' - message = AuthMessageDataMap[authType].message - break - } - - this.dispatcher.sendAuthNeededExceptionMessage(new AuthNeededException(message, authType, tabID, this.sender)) - } - - public openNewTask() { - this.dispatcher.sendOpenNewTask(new OpenNewTabMessage(this.sender)) - } -} diff --git a/packages/core/src/amazonq/commons/connector/connectorMessages.ts b/packages/core/src/amazonq/commons/connector/connectorMessages.ts deleted file mode 100644 index 6f60b786fcb..00000000000 --- a/packages/core/src/amazonq/commons/connector/connectorMessages.ts +++ /dev/null @@ -1,291 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { AuthFollowUpType } from '../../auth/model' -import { MessagePublisher } from '../../messages/messagePublisher' -import { CodeReference } from '../../webview/ui/connector' -import { ChatItemAction, ProgressField, SourceLink } from '@aws/mynah-ui' -import { ChatItemType } from '../model' -import { DeletedFileInfo, NewFileInfo } from '../types' -import { licenseText } from '../../../amazonqFeatureDev/constants' - -class UiMessage { - readonly time: number = Date.now() - readonly type: string = '' - - public constructor( - protected tabID: string, - protected sender: string - ) {} -} - -export class ErrorMessage extends UiMessage { - readonly title!: string - readonly message!: string - override type = 'errorMessage' - - constructor(title: string, message: string, tabID: string, sender: string) { - super(tabID, sender) - this.title = title - this.message = message - } -} - -export class CodeResultMessage extends UiMessage { - readonly message!: string - readonly codeGenerationId!: string - readonly references!: { - information: string - recommendationContentSpan: { - start: number - end: number - } - }[] - readonly conversationID!: string - override type = 'codeResultMessage' - - constructor( - readonly filePaths: NewFileInfo[], - readonly deletedFiles: DeletedFileInfo[], - references: CodeReference[], - tabID: string, - sender: string, - conversationID: string, - codeGenerationId: string - ) { - super(tabID, sender) - this.references = references - .filter((ref) => ref.licenseName && ref.repository && ref.url) - .map((ref) => { - return { - information: licenseText(ref), - - // We're forced to provide these otherwise mynah ui errors somewhere down the line. Though they aren't used - recommendationContentSpan: { - start: 0, - end: 0, - }, - } - }) - this.codeGenerationId = codeGenerationId - this.conversationID = conversationID - } -} - -export class FolderConfirmationMessage extends UiMessage { - readonly folderPath: string - readonly message: string - readonly followUps?: ChatItemAction[] - override type = 'folderConfirmationMessage' - constructor(tabID: string, sender: string, message: string, folderPath: string, followUps?: ChatItemAction[]) { - super(tabID, sender) - this.message = message - this.folderPath = folderPath - this.followUps = followUps - } -} - -export class UpdatePromptProgressMessage extends UiMessage { - readonly progressField: ProgressField | null - override type = 'updatePromptProgress' - constructor(tabID: string, sender: string, progressField: ProgressField | null) { - super(tabID, sender) - this.progressField = progressField - } -} - -export class AsyncEventProgressMessage extends UiMessage { - readonly inProgress: boolean - readonly message: string | undefined - override type = 'asyncEventProgressMessage' - - constructor(tabID: string, sender: string, inProgress: boolean, message: string | undefined) { - super(tabID, sender) - this.inProgress = inProgress - this.message = message - } -} - -export class AuthenticationUpdateMessage { - readonly time: number = Date.now() - readonly type = 'authenticationUpdateMessage' - - constructor( - readonly sender: string, - readonly featureEnabled: boolean, - readonly authenticatingTabIDs: string[] - ) {} -} - -export class FileComponent extends UiMessage { - readonly filePaths: NewFileInfo[] - readonly deletedFiles: DeletedFileInfo[] - override type = 'updateFileComponent' - readonly messageId: string - readonly disableFileActions: boolean - - constructor( - tabID: string, - sender: string, - filePaths: NewFileInfo[], - deletedFiles: DeletedFileInfo[], - messageId: string, - disableFileActions: boolean - ) { - super(tabID, sender) - this.filePaths = filePaths - this.deletedFiles = deletedFiles - this.messageId = messageId - this.disableFileActions = disableFileActions - } -} - -export class UpdatePlaceholderMessage extends UiMessage { - readonly newPlaceholder: string - override type = 'updatePlaceholderMessage' - - constructor(tabID: string, sender: string, newPlaceholder: string) { - super(tabID, sender) - this.newPlaceholder = newPlaceholder - } -} - -export class ChatInputEnabledMessage extends UiMessage { - readonly enabled: boolean - override type = 'chatInputEnabledMessage' - - constructor(tabID: string, sender: string, enabled: boolean) { - super(tabID, sender) - this.enabled = enabled - } -} - -export class OpenNewTabMessage { - readonly time: number = Date.now() - readonly type = 'openNewTabMessage' - - constructor(protected sender: string) {} -} - -export class AuthNeededException extends UiMessage { - readonly message: string - readonly authType: AuthFollowUpType - override type = 'authNeededException' - - constructor(message: string, authType: AuthFollowUpType, tabID: string, sender: string) { - super(tabID, sender) - this.message = message - this.authType = authType - } -} - -export interface ChatMessageProps { - readonly message: string | undefined - readonly messageType: ChatItemType - readonly followUps: ChatItemAction[] | undefined - readonly relatedSuggestions: SourceLink[] | undefined - readonly canBeVoted: boolean - readonly snapToTop: boolean - readonly messageId?: string -} - -export class ChatMessage extends UiMessage { - readonly message: string | undefined - readonly messageType: ChatItemType - readonly followUps: ChatItemAction[] | undefined - readonly relatedSuggestions: SourceLink[] | undefined - readonly canBeVoted: boolean - readonly requestID!: string - readonly snapToTop: boolean - readonly messageId: string | undefined - override type = 'chatMessage' - - constructor(props: ChatMessageProps, tabID: string, sender: string) { - super(tabID, sender) - this.message = props.message - this.messageType = props.messageType - this.followUps = props.followUps - this.relatedSuggestions = props.relatedSuggestions - this.canBeVoted = props.canBeVoted - this.snapToTop = props.snapToTop - this.messageId = props.messageId - } -} - -export interface UpdateAnswerMessageProps { - readonly messageId: string - readonly messageType: ChatItemType - readonly followUps: ChatItemAction[] | undefined -} - -export class UpdateAnswerMessage extends UiMessage { - readonly messageId: string - readonly messageType: ChatItemType - readonly followUps: ChatItemAction[] | undefined - override type = 'updateChatAnswer' - - constructor(props: UpdateAnswerMessageProps, tabID: string, sender: string) { - super(tabID, sender) - this.messageId = props.messageId - this.messageType = props.messageType - this.followUps = props.followUps - } -} - -export class AppToWebViewMessageDispatcher { - constructor(private readonly appsToWebViewMessagePublisher: MessagePublisher) {} - - public sendErrorMessage(message: ErrorMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendChatMessage(message: ChatMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendCodeResult(message: CodeResultMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendUpdatePromptProgress(message: UpdatePromptProgressMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendFolderConfirmationMessage(message: FolderConfirmationMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendAsyncEventProgress(message: AsyncEventProgressMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendPlaceholder(message: UpdatePlaceholderMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendChatInputEnabled(message: ChatInputEnabledMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendAuthNeededExceptionMessage(message: AuthNeededException) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendAuthenticationUpdate(message: AuthenticationUpdateMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendOpenNewTask(message: OpenNewTabMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public updateFileComponent(message: FileComponent) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public updateChatAnswer(message: UpdateAnswerMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } -} diff --git a/packages/core/src/amazonq/commons/session/sessionConfigFactory.ts b/packages/core/src/amazonq/commons/session/sessionConfigFactory.ts deleted file mode 100644 index 4204d1d56d6..00000000000 --- a/packages/core/src/amazonq/commons/session/sessionConfigFactory.ts +++ /dev/null @@ -1,38 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import { WorkspaceFolderNotFoundError } from '../../../amazonqFeatureDev/errors' -import { CurrentWsFolders } from '../types' -import { VirtualFileSystem } from '../../../shared/virtualFilesystem' -import { VirtualMemoryFile } from '../../../shared/virtualMemoryFile' - -export interface SessionConfig { - // The paths on disk to where the source code lives - workspaceRoots: string[] - readonly fs: VirtualFileSystem - readonly workspaceFolders: CurrentWsFolders -} - -/** - * Factory method for creating session configurations - * @returns An instantiated SessionConfig, using either the arguments provided or the defaults - */ -export async function createSessionConfig(scheme: string): Promise { - const workspaceFolders = vscode.workspace.workspaceFolders - const firstFolder = workspaceFolders?.[0] - if (workspaceFolders === undefined || workspaceFolders.length === 0 || firstFolder === undefined) { - throw new WorkspaceFolderNotFoundError() - } - - const workspaceRoots = workspaceFolders.map((f) => f.uri.fsPath) - - const fs = new VirtualFileSystem() - - // Register an empty featureDev file that's used when a new file is being added by the LLM - fs.registerProvider(vscode.Uri.from({ scheme, path: 'empty' }), new VirtualMemoryFile(new Uint8Array())) - - return Promise.resolve({ workspaceRoots, fs, workspaceFolders: [firstFolder, ...workspaceFolders.slice(1)] }) -} diff --git a/packages/core/src/amazonq/commons/types.ts b/packages/core/src/amazonq/commons/types.ts index c2d2c427596..3a4d014609d 100644 --- a/packages/core/src/amazonq/commons/types.ts +++ b/packages/core/src/amazonq/commons/types.ts @@ -4,13 +4,8 @@ */ import * as vscode from 'vscode' -import { VirtualFileSystem } from '../../shared/virtualFilesystem' -import type { CancellationTokenSource } from 'vscode' -import { CodeReference, UploadHistory } from '../webview/ui/connector' import { DiffTreeFileInfo } from '../webview/ui/diffTree/types' -import { Messenger } from './connector/baseMessenger' import { FeatureClient } from '../client/client' -import { TelemetryHelper } from '../util/telemetryHelper' import { MynahUI } from '@aws/mynah-ui' export enum FollowUpTypes { @@ -56,12 +51,6 @@ export type Interaction = { responseType?: LLMResponseType } -export interface SessionStateInteraction { - nextState: SessionState | Omit | undefined - interaction: Interaction - currentCodeGenerationId?: string -} - export enum Intent { DEV = 'DEV', DOC = 'DOC', @@ -86,24 +75,6 @@ export type SessionStatePhase = DevPhase.INIT | DevPhase.CODEGEN export type CurrentWsFolders = [vscode.WorkspaceFolder, ...vscode.WorkspaceFolder[]] -export interface SessionState { - readonly filePaths?: NewFileInfo[] - readonly deletedFiles?: DeletedFileInfo[] - readonly references?: CodeReference[] - readonly phase?: SessionStatePhase - readonly uploadId: string - readonly currentIteration?: number - currentCodeGenerationId?: string - tokenSource?: CancellationTokenSource - readonly codeGenerationId?: string - readonly tabID: string - interact(action: SessionStateAction): Promise - updateWorkspaceRoot?: (workspaceRoot: string) => void - codeGenerationRemainingIterationCount?: number - codeGenerationTotalIterationCount?: number - uploadHistory?: UploadHistory -} - export interface SessionStateConfig { workspaceRoots: string[] workspaceFolders: CurrentWsFolders @@ -113,16 +84,6 @@ export interface SessionStateConfig { currentCodeGenerationId?: string } -export interface SessionStateAction { - task: string - msg: string - messenger: Messenger - fs: VirtualFileSystem - telemetry: TelemetryHelper - uploadHistory?: UploadHistory - tokenSource?: CancellationTokenSource -} - export type NewFileZipContents = { zipFilePath: string; fileContent: string } export type NewFileInfo = DiffTreeFileInfo & NewFileZipContents & { diff --git a/packages/core/src/amazonq/index.ts b/packages/core/src/amazonq/index.ts index 3b7737b3547..aa266ce39dd 100644 --- a/packages/core/src/amazonq/index.ts +++ b/packages/core/src/amazonq/index.ts @@ -13,7 +13,6 @@ export { focusAmazonQChatWalkthrough, openAmazonQWalkthrough, walkthroughInlineSuggestionsExample, - walkthroughSecurityScanExample, } from './onboardingPage/walkthrough' export { api } from './extApi' export { AmazonQChatViewProvider } from './webview/webView' @@ -36,7 +35,6 @@ export { ChatItemType, referenceLogText } from './commons/model' export { ExtensionMessage } from '../amazonq/webview/ui/commands' export { CodeReference } from '../codewhispererChat/view/connector/connector' export { extractAuthFollowUp } from './util/authUtils' -export { Messenger } from './commons/connector/baseMessenger' export * as secondaryAuth from '../auth/secondaryAuth' export * as authConnection from '../auth/connection' export * as featureConfig from './webview/generators/featureConfig' diff --git a/packages/core/src/amazonq/indexNode.ts b/packages/core/src/amazonq/indexNode.ts index 88a3a4bba37..ccc01dc2832 100644 --- a/packages/core/src/amazonq/indexNode.ts +++ b/packages/core/src/amazonq/indexNode.ts @@ -7,7 +7,4 @@ * These agents have underlying requirements on node dependencies (e.g. jsdom, admzip) */ export { init as cwChatAppInit } from '../codewhispererChat/app' -export { init as featureDevChatAppInit } from '../amazonqFeatureDev/app' export { init as gumbyChatAppInit } from '../amazonqGumby/app' -export { init as testChatAppInit } from '../amazonqTest/app' -export { init as docChatAppInit } from '../amazonqDoc/app' diff --git a/packages/core/src/amazonq/onboardingPage/walkthrough.ts b/packages/core/src/amazonq/onboardingPage/walkthrough.ts index cb56c8b2abb..50b1db642a5 100644 --- a/packages/core/src/amazonq/onboardingPage/walkthrough.ts +++ b/packages/core/src/amazonq/onboardingPage/walkthrough.ts @@ -7,7 +7,6 @@ import { focusAmazonQPanel } from '../../codewhispererChat/commands/registerComm import globals, { isWeb } from '../../shared/extensionGlobals' import { VSCODE_EXTENSION_ID } from '../../shared/extensions' import { getLogger } from '../../shared/logger/logger' -import { localize } from '../../shared/utilities/vsCodeUtils' import { Commands, placeholder } from '../../shared/vscode/commands2' import vscode from 'vscode' @@ -66,11 +65,3 @@ fake_users = [ }) } ) - -export const walkthroughSecurityScanExample = Commands.declare( - `_aws.amazonq.walkthrough.securityScanExample`, - () => async () => { - const filterText = localize('AWS.command.amazonq.security.scan', 'Run Project Review') - void vscode.commands.executeCommand('workbench.action.quickOpen', `> ${filterText}`) - } -) diff --git a/packages/core/src/amazonq/session/sessionState.ts b/packages/core/src/amazonq/session/sessionState.ts deleted file mode 100644 index 1f206c23159..00000000000 --- a/packages/core/src/amazonq/session/sessionState.ts +++ /dev/null @@ -1,432 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import { ToolkitError } from '../../shared/errors' -import globals from '../../shared/extensionGlobals' -import { getLogger } from '../../shared/logger/logger' -import { AmazonqCreateUpload, Span, telemetry } from '../../shared/telemetry/telemetry' -import { VirtualFileSystem } from '../../shared/virtualFilesystem' -import { CodeReference, UploadHistory } from '../webview/ui/connector' -import { AuthUtil } from '../../codewhisperer/util/authUtil' -import { randomUUID } from '../../shared/crypto' -import { i18n } from '../../shared/i18n-helper' -import { - CodeGenerationStatus, - CurrentWsFolders, - DeletedFileInfo, - DevPhase, - NewFileInfo, - SessionState, - SessionStateAction, - SessionStateConfig, - SessionStateInteraction, - SessionStatePhase, -} from '../commons/types' -import { prepareRepoData, getDeletedFileInfos, registerNewFiles, PrepareRepoDataOptions } from '../util/files' -import { uploadCode } from '../util/upload' -import { truncate } from '../../shared/utilities/textUtilities' - -export const EmptyCodeGenID = 'EMPTY_CURRENT_CODE_GENERATION_ID' -export const RunCommandLogFileName = '.amazonq/dev/run_command.log' - -export interface BaseMessenger { - sendAnswer(params: any): void - sendUpdatePlaceholder?(tabId: string, message: string): void -} - -export abstract class CodeGenBase { - private pollCount = 360 - private requestDelay = 5000 - public tokenSource: vscode.CancellationTokenSource - public phase: SessionStatePhase = DevPhase.CODEGEN - public readonly conversationId: string - public readonly uploadId: string - public currentCodeGenerationId?: string - public isCancellationRequested?: boolean - - constructor( - protected config: SessionStateConfig, - public tabID: string - ) { - this.tokenSource = new vscode.CancellationTokenSource() - this.conversationId = config.conversationId - this.uploadId = config.uploadId - this.currentCodeGenerationId = config.currentCodeGenerationId || EmptyCodeGenID - } - - protected abstract handleProgress(messenger: BaseMessenger, action: SessionStateAction, detail?: string): void - protected abstract getScheme(): string - protected abstract getTimeoutErrorCode(): string - protected abstract handleGenerationComplete( - messenger: BaseMessenger, - newFileInfo: NewFileInfo[], - action: SessionStateAction - ): void - - async generateCode({ - messenger, - fs, - codeGenerationId, - telemetry: telemetry, - workspaceFolders, - action, - }: { - messenger: BaseMessenger - fs: VirtualFileSystem - codeGenerationId: string - telemetry: any - workspaceFolders: CurrentWsFolders - action: SessionStateAction - }): Promise<{ - newFiles: NewFileInfo[] - deletedFiles: DeletedFileInfo[] - references: CodeReference[] - codeGenerationRemainingIterationCount?: number - codeGenerationTotalIterationCount?: number - }> { - let codeGenerationRemainingIterationCount = undefined - let codeGenerationTotalIterationCount = undefined - for ( - let pollingIteration = 0; - pollingIteration < this.pollCount && !this.isCancellationRequested; - ++pollingIteration - ) { - const codegenResult = await this.config.proxyClient.getCodeGeneration(this.conversationId, codeGenerationId) - codeGenerationRemainingIterationCount = codegenResult.codeGenerationRemainingIterationCount - codeGenerationTotalIterationCount = codegenResult.codeGenerationTotalIterationCount - - getLogger().debug(`Codegen response: %O`, codegenResult) - telemetry.setCodeGenerationResult(codegenResult.codeGenerationStatus.status) - - switch (codegenResult.codeGenerationStatus.status as CodeGenerationStatus) { - case CodeGenerationStatus.COMPLETE: { - const { newFileContents, deletedFiles, references } = - await this.config.proxyClient.exportResultArchive(this.conversationId) - - const logFileInfo = newFileContents.find( - (file: { zipFilePath: string; fileContent: string }) => - file.zipFilePath === RunCommandLogFileName - ) - if (logFileInfo) { - logFileInfo.fileContent = truncate(logFileInfo.fileContent, 10000000, '\n... [truncated]') // Limit to max 20MB - getLogger().info(`sessionState: Run Command logs, ${logFileInfo.fileContent}`) - newFileContents.splice(newFileContents.indexOf(logFileInfo), 1) - } - - const newFileInfo = registerNewFiles( - fs, - newFileContents, - this.uploadId, - workspaceFolders, - this.conversationId, - this.getScheme() - ) - telemetry.setNumberOfFilesGenerated(newFileInfo.length) - - this.handleGenerationComplete(messenger, newFileInfo, action) - - return { - newFiles: newFileInfo, - deletedFiles: getDeletedFileInfos(deletedFiles, workspaceFolders), - references, - codeGenerationRemainingIterationCount, - codeGenerationTotalIterationCount, - } - } - case CodeGenerationStatus.PREDICT_READY: - case CodeGenerationStatus.IN_PROGRESS: { - if (codegenResult.codeGenerationStatusDetail) { - this.handleProgress(messenger, action, codegenResult.codeGenerationStatusDetail) - } - await new Promise((f) => globals.clock.setTimeout(f, this.requestDelay)) - break - } - case CodeGenerationStatus.PREDICT_FAILED: - case CodeGenerationStatus.DEBATE_FAILED: - case CodeGenerationStatus.FAILED: { - throw this.handleError(messenger, codegenResult) - } - default: { - const errorMessage = `Unknown status: ${codegenResult.codeGenerationStatus.status}\n` - throw new ToolkitError(errorMessage, { code: 'UnknownCodeGenError' }) - } - } - } - - if (!this.isCancellationRequested) { - const errorMessage = i18n('AWS.amazonq.featureDev.error.codeGen.timeout') - throw new ToolkitError(errorMessage, { code: this.getTimeoutErrorCode() }) - } - - return { - newFiles: [], - deletedFiles: [], - references: [], - codeGenerationRemainingIterationCount: codeGenerationRemainingIterationCount, - codeGenerationTotalIterationCount: codeGenerationTotalIterationCount, - } - } - - protected abstract handleError(messenger: BaseMessenger, codegenResult: any): Error -} - -export abstract class BasePrepareCodeGenState implements SessionState { - public tokenSource: vscode.CancellationTokenSource - public readonly phase = DevPhase.CODEGEN - public uploadId: string - public conversationId: string - - constructor( - protected config: SessionStateConfig, - public filePaths: NewFileInfo[], - public deletedFiles: DeletedFileInfo[], - public references: CodeReference[], - public tabID: string, - public currentIteration: number, - public codeGenerationRemainingIterationCount?: number, - public codeGenerationTotalIterationCount?: number, - public uploadHistory: UploadHistory = {}, - public superTokenSource: vscode.CancellationTokenSource = new vscode.CancellationTokenSource(), - public currentCodeGenerationId?: string, - public codeGenerationId?: string - ) { - this.tokenSource = superTokenSource || new vscode.CancellationTokenSource() - this.uploadId = config.uploadId - this.currentCodeGenerationId = currentCodeGenerationId - this.conversationId = config.conversationId - this.uploadHistory = uploadHistory - this.codeGenerationId = codeGenerationId - } - - updateWorkspaceRoot(workspaceRoot: string) { - this.config.workspaceRoots = [workspaceRoot] - } - - protected createNextState( - config: SessionStateConfig, - StateClass?: new ( - config: SessionStateConfig, - filePaths: NewFileInfo[], - deletedFiles: DeletedFileInfo[], - references: CodeReference[], - tabID: string, - currentIteration: number, - uploadHistory: UploadHistory, - codeGenerationRemainingIterationCount?: number, - codeGenerationTotalIterationCount?: number - ) => SessionState - ): SessionState { - return new StateClass!( - config, - this.filePaths, - this.deletedFiles, - this.references, - this.tabID, - this.currentIteration, - this.uploadHistory - ) - } - - protected abstract preUpload(action: SessionStateAction): void - protected abstract postUpload(action: SessionStateAction): void - - async interact(action: SessionStateAction): Promise { - this.preUpload(action) - const uploadId = await telemetry.amazonq_createUpload.run(async (span) => { - span.record({ - amazonqConversationId: this.config.conversationId, - credentialStartUrl: AuthUtil.instance.startUrl, - }) - const { zipFileBuffer, zipFileChecksum } = await this.prepareProjectZip( - this.config.workspaceRoots, - this.config.workspaceFolders, - span, - { telemetry: action.telemetry } - ) - const uploadId = randomUUID() - const { uploadUrl, kmsKeyArn } = await this.config.proxyClient.createUploadUrl( - this.config.conversationId, - zipFileChecksum, - zipFileBuffer.length, - uploadId - ) - - await uploadCode(uploadUrl, zipFileBuffer, zipFileChecksum, kmsKeyArn) - this.postUpload(action) - - return uploadId - }) - - this.uploadId = uploadId - const nextState = this.createNextState({ ...this.config, uploadId }) - return nextState.interact(action) - } - - protected async prepareProjectZip( - workspaceRoots: string[], - workspaceFolders: CurrentWsFolders, - span: Span, - options: PrepareRepoDataOptions - ) { - return await prepareRepoData(workspaceRoots, workspaceFolders, span, options) - } -} - -export interface CodeGenerationParams { - messenger: BaseMessenger - fs: VirtualFileSystem - codeGenerationId: string - telemetry: any - workspaceFolders: CurrentWsFolders -} - -export interface CreateNextStateParams { - filePaths: NewFileInfo[] - deletedFiles: DeletedFileInfo[] - references: CodeReference[] - currentIteration: number - remainingIterations?: number - totalIterations?: number - uploadHistory: UploadHistory - tokenSource: vscode.CancellationTokenSource - currentCodeGenerationId?: string - codeGenerationId?: string -} - -export abstract class BaseCodeGenState extends CodeGenBase implements SessionState { - constructor( - config: SessionStateConfig, - public filePaths: NewFileInfo[], - public deletedFiles: DeletedFileInfo[], - public references: CodeReference[], - tabID: string, - public currentIteration: number, - public uploadHistory: UploadHistory, - public codeGenerationRemainingIterationCount?: number, - public codeGenerationTotalIterationCount?: number - ) { - super(config, tabID) - } - - protected createNextState( - config: SessionStateConfig, - params: CreateNextStateParams, - StateClass?: new ( - config: SessionStateConfig, - filePaths: NewFileInfo[], - deletedFiles: DeletedFileInfo[], - references: CodeReference[], - tabID: string, - currentIteration: number, - remainingIterations?: number, - totalIterations?: number, - uploadHistory?: UploadHistory, - tokenSource?: vscode.CancellationTokenSource, - currentCodeGenerationId?: string, - codeGenerationId?: string - ) => SessionState - ): SessionState { - return new StateClass!( - config, - params.filePaths, - params.deletedFiles, - params.references, - this.tabID, - params.currentIteration, - params.remainingIterations, - params.totalIterations, - params.uploadHistory, - params.tokenSource, - params.currentCodeGenerationId, - params.codeGenerationId - ) - } - - async interact(action: SessionStateAction): Promise { - return telemetry.amazonq_codeGenerationInvoke.run(async (span) => { - try { - action.tokenSource?.token.onCancellationRequested(() => { - this.isCancellationRequested = true - if (action.tokenSource) { - this.tokenSource = action.tokenSource - } - }) - - span.record({ - amazonqConversationId: this.config.conversationId, - credentialStartUrl: AuthUtil.instance.startUrl, - }) - - action.telemetry.setGenerateCodeIteration(this.currentIteration) - action.telemetry.setGenerateCodeLastInvocationTime() - - const codeGenerationId = randomUUID() - await this.startCodeGeneration(action, codeGenerationId) - - const codeGeneration = await this.generateCode({ - messenger: action.messenger, - fs: action.fs, - codeGenerationId, - telemetry: action.telemetry, - workspaceFolders: this.config.workspaceFolders, - action, - }) - - if (codeGeneration && !action.tokenSource?.token.isCancellationRequested) { - this.config.currentCodeGenerationId = codeGenerationId - this.currentCodeGenerationId = codeGenerationId - } - - this.filePaths = codeGeneration.newFiles - this.deletedFiles = codeGeneration.deletedFiles - this.references = codeGeneration.references - this.codeGenerationRemainingIterationCount = codeGeneration.codeGenerationRemainingIterationCount - this.codeGenerationTotalIterationCount = codeGeneration.codeGenerationTotalIterationCount - this.currentIteration = - this.codeGenerationRemainingIterationCount && this.codeGenerationTotalIterationCount - ? this.codeGenerationTotalIterationCount - this.codeGenerationRemainingIterationCount - : this.currentIteration + 1 - - if (action.uploadHistory && !action.uploadHistory[codeGenerationId] && codeGenerationId) { - action.uploadHistory[codeGenerationId] = { - timestamp: Date.now(), - uploadId: this.config.uploadId, - filePaths: codeGeneration.newFiles, - deletedFiles: codeGeneration.deletedFiles, - tabId: this.tabID, - } - } - - action.telemetry.setAmazonqNumberOfReferences(this.references.length) - action.telemetry.recordUserCodeGenerationTelemetry(span, this.conversationId) - - const nextState = this.createNextState(this.config, { - filePaths: this.filePaths, - deletedFiles: this.deletedFiles, - references: this.references, - currentIteration: this.currentIteration, - remainingIterations: this.codeGenerationRemainingIterationCount, - totalIterations: this.codeGenerationTotalIterationCount, - uploadHistory: action.uploadHistory ? action.uploadHistory : {}, - tokenSource: this.tokenSource, - currentCodeGenerationId: this.currentCodeGenerationId, - codeGenerationId, - }) - - return { - nextState, - interaction: {}, - } - } catch (e) { - throw e instanceof ToolkitError - ? e - : ToolkitError.chain(e, 'Server side error', { code: 'UnhandledCodeGenServerSideError' }) - } - }) - } - - protected abstract startCodeGeneration(action: SessionStateAction, codeGenerationId: string): Promise -} diff --git a/packages/core/src/amazonq/util/files.ts b/packages/core/src/amazonq/util/files.ts deleted file mode 100644 index afa0b674928..00000000000 --- a/packages/core/src/amazonq/util/files.ts +++ /dev/null @@ -1,301 +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 path from 'path' -import { - collectFiles, - CollectFilesFilter, - defaultExcludePatterns, - getWorkspaceFoldersByPrefixes, -} from '../../shared/utilities/workspaceUtils' - -import { PrepareRepoFailedError } from '../../amazonqFeatureDev/errors' -import { getLogger } from '../../shared/logger/logger' -import { maxFileSizeBytes } from '../../amazonqFeatureDev/limits' -import { CurrentWsFolders, DeletedFileInfo, NewFileInfo, NewFileZipContents } from '../../amazonqDoc/types' -import { ContentLengthError, hasCode, ToolkitError } from '../../shared/errors' -import { AmazonqCreateUpload, Span, telemetry as amznTelemetry, telemetry } from '../../shared/telemetry/telemetry' -import { maxRepoSizeBytes } from '../../amazonqFeatureDev/constants' -import { isCodeFile } from '../../shared/filetypes' -import { fs } from '../../shared/fs/fs' -import { VirtualFileSystem } from '../../shared/virtualFilesystem' -import { VirtualMemoryFile } from '../../shared/virtualMemoryFile' -import { CodeWhispererSettings } from '../../codewhisperer/util/codewhispererSettings' -import { ZipStream } from '../../shared/utilities/zipStream' -import { isPresent } from '../../shared/utilities/collectionUtils' -import { AuthUtil } from '../../codewhisperer/util/authUtil' -import { TelemetryHelper } from '../util/telemetryHelper' - -export const SvgFileExtension = '.svg' - -export async function checkForDevFile(root: string) { - const devFilePath = root + '/devfile.yaml' - const hasDevFile = await fs.existsFile(devFilePath) - return hasDevFile -} - -function isInfraDiagramFile(relativePath: string) { - return ( - relativePath.toLowerCase().endsWith(path.join('docs', 'infra.dot')) || - relativePath.toLowerCase().endsWith(path.join('docs', 'infra.svg')) - ) -} - -export type PrepareRepoDataOptions = { - telemetry?: TelemetryHelper - zip?: ZipStream - isIncludeInfraDiagram?: boolean -} - -/** - * given the root path of the repo it zips its files in memory and generates a checksum for it. - */ -export async function prepareRepoData( - repoRootPaths: string[], - workspaceFolders: CurrentWsFolders, - span: Span, - options?: PrepareRepoDataOptions -) { - try { - const telemetry = options?.telemetry - const isIncludeInfraDiagram = options?.isIncludeInfraDiagram ?? false - const zip = options?.zip ?? new ZipStream() - - const autoBuildSetting = CodeWhispererSettings.instance.getAutoBuildSetting() - const useAutoBuildFeature = autoBuildSetting[repoRootPaths[0]] ?? false - const excludePatterns: string[] = [] - let filterFn: CollectFilesFilter | undefined = undefined - - // We only respect gitignore file rules if useAutoBuildFeature is on, this is to avoid dropping necessary files for building the code (e.g. png files imported in js code) - if (!useAutoBuildFeature) { - if (isIncludeInfraDiagram) { - // ensure svg is not filtered out by files search - excludePatterns.push(...defaultExcludePatterns.filter((p) => !p.endsWith(SvgFileExtension))) - // ensure only infra diagram is included from all svg files - filterFn = (relativePath: string) => { - if (!relativePath.toLowerCase().endsWith(SvgFileExtension)) { - return false - } - return !isInfraDiagramFile(relativePath) - } - } else { - excludePatterns.push(...defaultExcludePatterns) - } - } - - const files = await collectFiles(repoRootPaths, workspaceFolders, { - maxTotalSizeBytes: maxRepoSizeBytes, - excludeByGitIgnore: true, - excludePatterns: excludePatterns, - filterFn: filterFn, - }) - - let totalBytes = 0 - const ignoredExtensionMap = new Map() - for (const file of files) { - let fileSize - try { - fileSize = (await fs.stat(file.fileUri)).size - } catch (error) { - if (hasCode(error) && error.code === 'ENOENT') { - // No-op: Skip if file does not exist - continue - } - throw error - } - const isCodeFile_ = isCodeFile(file.relativeFilePath) - const isDevFile = file.relativeFilePath === 'devfile.yaml' - const isInfraDiagramFileExt = isInfraDiagramFile(file.relativeFilePath) - - let isExcludeFile = fileSize >= maxFileSizeBytes - // When useAutoBuildFeature is on, only respect the gitignore rules filtered earlier and apply the size limit - if (!isExcludeFile && !useAutoBuildFeature) { - isExcludeFile = isDevFile || (!isCodeFile_ && (!isIncludeInfraDiagram || !isInfraDiagramFileExt)) - } - - if (isExcludeFile) { - if (!isCodeFile_) { - const re = /(?:\.([^.]+))?$/ - const extensionArray = re.exec(file.relativeFilePath) - const extension = extensionArray?.length ? extensionArray[1] : undefined - if (extension) { - const currentCount = ignoredExtensionMap.get(extension) - - ignoredExtensionMap.set(extension, (currentCount ?? 0) + 1) - } - } - continue - } - - totalBytes += fileSize - // Paths in zip should be POSIX compliant regardless of OS - // Reference: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT - const posixPath = file.zipFilePath.split(path.sep).join(path.posix.sep) - - try { - zip.writeFile(file.fileUri.fsPath, posixPath) - } catch (error) { - if (error instanceof Error && error.message.includes('File not found')) { - // No-op: Skip if file was deleted or does not exist - // Reference: https://github.com/cthackers/adm-zip/blob/1cd32f7e0ad3c540142a76609bb538a5cda2292f/adm-zip.js#L296-L321 - continue - } - throw error - } - } - - const iterator = ignoredExtensionMap.entries() - - for (let i = 0; i < ignoredExtensionMap.size; i++) { - const iteratorValue = iterator.next().value - if (iteratorValue) { - const [key, value] = iteratorValue - await amznTelemetry.amazonq_bundleExtensionIgnored.run(async (bundleSpan) => { - const event = { - filenameExt: key, - count: value, - } - - bundleSpan.record(event) - }) - } - } - - if (telemetry) { - telemetry.setRepositorySize(totalBytes) - } - - span.record({ amazonqRepositorySize: totalBytes }) - const zipResult = await zip.finalize() - - const zipFileBuffer = zipResult.streamBuffer.getContents() || Buffer.from('') - return { - zipFileBuffer, - zipFileChecksum: zipResult.hash, - } - } catch (error) { - getLogger().debug(`Failed to prepare repo: ${error}`) - if (error instanceof ToolkitError && error.code === 'ContentLengthError') { - throw new ContentLengthError(error.message) - } - throw new PrepareRepoFailedError() - } -} - -/** - * gets the absolute path from a zip path - * @param zipFilePath the path in the zip file - * @param workspacesByPrefix the workspaces with generated prefixes - * @param workspaceFolders all workspace folders - * @returns all possible path info - */ -export function getPathsFromZipFilePath( - zipFilePath: string, - workspacesByPrefix: { [prefix: string]: vscode.WorkspaceFolder } | undefined, - workspaceFolders: CurrentWsFolders -): { - absolutePath: string - relativePath: string - workspaceFolder: vscode.WorkspaceFolder -} { - // when there is just a single workspace folder, there is no prefixing - if (workspacesByPrefix === undefined) { - return { - absolutePath: path.join(workspaceFolders[0].uri.fsPath, zipFilePath), - relativePath: zipFilePath, - workspaceFolder: workspaceFolders[0], - } - } - // otherwise the first part of the zipPath is the prefix - const prefix = zipFilePath.substring(0, zipFilePath.indexOf(path.sep)) - const workspaceFolder = - workspacesByPrefix[prefix] ?? - (workspacesByPrefix[Object.values(workspacesByPrefix).find((val) => val.index === 0)?.name ?? ''] || undefined) - if (workspaceFolder === undefined) { - throw new ToolkitError(`Could not find workspace folder for prefix ${prefix}`) - } - return { - absolutePath: path.join(workspaceFolder.uri.fsPath, zipFilePath.substring(prefix.length + 1)), - relativePath: zipFilePath.substring(prefix.length + 1), - workspaceFolder, - } -} - -export function getDeletedFileInfos(deletedFiles: string[], workspaceFolders: CurrentWsFolders): DeletedFileInfo[] { - const workspaceFolderPrefixes = getWorkspaceFoldersByPrefixes(workspaceFolders) - return deletedFiles - .map((deletedFilePath) => { - const prefix = - workspaceFolderPrefixes === undefined - ? '' - : deletedFilePath.substring(0, deletedFilePath.indexOf(path.sep)) - const folder = workspaceFolderPrefixes === undefined ? workspaceFolders[0] : workspaceFolderPrefixes[prefix] - if (folder === undefined) { - getLogger().error(`No workspace folder found for file: ${deletedFilePath} and prefix: ${prefix}`) - return undefined - } - const prefixLength = workspaceFolderPrefixes === undefined ? 0 : prefix.length + 1 - return { - zipFilePath: deletedFilePath, - workspaceFolder: folder, - relativePath: deletedFilePath.substring(prefixLength), - rejected: false, - changeApplied: false, - } - }) - .filter(isPresent) -} - -export function registerNewFiles( - fs: VirtualFileSystem, - newFileContents: NewFileZipContents[], - uploadId: string, - workspaceFolders: CurrentWsFolders, - conversationId: string, - scheme: string -): NewFileInfo[] { - const result: NewFileInfo[] = [] - const workspaceFolderPrefixes = getWorkspaceFoldersByPrefixes(workspaceFolders) - for (const { zipFilePath, fileContent } of newFileContents) { - const encoder = new TextEncoder() - const contents = encoder.encode(fileContent) - const generationFilePath = path.join(uploadId, zipFilePath) - const uri = vscode.Uri.from({ scheme, path: generationFilePath }) - fs.registerProvider(uri, new VirtualMemoryFile(contents)) - const prefix = - workspaceFolderPrefixes === undefined ? '' : zipFilePath.substring(0, zipFilePath.indexOf(path.sep)) - const folder = - workspaceFolderPrefixes === undefined - ? workspaceFolders[0] - : (workspaceFolderPrefixes[prefix] ?? - workspaceFolderPrefixes[ - Object.values(workspaceFolderPrefixes).find((val) => val.index === 0)?.name ?? '' - ]) - if (folder === undefined) { - telemetry.toolkit_trackScenario.emit({ - count: 1, - amazonqConversationId: conversationId, - credentialStartUrl: AuthUtil.instance.startUrl, - scenario: 'wsOrphanedDocuments', - }) - getLogger().error(`No workspace folder found for file: ${zipFilePath} and prefix: ${prefix}`) - continue - } - result.push({ - zipFilePath, - fileContent, - virtualMemoryUri: uri, - workspaceFolder: folder, - relativePath: zipFilePath.substring( - workspaceFolderPrefixes === undefined ? 0 : prefix.length > 0 ? prefix.length + 1 : 0 - ), - rejected: false, - changeApplied: false, - }) - } - - return result -} diff --git a/packages/core/src/amazonq/util/upload.ts b/packages/core/src/amazonq/util/upload.ts index bd4ff26cc45..92e88fbea0e 100644 --- a/packages/core/src/amazonq/util/upload.ts +++ b/packages/core/src/amazonq/util/upload.ts @@ -5,11 +5,10 @@ import request, { RequestError } from '../../shared/request' import { getLogger } from '../../shared/logger/logger' -import { featureName } from '../../amazonqFeatureDev/constants' -import { UploadCodeError, UploadURLExpired } from '../../amazonqFeatureDev/errors' -import { ToolkitError } from '../../shared/errors' +import { ToolkitError, UploadCodeError, UploadURLExpired } from '../../shared/errors' import { i18n } from '../../shared/i18n-helper' +import { featureName } from '../../shared/constants' /** * uploadCode diff --git a/packages/core/src/amazonq/webview/ui/apps/amazonqCommonsConnector.ts b/packages/core/src/amazonq/webview/ui/apps/amazonqCommonsConnector.ts index 04ed6907795..ee20b9b0726 100644 --- a/packages/core/src/amazonq/webview/ui/apps/amazonqCommonsConnector.ts +++ b/packages/core/src/amazonq/webview/ui/apps/amazonqCommonsConnector.ts @@ -7,13 +7,7 @@ import { ChatItem, ChatItemAction, ChatItemType, ChatPrompt } from '@aws/mynah-u import { ExtensionMessage } from '../commands' import { AuthFollowUpType } from '../followUps/generator' import { getTabCommandFromTabType, isTabType, TabType } from '../storages/tabsStorage' -import { - docUserGuide, - userGuideURL as featureDevUserGuide, - helpMessage, - reviewGuideUrl, - testGuideUrl, -} from '../texts/constants' +import { helpMessage, reviewGuideUrl } from '../texts/constants' import { linkToDocsHome } from '../../../../codewhisperer/models/constants' import { createClickTelemetry, createOpenAgentTelemetry } from '../telemetry/actions' @@ -39,14 +33,12 @@ export interface CodeReference { export class Connector { private readonly sendMessageToExtension private readonly onWelcomeFollowUpClicked - private readonly onNewTab private readonly handleCommand private readonly sendStaticMessage constructor(props: ConnectorProps) { this.sendMessageToExtension = props.sendMessageToExtension this.onWelcomeFollowUpClicked = props.onWelcomeFollowUpClicked - this.onNewTab = props.onNewTab this.handleCommand = props.handleCommand this.sendStaticMessage = props.sendStaticMessages } @@ -67,10 +59,7 @@ export class Connector { } handleMessageReceive = async (messageData: any): Promise => { - if (messageData.command === 'showExploreAgentsView') { - this.onNewTab('agentWalkthrough') - return - } else if (messageData.command === 'review') { + if (messageData.command === 'review') { // tabID does not exist when calling from QuickAction Menu bar this.handleCommand({ command: '/review' }, '') return @@ -110,18 +99,9 @@ export class Connector { private processUserGuideLink(tabType: TabType, actionId: string) { let userGuideLink = '' switch (tabType) { - case 'featuredev': - userGuideLink = featureDevUserGuide - break - case 'testgen': - userGuideLink = testGuideUrl - break case 'review': userGuideLink = reviewGuideUrl break - case 'doc': - userGuideLink = docUserGuide - break case 'gumby': userGuideLink = linkToDocsHome break diff --git a/packages/core/src/amazonq/webview/ui/apps/docChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/docChatConnector.ts deleted file mode 100644 index 96822c8336c..00000000000 --- a/packages/core/src/amazonq/webview/ui/apps/docChatConnector.ts +++ /dev/null @@ -1,226 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ChatItem, ChatItemType, FeedbackPayload, MynahIcons, ProgressField } from '@aws/mynah-ui' -import { TabType } from '../storages/tabsStorage' -import { DiffTreeFileInfo } from '../diffTree/types' -import { BaseConnectorProps, BaseConnector } from './baseConnector' - -export interface ConnectorProps extends BaseConnectorProps { - onAsyncEventProgress: (tabID: string, inProgress: boolean, message: string) => void - sendFeedback?: (tabId: string, feedbackPayload: FeedbackPayload) => void | undefined - onFileComponentUpdate: ( - tabID: string, - filePaths: DiffTreeFileInfo[], - deletedFiles: DiffTreeFileInfo[], - messageId: string, - disableFileActions: boolean - ) => void - onFileActionClick: (tabID: string, messageId: string, filePath: string, actionName: string) => void - onUpdatePlaceholder: (tabID: string, newPlaceholder: string) => void - onUpdatePromptProgress: (tabID: string, progressField: ProgressField) => void - onChatInputEnabled: (tabID: string, enabled: boolean) => void - onUpdateAuthentication: (featureDevEnabled: boolean, authenticatingTabIDs: string[]) => void -} - -export class Connector extends BaseConnector { - private readonly onFileComponentUpdate - private readonly onAsyncEventProgress - private readonly updatePlaceholder - private readonly chatInputEnabled - private readonly onUpdateAuthentication - private readonly updatePromptProgress - - override getTabType(): TabType { - return 'doc' - } - - constructor(props: ConnectorProps) { - super(props) - this.onFileComponentUpdate = props.onFileComponentUpdate - this.onAsyncEventProgress = props.onAsyncEventProgress - this.updatePlaceholder = props.onUpdatePlaceholder - this.chatInputEnabled = props.onChatInputEnabled - this.onUpdateAuthentication = props.onUpdateAuthentication - this.updatePromptProgress = props.onUpdatePromptProgress - } - - onOpenDiff = (tabID: string, filePath: string, deleted: boolean): void => { - this.sendMessageToExtension({ - command: 'open-diff', - tabID, - filePath, - deleted, - tabType: this.getTabType(), - }) - } - onFileActionClick = (tabID: string, messageId: string, filePath: string, actionName: string): void => { - this.sendMessageToExtension({ - command: 'file-click', - tabID, - messageId, - filePath, - actionName, - tabType: this.getTabType(), - }) - } - - private processFolderConfirmationMessage = async (messageData: any, folderPath: string): Promise => { - if (this.onChatAnswerReceived !== undefined) { - const answer: ChatItem = { - type: ChatItemType.ANSWER, - body: messageData.message ?? undefined, - messageId: messageData.messageID ?? messageData.triggerID ?? '', - fileList: { - rootFolderTitle: undefined, - fileTreeTitle: '', - filePaths: [folderPath], - details: { - [folderPath]: { - icon: MynahIcons.FOLDER, - clickable: false, - }, - }, - }, - followUp: { - text: '', - options: messageData.followUps, - }, - } - this.onChatAnswerReceived(messageData.tabID, answer, messageData) - } - } - - private processChatMessage = async (messageData: any): Promise => { - if (this.onChatAnswerReceived !== undefined) { - const answer: ChatItem = { - type: messageData.messageType, - body: messageData.message ?? undefined, - messageId: messageData.messageID ?? messageData.triggerID ?? '', - relatedContent: undefined, - canBeVoted: messageData.canBeVoted, - snapToTop: messageData.snapToTop, - followUp: - messageData.followUps !== undefined && messageData.followUps.length > 0 - ? { - text: - messageData.messageType === ChatItemType.SYSTEM_PROMPT - ? '' - : 'Select one of the following...', - options: messageData.followUps, - } - : undefined, - } - this.onChatAnswerReceived(messageData.tabID, answer, messageData) - } - } - - private processCodeResultMessage = async (messageData: any): Promise => { - if (this.onChatAnswerReceived !== undefined) { - const answer: ChatItem = { - type: ChatItemType.ANSWER, - relatedContent: undefined, - followUp: undefined, - canBeVoted: false, - codeReference: messageData.references, - // TODO get the backend to store a message id in addition to conversationID - messageId: - messageData.codeGenerationId ?? - messageData.messageID ?? - messageData.triggerID ?? - messageData.conversationID, - fileList: { - rootFolderTitle: 'Documentation', - fileTreeTitle: 'Documents ready', - filePaths: messageData.filePaths.map((f: DiffTreeFileInfo) => f.zipFilePath), - deletedFiles: messageData.deletedFiles.map((f: DiffTreeFileInfo) => f.zipFilePath), - }, - body: '', - } - this.onChatAnswerReceived(messageData.tabID, answer, messageData) - } - } - - handleMessageReceive = async (messageData: any): Promise => { - if (messageData.type === 'updateFileComponent') { - this.onFileComponentUpdate( - messageData.tabID, - messageData.filePaths, - messageData.deletedFiles, - messageData.messageId, - messageData.disableFileActions - ) - return - } - - if (messageData.type === 'chatMessage') { - await this.processChatMessage(messageData) - return - } - - if (messageData.type === 'folderConfirmationMessage') { - await this.processFolderConfirmationMessage(messageData, messageData.folderPath) - return - } - - if (messageData.type === 'codeResultMessage') { - await this.processCodeResultMessage(messageData) - return - } - - if (messageData.type === 'asyncEventProgressMessage') { - this.onAsyncEventProgress(messageData.tabID, messageData.inProgress, messageData.message ?? undefined) - return - } - - if (messageData.type === 'updatePlaceholderMessage') { - this.updatePlaceholder(messageData.tabID, messageData.newPlaceholder) - return - } - - if (messageData.type === 'chatInputEnabledMessage') { - this.chatInputEnabled(messageData.tabID, messageData.enabled) - return - } - - if (messageData.type === 'authenticationUpdateMessage') { - this.onUpdateAuthentication(messageData.featureEnabled, messageData.authenticatingTabIDs) - return - } - - if (messageData.type === 'openNewTabMessage') { - this.onNewTab(this.getTabType()) - return - } - - if (messageData.type === 'updatePromptProgress') { - this.updatePromptProgress(messageData.tabID, messageData.progressField) - return - } - - // For other message types, call the base class handleMessageReceive - await this.baseHandleMessageReceive(messageData) - } - - onCustomFormAction( - tabId: string, - action: { - id: string - text?: string | undefined - formItemValues?: Record | undefined - } - ) { - if (action === undefined) { - return - } - this.sendMessageToExtension({ - command: 'form-action-click', - action: action.id, - formSelectedValues: action.formItemValues, - tabType: 'doc', - tabID: tabId, - }) - } -} diff --git a/packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts deleted file mode 100644 index 1f6d33a1ec4..00000000000 --- a/packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts +++ /dev/null @@ -1,212 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ChatItem, ChatItemType, FeedbackPayload } from '@aws/mynah-ui' -import { TabType } from '../storages/tabsStorage' -import { getActions } from '../diffTree/actions' -import { DiffTreeFileInfo } from '../diffTree/types' -import { BaseConnector, BaseConnectorProps } from './baseConnector' - -export interface ConnectorProps extends BaseConnectorProps { - onAsyncEventProgress: ( - tabID: string, - inProgress: boolean, - message: string, - messageId: string | undefined, - enableStopAction: boolean - ) => void - onChatAnswerUpdated?: (tabID: string, message: ChatItem) => void - sendFeedback?: (tabId: string, feedbackPayload: FeedbackPayload) => void | undefined - onFileComponentUpdate: ( - tabID: string, - filePaths: DiffTreeFileInfo[], - deletedFiles: DiffTreeFileInfo[], - messageId: string, - disableFileActions: boolean - ) => void - onFileActionClick: (tabID: string, messageId: string, filePath: string, actionName: string) => void - onUpdatePlaceholder: (tabID: string, newPlaceholder: string) => void - onChatInputEnabled: (tabID: string, enabled: boolean) => void - onUpdateAuthentication: (featureDevEnabled: boolean, authenticatingTabIDs: string[]) => void -} - -export class Connector extends BaseConnector { - private readonly onFileComponentUpdate - private readonly onChatAnswerUpdated - private readonly onAsyncEventProgress - private readonly updatePlaceholder - private readonly chatInputEnabled - private readonly onUpdateAuthentication - - override getTabType(): TabType { - return 'featuredev' - } - - constructor(props: ConnectorProps) { - super(props) - this.onFileComponentUpdate = props.onFileComponentUpdate - this.onAsyncEventProgress = props.onAsyncEventProgress - this.updatePlaceholder = props.onUpdatePlaceholder - this.chatInputEnabled = props.onChatInputEnabled - this.onUpdateAuthentication = props.onUpdateAuthentication - this.onChatAnswerUpdated = props.onChatAnswerUpdated - } - - onOpenDiff = (tabID: string, filePath: string, deleted: boolean, messageId?: string): void => { - this.sendMessageToExtension({ - command: 'open-diff', - tabID, - filePath, - deleted, - messageId, - tabType: this.getTabType(), - }) - } - onFileActionClick = (tabID: string, messageId: string, filePath: string, actionName: string): void => { - this.sendMessageToExtension({ - command: 'file-click', - tabID, - messageId, - filePath, - actionName, - tabType: this.getTabType(), - }) - } - - private createAnswer = (messageData: any): ChatItem => { - return { - type: messageData.messageType, - body: messageData.message ?? undefined, - messageId: messageData.messageId ?? messageData.messageID ?? messageData.triggerID ?? '', - relatedContent: undefined, - canBeVoted: messageData.canBeVoted ?? undefined, - snapToTop: messageData.snapToTop ?? undefined, - followUp: - messageData.followUps !== undefined && Array.isArray(messageData.followUps) - ? { - text: - messageData.messageType === ChatItemType.SYSTEM_PROMPT || - messageData.followUps.length === 0 - ? '' - : 'Please follow up with one of these', - options: messageData.followUps, - } - : undefined, - } - } - - private processChatMessage = async (messageData: any): Promise => { - if (this.onChatAnswerReceived !== undefined) { - const answer = this.createAnswer(messageData) - this.onChatAnswerReceived(messageData.tabID, answer, messageData) - } - } - - private processCodeResultMessage = async (messageData: any): Promise => { - if (this.onChatAnswerReceived !== undefined) { - const messageId = - messageData.codeGenerationId ?? - messageData.messageId ?? - messageData.messageID ?? - messageData.triggerID ?? - messageData.conversationID - this.sendMessageToExtension({ - tabID: messageData.tabID, - command: 'store-code-result-message-id', - messageId, - tabType: 'featuredev', - }) - const actions = getActions([...messageData.filePaths, ...messageData.deletedFiles]) - const answer: ChatItem = { - type: ChatItemType.ANSWER, - relatedContent: undefined, - followUp: undefined, - canBeVoted: true, - codeReference: messageData.references, - messageId, - fileList: { - rootFolderTitle: 'Changes', - filePaths: messageData.filePaths.map((f: DiffTreeFileInfo) => f.zipFilePath), - deletedFiles: messageData.deletedFiles.map((f: DiffTreeFileInfo) => f.zipFilePath), - actions, - }, - body: '', - } - this.onChatAnswerReceived(messageData.tabID, answer, messageData) - } - } - - handleMessageReceive = async (messageData: any): Promise => { - if (messageData.type === 'updateFileComponent') { - this.onFileComponentUpdate( - messageData.tabID, - messageData.filePaths, - messageData.deletedFiles, - messageData.messageId, - messageData.disableFileActions - ) - return - } - if (messageData.type === 'updateChatAnswer') { - const answer = this.createAnswer(messageData) - this.onChatAnswerUpdated?.(messageData.tabID, answer) - return - } - - if (messageData.type === 'chatMessage') { - await this.processChatMessage(messageData) - return - } - - if (messageData.type === 'codeResultMessage') { - await this.processCodeResultMessage(messageData) - return - } - - if (messageData.type === 'asyncEventProgressMessage') { - const enableStopAction = true - this.onAsyncEventProgress( - messageData.tabID, - messageData.inProgress, - messageData.message ?? undefined, - messageData.messageId ?? undefined, - enableStopAction - ) - return - } - - if (messageData.type === 'updatePlaceholderMessage') { - this.updatePlaceholder(messageData.tabID, messageData.newPlaceholder) - return - } - - if (messageData.type === 'chatInputEnabledMessage') { - this.chatInputEnabled(messageData.tabID, messageData.enabled) - return - } - - if (messageData.type === 'authenticationUpdateMessage') { - this.onUpdateAuthentication(messageData.featureEnabled, messageData.authenticatingTabIDs) - return - } - - if (messageData.type === 'openNewTabMessage') { - this.onNewTab('featuredev') - return - } - - // For other message types, call the base class handleMessageReceive - await this.baseHandleMessageReceive(messageData) - } - - sendFeedback = (tabId: string, feedbackPayload: FeedbackPayload): void | undefined => { - this.sendMessageToExtension({ - command: 'chat-item-feedback', - ...feedbackPayload, - tabType: this.getTabType(), - tabID: tabId, - }) - } -} diff --git a/packages/core/src/amazonq/webview/ui/apps/testChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/testChatConnector.ts deleted file mode 100644 index 35fb0bc0683..00000000000 --- a/packages/core/src/amazonq/webview/ui/apps/testChatConnector.ts +++ /dev/null @@ -1,293 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * This class is responsible for listening to and processing events - * from the webview and translating them into events to be handled by the extension, - * and events from the extension and translating them into events to be handled by the webview. - */ - -import { ChatItem, ChatItemType, MynahIcons, ProgressField } from '@aws/mynah-ui' -import { ExtensionMessage } from '../commands' -import { TabsStorage, TabType } from '../storages/tabsStorage' -import { TestMessageType } from '../../../../amazonqTest/chat/views/connector/connector' -import { ChatPayload } from '../connector' -import { BaseConnector, BaseConnectorProps } from './baseConnector' -import { FollowUpTypes } from '../../../commons/types' - -export interface ConnectorProps extends BaseConnectorProps { - sendMessageToExtension: (message: ExtensionMessage) => void - onChatAnswerReceived?: (tabID: string, message: ChatItem, messageData: any) => void - onRunTestMessageReceived?: (tabID: string, showRunTestMessage: boolean) => void - onChatAnswerUpdated?: (tabID: string, message: ChatItem) => void - onQuickHandlerCommand: (tabID: string, command: string, eventId?: string) => void - onWarning: (tabID: string, message: string, title: string) => void - onError: (tabID: string, message: string, title: string) => void - onUpdateAuthentication: (testEnabled: boolean, authenticatingTabIDs: string[]) => void - onChatInputEnabled: (tabID: string, enabled: boolean) => void - onUpdatePlaceholder: (tabID: string, newPlaceholder: string) => void - onUpdatePromptProgress: (tabID: string, progressField: ProgressField) => void - tabsStorage: TabsStorage -} - -export interface MessageData { - tabID: string - type: TestMessageType -} -// TODO: Refactor testChatConnector, scanChatConnector and other apps connector files post RIV -export class Connector extends BaseConnector { - override getTabType(): TabType { - return 'testgen' - } - readonly onAuthenticationUpdate - override readonly sendMessageToExtension - override readonly onChatAnswerReceived - private readonly onChatAnswerUpdated - private readonly chatInputEnabled - private readonly updatePlaceholder - private readonly updatePromptProgress - override readonly onError - private readonly tabStorage - private readonly runTestMessageReceived - - constructor(props: ConnectorProps) { - super(props) - this.runTestMessageReceived = props.onRunTestMessageReceived - this.sendMessageToExtension = props.sendMessageToExtension - this.onChatAnswerReceived = props.onChatAnswerReceived - this.onChatAnswerUpdated = props.onChatAnswerUpdated - this.chatInputEnabled = props.onChatInputEnabled - this.updatePlaceholder = props.onUpdatePlaceholder - this.updatePromptProgress = props.onUpdatePromptProgress - this.onAuthenticationUpdate = props.onUpdateAuthentication - this.onError = props.onError - this.tabStorage = props.tabsStorage - } - - startTestGen(tabID: string, prompt: string) { - this.sendMessageToExtension({ - tabID: tabID, - command: 'start-test-gen', - tabType: 'testgen', - prompt, - }) - } - - requestAnswer = (tabID: string, payload: ChatPayload) => { - this.tabStorage.updateTabStatus(tabID, 'busy') - this.sendMessageToExtension({ - tabID: tabID, - command: 'chat-prompt', - chatMessage: payload.chatMessage, - chatCommand: payload.chatCommand, - tabType: 'testgen', - }) - } - - onCustomFormAction( - tabId: string, - messageId: string, - action: { - id: string - text?: string | undefined - description?: string | undefined - formItemValues?: Record | undefined - } - ) { - if (action === undefined) { - return - } - - this.sendMessageToExtension({ - command: 'form-action-click', - action: action.id, - formSelectedValues: action.formItemValues, - tabType: 'testgen', - tabID: tabId, - description: action.description, - }) - - if (this.onChatAnswerUpdated === undefined) { - return - } - const answer: ChatItem = { - type: ChatItemType.ANSWER, - messageId: messageId, - buttons: [], - } - // TODO: Add more cases for Accept/Reject/viewDiff. - switch (action.id) { - case 'Provide-Feedback': - answer.buttons = [ - { - keepCardAfterClick: true, - text: 'Thanks for providing feedback.', - id: 'utg_provided_feedback', - status: 'success', - position: 'outside', - disabled: true, - }, - ] - break - default: - break - } - this.onChatAnswerUpdated(tabId, answer) - } - - onFileDiff = (tabID: string, filePath: string, deleted: boolean, messageId?: string): void => { - if (this.onChatAnswerReceived === undefined) { - return - } - // Open diff view - this.sendMessageToExtension({ - command: 'open-diff', - tabID, - filePath, - deleted, - messageId, - tabType: 'testgen', - }) - this.onChatAnswerReceived( - tabID, - { - type: ChatItemType.ANSWER, - messageId: messageId, - followUp: { - text: ' ', - options: [ - { - type: FollowUpTypes.AcceptCode, - pillText: 'Accept', - status: 'success', - icon: MynahIcons.OK, - }, - { - type: FollowUpTypes.RejectCode, - pillText: 'Reject', - status: 'error', - icon: MynahIcons.REVERT, - }, - ], - }, - }, - {} - ) - } - - private processChatMessage = async (messageData: any): Promise => { - if (this.onChatAnswerReceived === undefined) { - return - } - if (messageData.command === 'test' && this.runTestMessageReceived) { - this.runTestMessageReceived(messageData.tabID, true) - return - } - if (messageData.message !== undefined) { - const answer: ChatItem = { - type: messageData.messageType, - messageId: messageData.messageId ?? messageData.triggerID, - body: messageData.message, - canBeVoted: false, - informationCard: messageData.informationCard, - buttons: messageData.buttons ?? [], - } - this.onChatAnswerReceived(messageData.tabID, answer, messageData) - } - } - // Displays the test generation summary message in the /test Tab before generating unit tests - private processChatSummaryMessage = async (messageData: any): Promise => { - if (this.onChatAnswerUpdated === undefined) { - return - } - if (messageData.message !== undefined) { - const answer: ChatItem = { - type: messageData.messageType, - messageId: messageData.messageId ?? messageData.triggerID, - body: messageData.message, - canBeVoted: true, - footer: messageData.filePath - ? { - fileList: { - rootFolderTitle: undefined, - fileTreeTitle: '', - filePaths: [messageData.filePath], - details: { - [messageData.filePath]: { - icon: MynahIcons.FILE, - description: `Generating tests in ${messageData.filePath}`, - }, - }, - }, - } - : {}, - } - this.onChatAnswerUpdated(messageData.tabID, answer) - } - } - - override processAuthNeededException = async (messageData: any): Promise => { - if (this.onChatAnswerReceived === undefined) { - return - } - - this.onChatAnswerReceived( - messageData.tabID, - { - type: ChatItemType.SYSTEM_PROMPT, - body: messageData.message, - }, - messageData - ) - } - - private processBuildProgressMessage = async ( - messageData: { type: TestMessageType } & Record - ): Promise => { - if (this.onChatAnswerReceived === undefined) { - return - } - const answer: ChatItem = { - type: messageData.messageType, - canBeVoted: messageData.canBeVoted, - messageId: messageData.messageId, - followUp: messageData.followUps, - fileList: messageData.fileList, - body: messageData.message, - codeReference: messageData.codeReference, - } - this.onChatAnswerReceived(messageData.tabID, answer, messageData) - } - - // This handles messages received from the extension, to be forwarded to the webview - handleMessageReceive = async (messageData: { type: TestMessageType } & Record) => { - switch (messageData.type) { - case 'authNeededException': - await this.processAuthNeededException(messageData) - break - case 'authenticationUpdateMessage': - this.onAuthenticationUpdate(messageData.testEnabled, messageData.authenticatingTabIDs) - break - case 'chatInputEnabledMessage': - this.chatInputEnabled(messageData.tabID, messageData.enabled) - break - case 'chatMessage': - await this.processChatMessage(messageData) - break - case 'chatSummaryMessage': - await this.processChatSummaryMessage(messageData) - break - case 'updatePlaceholderMessage': - this.updatePlaceholder(messageData.tabID, messageData.newPlaceholder) - break - case 'buildProgressMessage': - await this.processBuildProgressMessage(messageData) - break - case 'updatePromptProgress': - this.updatePromptProgress(messageData.tabID, messageData.progressField) - break - case 'errorMessage': - this.onError(messageData.tabID, messageData.message, messageData.title) - } - } -} diff --git a/packages/core/src/amazonq/webview/ui/connector.ts b/packages/core/src/amazonq/webview/ui/connector.ts index 1c31f6cc842..97821fc842f 100644 --- a/packages/core/src/amazonq/webview/ui/connector.ts +++ b/packages/core/src/amazonq/webview/ui/connector.ts @@ -19,12 +19,9 @@ import { DetailedList, } from '@aws/mynah-ui' import { Connector as CWChatConnector } from './apps/cwChatConnector' -import { Connector as FeatureDevChatConnector } from './apps/featureDevChatConnector' import { Connector as AmazonQCommonsConnector } from './apps/amazonqCommonsConnector' import { Connector as GumbyChatConnector } from './apps/gumbyChatConnector' import { Connector as ScanChatConnector } from './apps/scanChatConnector' -import { Connector as TestChatConnector } from './apps/testChatConnector' -import { Connector as docChatConnector } from './apps/docChatConnector' import { ExtensionMessage } from './commands' import { TabType, TabsStorage } from './storages/tabsStorage' import { WelcomeFollowupType } from './apps/amazonqCommonsConnector' @@ -123,11 +120,8 @@ export class Connector { private readonly sendMessageToExtension private readonly onMessageReceived private readonly cwChatConnector - private readonly featureDevChatConnector private readonly gumbyChatConnector private readonly scanChatConnector - private readonly testChatConnector - private readonly docChatConnector private readonly tabsStorage private readonly amazonqCommonsConnector: AmazonQCommonsConnector @@ -137,11 +131,8 @@ export class Connector { this.sendMessageToExtension = props.sendMessageToExtension this.onMessageReceived = props.onMessageReceived this.cwChatConnector = new CWChatConnector(props as ConnectorProps) - this.featureDevChatConnector = new FeatureDevChatConnector(props) - this.docChatConnector = new docChatConnector(props) this.gumbyChatConnector = new GumbyChatConnector(props) this.scanChatConnector = new ScanChatConnector(props) - this.testChatConnector = new TestChatConnector(props) this.amazonqCommonsConnector = new AmazonQCommonsConnector({ sendMessageToExtension: this.sendMessageToExtension, onWelcomeFollowUpClicked: props.onWelcomeFollowUpClicked, @@ -172,20 +163,12 @@ export class Connector { case 'cwc': this.cwChatConnector.onResponseBodyLinkClick(tabID, messageId, link) break - case 'featuredev': - this.featureDevChatConnector.onResponseBodyLinkClick(tabID, messageId, link) - break case 'gumby': this.gumbyChatConnector.onResponseBodyLinkClick(tabID, messageId, link) break case 'review': this.scanChatConnector.onResponseBodyLinkClick(tabID, messageId, link) break - case 'testgen': - this.testChatConnector.onResponseBodyLinkClick(tabID, messageId, link) - break - case 'doc': - this.docChatConnector.onResponseBodyLinkClick(tabID, messageId, link) } } @@ -201,8 +184,6 @@ export class Connector { switch (this.tabsStorage.getTab(tabID)?.type) { case 'gumby': return this.gumbyChatConnector.requestAnswer(tabID, payload) - case 'testgen': - return this.testChatConnector.requestAnswer(tabID, payload) } } @@ -210,10 +191,6 @@ export class Connector { new Promise((resolve, reject) => { if (this.isUIReady) { switch (this.tabsStorage.getTab(tabID)?.type) { - case 'featuredev': - return this.featureDevChatConnector.requestGenerativeAIAnswer(tabID, messageId, payload) - case 'doc': - return this.docChatConnector.requestGenerativeAIAnswer(tabID, messageId, payload) default: return this.cwChatConnector.requestGenerativeAIAnswer(tabID, messageId, payload) } @@ -247,10 +224,6 @@ export class Connector { } } - startTestGen = (tabID: string, prompt: string): void => { - this.testChatConnector.startTestGen(tabID, prompt) - } - transform = (tabID: string): void => { this.gumbyChatConnector.transform(tabID) } @@ -261,9 +234,6 @@ export class Connector { onStopChatResponse = (tabID: string): void => { switch (this.tabsStorage.getTab(tabID)?.type) { - case 'featuredev': - this.featureDevChatConnector.onStopChatResponse(tabID) - break case 'cwc': this.cwChatConnector.onStopChatResponse(tabID) break @@ -283,16 +253,10 @@ export class Connector { if (messageData.sender === 'CWChat') { await this.cwChatConnector.handleMessageReceive(messageData) - } else if (messageData.sender === 'featureDevChat') { - await this.featureDevChatConnector.handleMessageReceive(messageData) } else if (messageData.sender === 'gumbyChat') { await this.gumbyChatConnector.handleMessageReceive(messageData) } else if (messageData.sender === 'scanChat') { await this.scanChatConnector.handleMessageReceive(messageData) - } else if (messageData.sender === 'testChat') { - await this.testChatConnector.handleMessageReceive(messageData) - } else if (messageData.sender === 'docChat') { - await this.docChatConnector.handleMessageReceive(messageData) } else if (messageData.sender === 'amazonqCore') { await this.amazonqCommonsConnector.handleMessageReceive(messageData) } @@ -323,20 +287,6 @@ export class Connector { case 'review': this.scanChatConnector.onTabAdd(tabID) break - case 'testgen': - this.testChatConnector.onTabAdd(tabID) - break - } - } - - onKnownTabOpen = (tabID: string): void => { - switch (this.tabsStorage.getTab(tabID)?.type) { - case 'featuredev': - this.featureDevChatConnector.onTabOpen(tabID) - break - case 'doc': - this.docChatConnector.onTabOpen(tabID) - break } } @@ -372,23 +322,6 @@ export class Connector { codeBlockLanguage ) break - case 'featuredev': - this.featureDevChatConnector.onCodeInsertToCursorPosition( - tabID, - messageId, - code, - type, - codeReference, - eventId, - codeBlockIndex, - totalCodeBlocks, - userIntent, - codeBlockLanguage - ) - break - case 'testgen': - this.testChatConnector.onCodeInsertToCursorPosition(tabID, messageId, code, type, codeReference) - break } } @@ -477,20 +410,6 @@ export class Connector { codeBlockLanguage ) break - case 'featuredev': - this.featureDevChatConnector.onCopyCodeToClipboard( - tabID, - messageId, - code, - type, - codeReference, - eventId, - codeBlockIndex, - totalCodeBlocks, - userIntent, - codeBlockLanguage - ) - break } } @@ -501,21 +420,12 @@ export class Connector { case 'cwc': this.cwChatConnector.onTabRemove(tabID) break - case 'featuredev': - this.featureDevChatConnector.onTabRemove(tabID) - break - case 'doc': - this.docChatConnector.onTabRemove(tabID) - break case 'gumby': this.gumbyChatConnector.onTabRemove(tabID) break case 'review': this.scanChatConnector.onTabRemove(tabID) break - case 'testgen': - this.testChatConnector.onTabRemove(tabID) - break } } @@ -564,8 +474,6 @@ export class Connector { const tabType = this.tabsStorage.getTab(tabID)?.type switch (tabType) { case 'cwc': - case 'doc': - case 'featuredev': this.amazonqCommonsConnector.authFollowUpClicked(tabID, tabType, authType) } } @@ -578,49 +486,20 @@ export class Connector { case 'unknown': this.amazonqCommonsConnector.followUpClicked(tabID, followUp) break - case 'featuredev': - this.featureDevChatConnector.followUpClicked(tabID, messageId, followUp) - break - case 'testgen': - this.testChatConnector.followUpClicked(tabID, messageId, followUp) - break case 'review': this.scanChatConnector.followUpClicked(tabID, messageId, followUp) break - case 'doc': - this.docChatConnector.followUpClicked(tabID, messageId, followUp) - break default: this.cwChatConnector.followUpClicked(tabID, messageId, followUp) break } } - onFileActionClick = (tabID: string, messageId: string, filePath: string, actionName: string): void => { - switch (this.tabsStorage.getTab(tabID)?.type) { - case 'featuredev': - this.featureDevChatConnector.onFileActionClick(tabID, messageId, filePath, actionName) - break - case 'doc': - this.docChatConnector.onFileActionClick(tabID, messageId, filePath, actionName) - break - } - } - onFileClick = (tabID: string, filePath: string, deleted: boolean, messageId?: string): void => { switch (this.tabsStorage.getTab(tabID)?.type) { - case 'featuredev': - this.featureDevChatConnector.onOpenDiff(tabID, filePath, deleted, messageId) - break - case 'testgen': - this.testChatConnector.onFileDiff(tabID, filePath, deleted, messageId) - break case 'review': this.scanChatConnector.onFileClick(tabID, filePath, messageId) break - case 'doc': - this.docChatConnector.onOpenDiff(tabID, filePath, deleted) - break case 'cwc': this.cwChatConnector.onFileClick(tabID, filePath, messageId) break @@ -629,12 +508,6 @@ export class Connector { sendFeedback = (tabId: string, feedbackPayload: FeedbackPayload): void | undefined => { switch (this.tabsStorage.getTab(tabId)?.type) { - case 'featuredev': - this.featureDevChatConnector.sendFeedback(tabId, feedbackPayload) - break - case 'testgen': - this.testChatConnector.onSendFeedback(tabId, feedbackPayload) - break case 'cwc': this.cwChatConnector.onSendFeedback(tabId, feedbackPayload) break @@ -669,15 +542,9 @@ export class Connector { case 'cwc': this.cwChatConnector.onChatItemVoted(tabId, messageId, vote) break - case 'featuredev': - this.featureDevChatConnector.onChatItemVoted(tabId, messageId, vote) - break case 'review': this.scanChatConnector.onChatItemVoted(tabId, messageId, vote) break - case 'testgen': - this.testChatConnector.onChatItemVoted(tabId, messageId, vote) - break } } @@ -715,15 +582,9 @@ export class Connector { case 'gumby': this.gumbyChatConnector.onCustomFormAction(tabId, action) break - case 'testgen': - this.testChatConnector.onCustomFormAction(tabId, messageId ?? '', action) - break case 'review': this.scanChatConnector.onCustomFormAction(tabId, action) break - case 'doc': - this.docChatConnector.onCustomFormAction(tabId, action) - break case 'cwc': if (action.id === `open-settings`) { this.sendMessageToExtension({ @@ -735,10 +596,6 @@ export class Connector { this.cwChatConnector.onCustomFormAction(tabId, action) } break - case 'agentWalkthrough': { - this.amazonqCommonsConnector.onCustomFormAction(tabId, action) - break - } } } } diff --git a/packages/core/src/amazonq/webview/ui/connectorAdapter.ts b/packages/core/src/amazonq/webview/ui/connectorAdapter.ts index 1de1d8556c4..e645e74bd25 100644 --- a/packages/core/src/amazonq/webview/ui/connectorAdapter.ts +++ b/packages/core/src/amazonq/webview/ui/connectorAdapter.ts @@ -70,13 +70,7 @@ export class HybridChatAdapter implements ChatClientAdapter { } isSupportedQuickAction(command: string): boolean { - return ( - command === '/dev' || - command === '/test' || - command === '/review' || - command === '/doc' || - command === '/transform' - ) + return command === '/review' || command === '/transform' } handleQuickAction(prompt: ChatPrompt, tabId: string, eventId: string | undefined): void { @@ -85,11 +79,8 @@ export class HybridChatAdapter implements ChatClientAdapter { get initialQuickActions(): QuickActionCommandGroup[] { const tabDataGenerator = new TabDataGenerator({ - isDocEnabled: this.enableAgents, - isFeatureDevEnabled: this.enableAgents, isGumbyEnabled: this.enableAgents, isScanEnabled: this.enableAgents, - isTestEnabled: this.enableAgents, disabledCommands: this.disabledCommands, commandHighlight: this.featureConfigsSerialized.find(([name]) => name === 'highlightCommand')?.[1], }) diff --git a/packages/core/src/amazonq/webview/ui/followUps/generator.ts b/packages/core/src/amazonq/webview/ui/followUps/generator.ts index cce5726398f..a275cbaae6d 100644 --- a/packages/core/src/amazonq/webview/ui/followUps/generator.ts +++ b/packages/core/src/amazonq/webview/ui/followUps/generator.ts @@ -42,32 +42,6 @@ export class FollowUpGenerator { public generateWelcomeBlockForTab(tabType: TabType): FollowUpsBlock { switch (tabType) { - case 'featuredev': - return { - text: 'Ask a follow up question', - options: [ - { - pillText: 'What are some examples of tasks?', - type: 'DevExamples', - }, - ], - } - case 'doc': - return { - text: 'Select one of the following...', - options: [ - { - pillText: 'Create a README', - prompt: 'Create a README', - type: 'CreateDocumentation', - }, - { - pillText: 'Update an existing README', - prompt: 'Update an existing README', - type: 'UpdateDocumentation', - }, - ], - } default: return { text: 'Try Examples:', diff --git a/packages/core/src/amazonq/webview/ui/followUps/handler.ts b/packages/core/src/amazonq/webview/ui/followUps/handler.ts index 1fd38643827..6024a93ddee 100644 --- a/packages/core/src/amazonq/webview/ui/followUps/handler.ts +++ b/packages/core/src/amazonq/webview/ui/followUps/handler.ts @@ -3,12 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ChatItemAction, ChatItemType, MynahIcons, MynahUI } from '@aws/mynah-ui' +import { ChatItemAction, ChatItemType, MynahUI } from '@aws/mynah-ui' import { Connector } from '../connector' import { TabsStorage } from '../storages/tabsStorage' import { WelcomeFollowupType } from '../apps/amazonqCommonsConnector' import { AuthFollowUpType } from './generator' -import { FollowUpTypes, MynahUIRef } from '../../../commons/types' +import { MynahUIRef } from '../../../commons/types' export interface FollowUpInteractionHandlerProps { mynahUIRef: MynahUIRef @@ -74,82 +74,6 @@ export class FollowUpInteractionHandler { } } - const addChatItem = (tabID: string, messageId: string, options: any[]) => { - this.mynahUI?.addChatItem(tabID, { - type: ChatItemType.ANSWER_PART, - messageId, - followUp: { - text: '', - options, - }, - }) - } - - const ViewDiffOptions = [ - { - icon: MynahIcons.OK, - pillText: 'Accept', - status: 'success', - type: FollowUpTypes.AcceptCode, - }, - { - icon: MynahIcons.REVERT, - pillText: 'Reject', - status: 'error', - type: FollowUpTypes.RejectCode, - }, - ] - - const AcceptCodeOptions = [ - { - icon: MynahIcons.OK, - pillText: 'Accepted', - status: 'success', - disabled: true, - }, - ] - - const RejectCodeOptions = [ - { - icon: MynahIcons.REVERT, - pillText: 'Rejected', - status: 'error', - disabled: true, - }, - ] - - const ViewCodeDiffAfterIterationOptions = [ - { - icon: MynahIcons.OK, - pillText: 'Accept', - status: 'success', - type: FollowUpTypes.AcceptCode, - }, - { - icon: MynahIcons.REVERT, - pillText: 'Reject', - status: 'error', - type: FollowUpTypes.RejectCode, // TODO: Add new Followup Action for "Reject" - }, - ] - - if (this.tabsStorage.getTab(tabID)?.type === 'testgen') { - switch (followUp.type) { - case FollowUpTypes.ViewDiff: - addChatItem(tabID, messageId, ViewDiffOptions) - break - case FollowUpTypes.AcceptCode: - addChatItem(tabID, messageId, AcceptCodeOptions) - break - case FollowUpTypes.RejectCode: - addChatItem(tabID, messageId, RejectCodeOptions) - break - case FollowUpTypes.ViewCodeDiffAfterIteration: - addChatItem(tabID, messageId, ViewCodeDiffAfterIterationOptions) - break - } - } - this.connector.onFollowUpClicked(tabID, messageId, followUp) } diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts index 7d5bd48eaeb..c6df42f0566 100644 --- a/packages/core/src/amazonq/webview/ui/main.ts +++ b/packages/core/src/amazonq/webview/ui/main.ts @@ -32,7 +32,6 @@ import { DiffTreeFileInfo } from './diffTree/types' import { FeatureContext } from '../../../shared/featureConfig' import { tryNewMap } from '../../util/functionUtils' import { welcomeScreenTabData } from './walkthrough/welcome' -import { agentWalkthroughDataModel } from './walkthrough/agent' import { createClickTelemetry, createOpenAgentTelemetry } from './telemetry/actions' import { disclaimerAcknowledgeButtonId, disclaimerCard } from './texts/disclaimer' import { DetailedListSheetProps } from '@aws/mynah-ui/dist/components/detailed-list/detailed-list-sheet' @@ -91,11 +90,8 @@ export class WebviewUIHandler { tabDataGenerator?: TabDataGenerator // are agents enabled - isFeatureDevEnabled: boolean isGumbyEnabled: boolean isScanEnabled: boolean - isTestEnabled: boolean - isDocEnabled: boolean isSMUS: boolean isSM: boolean @@ -166,21 +162,15 @@ export class WebviewUIHandler { }, }) - this.isFeatureDevEnabled = enableAgents this.isGumbyEnabled = enableAgents this.isScanEnabled = enableAgents - this.isTestEnabled = enableAgents - this.isDocEnabled = enableAgents this.featureConfigs = tryNewMap(featureConfigsSerialized) const highlightCommand = this.featureConfigs.get('highlightCommand') this.tabDataGenerator = new TabDataGenerator({ - isFeatureDevEnabled: enableAgents, isGumbyEnabled: enableAgents, isScanEnabled: enableAgents, - isTestEnabled: enableAgents, - isDocEnabled: enableAgents, disabledCommands, commandHighlight: highlightCommand, regionProfile, // TODO @@ -195,31 +185,22 @@ export class WebviewUIHandler { this.quickActionHandler?.handle(chatPrompt, tabId) }, onUpdateAuthentication: (isAmazonQEnabled: boolean, authenticatingTabIDs: string[]): void => { - this.isFeatureDevEnabled = isAmazonQEnabled this.isGumbyEnabled = isAmazonQEnabled this.isScanEnabled = isAmazonQEnabled - this.isTestEnabled = isAmazonQEnabled - this.isDocEnabled = isAmazonQEnabled this.quickActionHandler = new QuickActionHandler({ mynahUIRef: this.mynahUIRef, connector: this.connector!, tabsStorage: this.tabsStorage, - isFeatureDevEnabled: this.isFeatureDevEnabled, isGumbyEnabled: this.isGumbyEnabled, isScanEnabled: this.isScanEnabled, - isTestEnabled: this.isTestEnabled, - isDocEnabled: this.isDocEnabled, hybridChat, disabledCommands, }) this.tabDataGenerator = new TabDataGenerator({ - isFeatureDevEnabled: this.isFeatureDevEnabled, isGumbyEnabled: this.isGumbyEnabled, isScanEnabled: this.isScanEnabled, - isTestEnabled: this.isTestEnabled, - isDocEnabled: this.isDocEnabled, disabledCommands, commandHighlight: highlightCommand, regionProfile, // TODO @@ -244,8 +225,7 @@ export class WebviewUIHandler { if ( this.tabsStorage.getTab(tabID)?.type === 'gumby' || - this.tabsStorage.getTab(tabID)?.type === 'review' || - this.tabsStorage.getTab(tabID)?.type === 'testgen' + this.tabsStorage.getTab(tabID)?.type === 'review' ) { this.mynahUI?.updateStore(tabID, { promptInputDisabledState: false, @@ -567,7 +547,6 @@ export class WebviewUIHandler { return } this.tabsStorage.updateTabTypeFromUnknown(newTabID, tabType) - this.connector?.onKnownTabOpen(newTabID) this.connector?.onUpdateTabType(newTabID) this.mynahUI?.updateStore(newTabID, { @@ -716,11 +695,7 @@ export class WebviewUIHandler { } const tabType = this.tabsStorage.getTab(tabID)?.type - if (tabType === 'featuredev') { - this.mynahUI?.addChatItem(tabID, { - type: ChatItemType.ANSWER_STREAM, - }) - } else if (tabType === 'gumby') { + if (tabType === 'gumby') { this.connector?.requestAnswer(tabID, { chatMessage: prompt.prompt ?? '', }) @@ -807,19 +782,6 @@ export class WebviewUIHandler { this.postMessage(createClickTelemetry('amazonq-welcome-quick-start-button')) return } - case 'explore': { - const newTabId = this.mynahUI?.updateStore('', agentWalkthroughDataModel) - if (newTabId === undefined) { - this.mynahUI?.notify({ - content: uiComponentsTexts.noMoreTabsTooltip, - type: NotificationType.WARNING, - }) - return - } - this.tabsStorage.updateTabTypeFromUnknown(newTabId, 'agentWalkthrough') - this.postMessage(createClickTelemetry('amazonq-welcome-explore-button')) - return - } default: { this.connector?.onCustomFormAction(tabId, messageId, action, eventId) return @@ -973,9 +935,6 @@ export class WebviewUIHandler { onFollowUpClicked: (tabID, messageId, followUp) => { this.followUpsInteractionHandler?.onFollowUpClicked(tabID, messageId, followUp) }, - onFileActionClick: async (tabID: string, messageId: string, filePath: string, actionName: string) => { - this.connector?.onFileActionClick(tabID, messageId, filePath, actionName) - }, onFileClick: this.connector.onFileClick, tabs: { 'tab-1': { @@ -1037,11 +996,8 @@ export class WebviewUIHandler { mynahUIRef: this.mynahUIRef, connector: this.connector, tabsStorage: this.tabsStorage, - isFeatureDevEnabled: this.isFeatureDevEnabled, isGumbyEnabled: this.isGumbyEnabled, isScanEnabled: this.isScanEnabled, - isTestEnabled: this.isTestEnabled, - isDocEnabled: this.isDocEnabled, hybridChat, }) this.textMessageHandler = new TextMessageHandler({ @@ -1053,11 +1009,8 @@ export class WebviewUIHandler { mynahUIRef: this.mynahUIRef, connector: this.connector, tabsStorage: this.tabsStorage, - isFeatureDevEnabled: this.isFeatureDevEnabled, isGumbyEnabled: this.isGumbyEnabled, isScanEnabled: this.isScanEnabled, - isTestEnabled: this.isTestEnabled, - isDocEnabled: this.isDocEnabled, }) } @@ -1102,12 +1055,6 @@ export class WebviewUIHandler { }, } } - // Show only "Copy" option for codeblocks in Q Test Tab - if (tab?.type === 'testgen') { - return { - 'insert-to-cursor': undefined, - } - } // Default will show "Copy" and "Insert at cursor" for codeblocks return {} } diff --git a/packages/core/src/amazonq/webview/ui/messages/controller.ts b/packages/core/src/amazonq/webview/ui/messages/controller.ts index a41d6a1f7f5..37a8077f8ae 100644 --- a/packages/core/src/amazonq/webview/ui/messages/controller.ts +++ b/packages/core/src/amazonq/webview/ui/messages/controller.ts @@ -14,11 +14,8 @@ export interface MessageControllerProps { mynahUIRef: MynahUIRef connector: Connector tabsStorage: TabsStorage - isFeatureDevEnabled: boolean isGumbyEnabled: boolean isScanEnabled: boolean - isTestEnabled: boolean - isDocEnabled: boolean disabledCommands?: string[] } @@ -33,11 +30,8 @@ export class MessageController { this.connector = props.connector this.tabsStorage = props.tabsStorage this.tabDataGenerator = new TabDataGenerator({ - isFeatureDevEnabled: props.isFeatureDevEnabled, isGumbyEnabled: props.isGumbyEnabled, isScanEnabled: props.isScanEnabled, - isTestEnabled: props.isTestEnabled, - isDocEnabled: props.isDocEnabled, disabledCommands: props.disabledCommands, }) } diff --git a/packages/core/src/amazonq/webview/ui/quickActions/generator.ts b/packages/core/src/amazonq/webview/ui/quickActions/generator.ts index f7bdc6a5089..4a0c5bad16c 100644 --- a/packages/core/src/amazonq/webview/ui/quickActions/generator.ts +++ b/packages/core/src/amazonq/webview/ui/quickActions/generator.ts @@ -8,80 +8,24 @@ import { TabType } from '../storages/tabsStorage' import { MynahIcons } from '@aws/mynah-ui' export interface QuickActionGeneratorProps { - isFeatureDevEnabled: boolean isGumbyEnabled: boolean isScanEnabled: boolean - isTestEnabled: boolean - isDocEnabled: boolean disableCommands?: string[] } export class QuickActionGenerator { - public isFeatureDevEnabled: boolean private isGumbyEnabled: boolean - private isScanEnabled: boolean - private isTestEnabled: boolean - private isDocEnabled: boolean private disabledCommands: string[] constructor(props: QuickActionGeneratorProps) { - this.isFeatureDevEnabled = props.isFeatureDevEnabled this.isGumbyEnabled = props.isGumbyEnabled - this.isScanEnabled = props.isScanEnabled - this.isTestEnabled = props.isTestEnabled - this.isDocEnabled = props.isDocEnabled this.disabledCommands = props.disableCommands ?? [] } public generateForTab(tabType: TabType): QuickActionCommandGroup[] { - // agentWalkthrough is static and doesn't have any quick actions - if (tabType === 'agentWalkthrough') { - return [] - } - - // TODO: Update acc to UX const quickActionCommands = [ { - groupName: `Q Developer agentic capabilities`, commands: [ - ...(this.isFeatureDevEnabled && !this.disabledCommands.includes('/dev') - ? [ - { - command: '/dev', - icon: MynahIcons.CODE_BLOCK, - placeholder: 'Describe your task or issue in as much detail as possible', - description: 'Generate code to make a change in your project', - }, - ] - : []), - ...(this.isTestEnabled && !this.disabledCommands.includes('/test') - ? [ - { - command: '/test', - icon: MynahIcons.CHECK_LIST, - placeholder: 'Specify a function(s) in the current file (optional)', - description: 'Generate unit tests for selected code', - }, - ] - : []), - ...(this.isScanEnabled && !this.disabledCommands.includes('/review') - ? [ - { - command: '/review', - icon: MynahIcons.BUG, - description: 'Identify and fix code issues before committing', - }, - ] - : []), - ...(this.isDocEnabled && !this.disabledCommands.includes('/doc') - ? [ - { - command: '/doc', - icon: MynahIcons.FILE, - description: 'Generate documentation', - }, - ] - : []), ...(this.isGumbyEnabled && !this.disabledCommands.includes('/transform') ? [ { @@ -111,7 +55,7 @@ export class QuickActionGenerator { ].filter((section) => section.commands.length > 0) const commandUnavailability: Record< - Exclude, + Exclude, { description: string unavailableItems: string[] @@ -121,10 +65,6 @@ export class QuickActionGenerator { description: '', unavailableItems: [], }, - featuredev: { - description: "This command isn't available in /dev", - unavailableItems: ['/help', '/clear'], - }, review: { description: "This command isn't available in /review", unavailableItems: ['/help', '/clear'], @@ -133,14 +73,6 @@ export class QuickActionGenerator { description: "This command isn't available in /transform", unavailableItems: ['/dev', '/test', '/doc', '/review', '/help', '/clear'], }, - testgen: { - description: "This command isn't available in /test", - unavailableItems: ['/help', '/clear'], - }, - doc: { - description: "This command isn't available in /doc", - unavailableItems: ['/help', '/clear'], - }, welcome: { description: '', unavailableItems: ['/clear'], diff --git a/packages/core/src/amazonq/webview/ui/quickActions/handler.ts b/packages/core/src/amazonq/webview/ui/quickActions/handler.ts index 2b8b9acabd3..ff23fb72635 100644 --- a/packages/core/src/amazonq/webview/ui/quickActions/handler.ts +++ b/packages/core/src/amazonq/webview/ui/quickActions/handler.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ChatItemType, ChatPrompt, MynahUI, NotificationType, MynahIcons } from '@aws/mynah-ui' +import { ChatItemType, ChatPrompt, MynahUI, NotificationType } from '@aws/mynah-ui' import { TabDataGenerator } from '../tabs/generator' import { Connector } from '../connector' import { TabsStorage, TabType } from '../storages/tabsStorage' @@ -14,11 +14,8 @@ export interface QuickActionsHandlerProps { mynahUIRef: { mynahUI: MynahUI | undefined } connector: Connector tabsStorage: TabsStorage - isFeatureDevEnabled: boolean isGumbyEnabled: boolean isScanEnabled: boolean - isTestEnabled: boolean - isDocEnabled: boolean hybridChat?: boolean disabledCommands?: string[] } @@ -36,30 +33,21 @@ export class QuickActionHandler { private connector: Connector private tabsStorage: TabsStorage private tabDataGenerator: TabDataGenerator - private isFeatureDevEnabled: boolean private isGumbyEnabled: boolean private isScanEnabled: boolean - private isTestEnabled: boolean - private isDocEnabled: boolean private isHybridChatEnabled: boolean constructor(props: QuickActionsHandlerProps) { this.mynahUIRef = props.mynahUIRef this.connector = props.connector this.tabsStorage = props.tabsStorage - this.isDocEnabled = props.isDocEnabled this.tabDataGenerator = new TabDataGenerator({ - isFeatureDevEnabled: props.isFeatureDevEnabled, isGumbyEnabled: props.isGumbyEnabled, isScanEnabled: props.isScanEnabled, - isTestEnabled: props.isTestEnabled, - isDocEnabled: props.isDocEnabled, disabledCommands: props.disabledCommands, }) - this.isFeatureDevEnabled = props.isFeatureDevEnabled this.isGumbyEnabled = props.isGumbyEnabled this.isScanEnabled = props.isScanEnabled - this.isTestEnabled = props.isTestEnabled this.isHybridChatEnabled = props.hybridChat ?? false } @@ -71,15 +59,6 @@ export class QuickActionHandler { public handle(chatPrompt: ChatPrompt, tabID: string, eventId?: string) { this.tabsStorage.resetTabTimer(tabID) switch (chatPrompt.command) { - case '/dev': - this.handleCommand({ - chatPrompt, - tabID, - taskName: 'Q - Dev', - tabType: 'featuredev', - isEnabled: this.isFeatureDevEnabled, - }) - break case '/help': this.handleHelpCommand(tabID) break @@ -89,18 +68,6 @@ export class QuickActionHandler { case '/review': this.handleScanCommand(tabID, eventId) break - case '/test': - this.handleTestCommand(chatPrompt, tabID, eventId) - break - case '/doc': - this.handleCommand({ - chatPrompt, - tabID, - taskName: 'Q - Doc', - tabType: 'doc', - isEnabled: this.isDocEnabled, - }) - break case '/clear': this.handleClearCommand(tabID) break @@ -145,7 +112,6 @@ export class QuickActionHandler { return } else { this.tabsStorage.updateTabTypeFromUnknown(affectedTabId, 'review') - this.connector.onKnownTabOpen(affectedTabId) this.connector.onUpdateTabType(affectedTabId) // reset chat history @@ -163,135 +129,6 @@ export class QuickActionHandler { } } - private handleTestCommand(chatPrompt: ChatPrompt, tabID: string | undefined, eventId: string | undefined) { - if (!this.isTestEnabled || !this.mynahUI) { - return - } - const testTabId = this.tabsStorage.getTabs().find((tab) => tab.type === 'testgen')?.id - const realPromptText = chatPrompt.escapedPrompt?.trim() ?? '' - - if (testTabId !== undefined) { - this.mynahUI.selectTab(testTabId, eventId || '') - this.connector.onTabChange(testTabId) - this.connector.startTestGen(testTabId, realPromptText) - return - } - - /** - * right click -> generate test has no tab id - * we have to manually create one if a testgen tab - * wasn't previously created - */ - if (!tabID) { - tabID = this.mynahUI.updateStore('', {}) - } - - // if there is no test tab, open a new one - const affectedTabId: string | undefined = this.addTab(tabID) - - if (affectedTabId === undefined) { - this.mynahUI.notify({ - content: uiComponentsTexts.noMoreTabsTooltip, - type: NotificationType.WARNING, - }) - return - } else { - this.tabsStorage.updateTabTypeFromUnknown(affectedTabId, 'testgen') - this.connector.onKnownTabOpen(affectedTabId) - this.connector.onUpdateTabType(affectedTabId) - - // reset chat history - this.mynahUI.updateStore(affectedTabId, { - chatItems: [], - }) - - // creating a new tab and printing some title - this.mynahUI.updateStore( - affectedTabId, - this.tabDataGenerator.getTabData('testgen', realPromptText === '', 'Q - Test') - ) - - this.connector.startTestGen(affectedTabId, realPromptText) - } - } - - private handleCommand(props: HandleCommandProps) { - if (!props.isEnabled || !this.mynahUI) { - return - } - - const realPromptText = props.chatPrompt?.escapedPrompt?.trim() ?? '' - - const affectedTabId = this.addTab(props.tabID) - - if (affectedTabId === undefined) { - this.mynahUI.notify({ - content: uiComponentsTexts.noMoreTabsTooltip, - type: NotificationType.WARNING, - }) - return - } else { - this.tabsStorage.updateTabTypeFromUnknown(affectedTabId, props.tabType) - this.connector.onKnownTabOpen(affectedTabId) - this.connector.onUpdateTabType(affectedTabId) - - this.mynahUI.updateStore(affectedTabId, { chatItems: [] }) - - if (props.tabType === 'featuredev') { - this.mynahUI.updateStore( - affectedTabId, - this.tabDataGenerator.getTabData(props.tabType, false, props.taskName) - ) - } else { - this.mynahUI.updateStore( - affectedTabId, - this.tabDataGenerator.getTabData(props.tabType, realPromptText === '', props.taskName) - ) - } - - const addInformationCard = (tabId: string) => { - if (props.tabType === 'featuredev') { - this.mynahUI?.addChatItem(tabId, { - type: ChatItemType.ANSWER, - informationCard: { - title: 'Feature development', - description: 'Amazon Q Developer Agent for Software Development', - icon: MynahIcons.BUG, - content: { - body: [ - 'After you provide a task, I will:', - '1. Generate code based on your description and the code in your workspace', - '2. Provide a list of suggestions for you to review and add to your workspace', - '3. If needed, iterate based on your feedback', - 'To learn more, visit the [user guide](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/software-dev.html)', - ].join('\n'), - }, - }, - }) - } - } - if (realPromptText !== '') { - this.mynahUI.addChatItem(affectedTabId, { - type: ChatItemType.PROMPT, - body: realPromptText, - }) - addInformationCard(affectedTabId) - - this.mynahUI.updateStore(affectedTabId, { - loadingChat: true, - cancelButtonWhenLoading: false, - promptInputDisabledState: true, - }) - - void this.connector.requestGenerativeAIAnswer(affectedTabId, '', { - chatMessage: realPromptText, - }) - } else { - addInformationCard(affectedTabId) - } - } - } - private handleGumbyCommand(tabID: string, eventId: string | undefined) { if (!this.isGumbyEnabled || !this.mynahUI) { return @@ -328,7 +165,6 @@ export class QuickActionHandler { return } else { this.tabsStorage.updateTabTypeFromUnknown(affectedTabId, 'gumby') - this.connector.onKnownTabOpen(affectedTabId) this.connector.onUpdateTabType(affectedTabId) // reset chat history diff --git a/packages/core/src/amazonq/webview/ui/storages/tabsStorage.ts b/packages/core/src/amazonq/webview/ui/storages/tabsStorage.ts index f9a419fed96..2a803759fd0 100644 --- a/packages/core/src/amazonq/webview/ui/storages/tabsStorage.ts +++ b/packages/core/src/amazonq/webview/ui/storages/tabsStorage.ts @@ -4,17 +4,7 @@ */ export type TabStatus = 'free' | 'busy' | 'dead' -const TabTypes = [ - 'cwc', - 'featuredev', - 'gumby', - 'review', - 'testgen', - 'doc', - 'agentWalkthrough', - 'welcome', - 'unknown', -] as const +const TabTypes = ['cwc', 'gumby', 'review', 'welcome', 'unknown'] as const export type TabType = (typeof TabTypes)[number] export function isTabType(value: string): value is TabType { return (TabTypes as readonly string[]).includes(value) @@ -22,16 +12,10 @@ export function isTabType(value: string): value is TabType { export function getTabCommandFromTabType(tabType: TabType): string { switch (tabType) { - case 'featuredev': - return '/dev' - case 'doc': - return '/doc' case 'gumby': return '/transform' case 'review': return '/review' - case 'testgen': - return '/test' default: return '' } diff --git a/packages/core/src/amazonq/webview/ui/tabs/constants.ts b/packages/core/src/amazonq/webview/ui/tabs/constants.ts index 8578c72377a..0872b829a6a 100644 --- a/packages/core/src/amazonq/webview/ui/tabs/constants.ts +++ b/packages/core/src/amazonq/webview/ui/tabs/constants.ts @@ -4,7 +4,6 @@ */ import { TabType } from '../storages/tabsStorage' import { QuickActionCommandGroup } from '@aws/mynah-ui' -import { userGuideURL } from '../texts/constants' const qChatIntroMessage = `Hi, I'm Amazon Q. I can answer your software development questions. Ask me to explain, debug, or optimize your code. @@ -45,42 +44,18 @@ export const commonTabData: TabTypeData = { contextCommands: [workspaceCommand], } -export const TabTypeDataMap: Record, TabTypeData> = { +export const TabTypeDataMap: Record, TabTypeData> = { unknown: commonTabData, cwc: commonTabData, - featuredev: { - title: 'Q - Dev', - placeholder: 'Describe your task or issue in as much detail as possible', - welcome: `I can generate code to accomplish a task or resolve an issue. - -After you provide a description, I will: -1. Generate code based on your description and the code in your workspace -2. Provide a list of suggestions for you to review and add to your workspace -3. If needed, iterate based on your feedback - -To learn more, visit the [User Guide](${userGuideURL}).`, - }, gumby: { title: 'Q - Code Transformation', placeholder: 'Open a new tab to chat with Q', welcome: - 'Welcome to Code Transformation! You can also run transformations from the command line. To install the tool, see the [documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/run-CLI-transformations.html).', + 'Welcome to Code Transformation! **You can also run transformations from the command line. To install the tool, see the [documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/run-CLI-transformations.html).**', }, review: { title: 'Q - Review', placeholder: `Ask a question or enter "/" for quick actions`, welcome: `Welcome to code reviews. I can help you identify code issues and provide suggested fixes for the active file or workspace you have opened in your IDE.`, }, - testgen: { - title: 'Q - Test', - placeholder: `Waiting on your inputs...`, - welcome: `Welcome to unit test generation. I can help you generate unit tests for your active file.`, - }, - doc: { - title: 'Q - Doc Generation', - placeholder: 'Ask Amazon Q to generate documentation for your project', - welcome: `Welcome to doc generation! - -I can help generate documentation for your code. To get started, choose what type of doc update you'd like to make.`, - }, } diff --git a/packages/core/src/amazonq/webview/ui/tabs/generator.ts b/packages/core/src/amazonq/webview/ui/tabs/generator.ts index 9698a9d8076..68b758d51cb 100644 --- a/packages/core/src/amazonq/webview/ui/tabs/generator.ts +++ b/packages/core/src/amazonq/webview/ui/tabs/generator.ts @@ -8,16 +8,12 @@ import { TabType } from '../storages/tabsStorage' import { FollowUpGenerator } from '../followUps/generator' import { QuickActionGenerator } from '../quickActions/generator' import { qChatIntroMessageForSMUS, TabTypeDataMap } from './constants' -import { agentWalkthroughDataModel } from '../walkthrough/agent' import { FeatureContext } from '../../../../shared/featureConfig' import { RegionProfile } from '../../../../codewhisperer/models/model' export interface TabDataGeneratorProps { - isFeatureDevEnabled: boolean isGumbyEnabled: boolean isScanEnabled: boolean - isTestEnabled: boolean - isDocEnabled: boolean disabledCommands?: string[] commandHighlight?: FeatureContext regionProfile?: RegionProfile @@ -32,11 +28,8 @@ export class TabDataGenerator { constructor(props: TabDataGeneratorProps) { this.followUpsGenerator = new FollowUpGenerator() this.quickActionsGenerator = new QuickActionGenerator({ - isFeatureDevEnabled: props.isFeatureDevEnabled, isGumbyEnabled: props.isGumbyEnabled, isScanEnabled: props.isScanEnabled, - isTestEnabled: props.isTestEnabled, - isDocEnabled: props.isDocEnabled, disableCommands: props.disabledCommands, }) this.highlightCommand = props.commandHighlight @@ -49,10 +42,6 @@ export class TabDataGenerator { taskName?: string, isSMUS?: boolean ): MynahUIDataModel { - if (tabType === 'agentWalkthrough') { - return agentWalkthroughDataModel - } - if (tabType === 'welcome') { return {} } @@ -92,7 +81,7 @@ export class TabDataGenerator { } private getContextCommands(tabType: TabType): QuickActionCommandGroup[] | undefined { - if (tabType === 'agentWalkthrough' || tabType === 'welcome') { + if (tabType === 'welcome') { return } diff --git a/packages/core/src/amazonq/webview/ui/walkthrough/agent.ts b/packages/core/src/amazonq/webview/ui/walkthrough/agent.ts deleted file mode 100644 index f4a5add7aa1..00000000000 --- a/packages/core/src/amazonq/webview/ui/walkthrough/agent.ts +++ /dev/null @@ -1,201 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ChatItemContent, ChatItemType, MynahIcons, MynahUIDataModel } from '@aws/mynah-ui' - -function createdTabbedData(examples: string[], agent: string): ChatItemContent['tabbedContent'] { - const exampleText = examples.map((example) => `- ${example}`).join('\n') - return [ - { - label: 'Examples', - value: 'examples', - content: { - body: `**Example use cases:**\n${exampleText}\n\nEnter ${agent} in Q Chat to get started`, - }, - }, - ] -} - -export const agentWalkthroughDataModel: MynahUIDataModel = { - tabBackground: false, - compactMode: false, - tabTitle: 'Explore', - promptInputVisible: false, - tabHeaderDetails: { - icon: MynahIcons.ASTERISK, - title: 'Amazon Q Developer agents capabilities', - description: '', - }, - chatItems: [ - { - type: ChatItemType.ANSWER, - snapToTop: true, - hoverEffect: true, - body: `### Feature development -Implement features or make changes across your workspace, all from a single prompt. -`, - icon: MynahIcons.CODE_BLOCK, - footer: { - tabbedContent: createdTabbedData( - [ - '/dev update app.py to add a new api', - '/dev fix the error', - '/dev add a new button to sort by ', - ], - '/dev' - ), - }, - buttons: [ - { - status: 'clear', - id: `user-guide-featuredev`, - disabled: false, - text: 'Read user guide', - }, - { - status: 'main', - disabled: false, - flash: 'once', - fillState: 'hover', - icon: MynahIcons.RIGHT_OPEN, - id: 'quick-start-featuredev', - text: `Quick start with **/dev**`, - }, - ], - }, - { - type: ChatItemType.ANSWER, - hoverEffect: true, - body: `### Unit test generation -Automatically generate unit tests for your active file. -`, - icon: MynahIcons.BUG, - footer: { - tabbedContent: createdTabbedData( - ['Generate tests for specific functions', 'Generate tests for null and empty inputs'], - '/test' - ), - }, - buttons: [ - { - status: 'clear', - id: 'user-guide-testgen', - disabled: false, - text: 'Read user guide', - }, - { - status: 'main', - disabled: false, - flash: 'once', - fillState: 'hover', - icon: MynahIcons.RIGHT_OPEN, - id: 'quick-start-testgen', - text: `Quick start with **/test**`, - }, - ], - }, - { - type: ChatItemType.ANSWER, - hoverEffect: true, - body: `### Documentation generation -Create and update READMEs for better documented code. -`, - icon: MynahIcons.CHECK_LIST, - footer: { - tabbedContent: createdTabbedData( - [ - 'Generate new READMEs for your project', - 'Update existing READMEs with recent code changes', - 'Request specific changes to a README', - ], - '/doc' - ), - }, - buttons: [ - { - status: 'clear', - id: 'user-guide-doc', - disabled: false, - text: 'Read user guide', - }, - { - status: 'main', - disabled: false, - flash: 'once', - fillState: 'hover', - icon: MynahIcons.RIGHT_OPEN, - id: 'quick-start-doc', - text: `Quick start with **/doc**`, - }, - ], - }, - { - type: ChatItemType.ANSWER, - hoverEffect: true, - body: `### Code reviews -Review code for issues, then get suggestions to fix your code instantaneously. -`, - icon: MynahIcons.TRANSFORM, - footer: { - tabbedContent: createdTabbedData( - [ - 'Review code for security vulnerabilities and code quality issues', - 'Get detailed explanations about code issues', - 'Apply automatic code fixes to your files', - ], - '/review' - ), - }, - buttons: [ - { - status: 'clear', - id: 'user-guide-review', - disabled: false, - text: 'Read user guide', - }, - { - status: 'main', - disabled: false, - flash: 'once', - fillState: 'hover', - icon: MynahIcons.RIGHT_OPEN, - id: 'quick-start-review', - text: `Quick start with **/review**`, - }, - ], - }, - { - type: ChatItemType.ANSWER, - hoverEffect: true, - body: `### Transformation -Upgrade library and language versions in your codebase. -`, - icon: MynahIcons.TRANSFORM, - footer: { - tabbedContent: createdTabbedData( - ['Upgrade Java language and dependency versions', 'Convert embedded SQL code in Java apps'], - '/transform' - ), - }, - buttons: [ - { - status: 'clear', - id: 'user-guide-gumby', - disabled: false, - text: 'Read user guide', - }, - { - status: 'main', - disabled: false, - flash: 'once', - fillState: 'hover', - icon: MynahIcons.RIGHT_OPEN, - id: 'quick-start-gumby', - text: `Quick start with **/transform**`, - }, - ], - }, - ], -} diff --git a/packages/core/src/amazonqDoc/app.ts b/packages/core/src/amazonqDoc/app.ts deleted file mode 100644 index 929cf1d45de..00000000000 --- a/packages/core/src/amazonqDoc/app.ts +++ /dev/null @@ -1,106 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import { ChatControllerEventEmitters, DocController } from './controllers/chat/controller' -import { AmazonQAppInitContext } from '../amazonq/apps/initContext' -import { MessagePublisher } from '../amazonq/messages/messagePublisher' -import { MessageListener } from '../amazonq/messages/messageListener' -import { fromQueryToParameters } from '../shared/utilities/uriUtils' -import { getLogger } from '../shared/logger/logger' -import { AuthUtil } from '../codewhisperer/util/authUtil' -import { debounce } from 'lodash' -import { DocChatSessionStorage } from './storages/chatSession' -import { UIMessageListener } from './views/actions/uiMessageListener' -import globals from '../shared/extensionGlobals' -import { AppToWebViewMessageDispatcher } from '../amazonq/commons/connector/connectorMessages' -import { docChat, docScheme } from './constants' -import { TabIdNotFoundError } from '../amazonqFeatureDev/errors' -import { DocMessenger } from './messenger' - -export function init(appContext: AmazonQAppInitContext) { - const docChatControllerEventEmitters: ChatControllerEventEmitters = { - processHumanChatMessage: new vscode.EventEmitter(), - followUpClicked: new vscode.EventEmitter(), - openDiff: new vscode.EventEmitter(), - processChatItemVotedMessage: new vscode.EventEmitter(), - stopResponse: new vscode.EventEmitter(), - tabOpened: new vscode.EventEmitter(), - processChatItemFeedbackMessage: new vscode.EventEmitter(), - tabClosed: new vscode.EventEmitter(), - authClicked: new vscode.EventEmitter(), - formActionClicked: new vscode.EventEmitter(), - processResponseBodyLinkClick: new vscode.EventEmitter(), - insertCodeAtPositionClicked: new vscode.EventEmitter(), - fileClicked: new vscode.EventEmitter(), - } - - const messenger = new DocMessenger( - new AppToWebViewMessageDispatcher(appContext.getAppsToWebViewMessagePublisher()), - docChat - ) - const sessionStorage = new DocChatSessionStorage(messenger) - - new DocController( - docChatControllerEventEmitters, - messenger, - sessionStorage, - appContext.onDidChangeAmazonQVisibility.event - ) - - const docProvider = new (class implements vscode.TextDocumentContentProvider { - async provideTextDocumentContent(uri: vscode.Uri): Promise { - const params = fromQueryToParameters(uri.query) - - const tabID = params.get('tabID') - if (!tabID) { - getLogger().error(`Unable to find tabID from ${uri.toString()}`) - throw new TabIdNotFoundError() - } - - const session = await sessionStorage.getSession(tabID) - const content = await session.config.fs.readFile(uri) - const decodedContent = new TextDecoder().decode(content) - return decodedContent - } - })() - - const textDocumentProvider = vscode.workspace.registerTextDocumentContentProvider(docScheme, docProvider) - - globals.context.subscriptions.push(textDocumentProvider) - - const docChatUIInputEventEmitter = new vscode.EventEmitter() - - new UIMessageListener({ - chatControllerEventEmitters: docChatControllerEventEmitters, - webViewMessageListener: new MessageListener(docChatUIInputEventEmitter), - }) - - appContext.registerWebViewToAppMessagePublisher(new MessagePublisher(docChatUIInputEventEmitter), 'doc') - - const debouncedEvent = debounce(async () => { - const authenticated = (await AuthUtil.instance.getChatAuthState()).amazonQ === 'connected' - let authenticatingSessionIDs: string[] = [] - if (authenticated) { - const authenticatingSessions = sessionStorage.getAuthenticatingSessions() - - authenticatingSessionIDs = authenticatingSessions.map((session: any) => session.tabID) - - // We've already authenticated these sessions - for (const session of authenticatingSessions) { - session.isAuthenticating = false - } - } - - messenger.sendAuthenticationUpdate(authenticated, authenticatingSessionIDs) - }, 500) - - AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(() => { - return debouncedEvent() - }) - AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { - return debouncedEvent() - }) -} diff --git a/packages/core/src/amazonqDoc/constants.ts b/packages/core/src/amazonqDoc/constants.ts deleted file mode 100644 index 7b57e7c2ce9..00000000000 --- a/packages/core/src/amazonqDoc/constants.ts +++ /dev/null @@ -1,163 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { MynahIcons, Status } from '@aws/mynah-ui' -import { FollowUpTypes } from '../amazonq/commons/types' -import { NewFileInfo } from './types' -import { i18n } from '../shared/i18n-helper' - -// For uniquely identifiying which chat messages should be routed to Doc -export const docChat = 'docChat' - -export const docScheme = 'aws-doc' - -export const featureName = 'Amazon Q Doc Generation' - -export function getFileSummaryPercentage(input: string): number { - // Split the input string by newline characters - const lines = input.split('\n') - - // Find the line containing "summarized:" - const summaryLine = lines.find((line) => line.includes('summarized:')) - - // If the line is not found, return null - if (!summaryLine) { - return -1 - } - - // Extract the numbers from the summary line - const [summarized, total] = summaryLine.split(':')[1].trim().split(' of ').map(Number) - - // Calculate the percentage - const percentage = (summarized / total) * 100 - - return percentage -} - -const checkIcons = { - wait: '☐', - current: '☐', - done: '☑', -} - -const getIconForStep = (targetStep: number, currentStep: number) => { - return currentStep === targetStep - ? checkIcons.current - : currentStep > targetStep - ? checkIcons.done - : checkIcons.wait -} - -export enum DocGenerationStep { - UPLOAD_TO_S3, - SUMMARIZING_FILES, - GENERATING_ARTIFACTS, -} - -export const docGenerationProgressMessage = (currentStep: DocGenerationStep, mode: Mode) => ` -${mode === Mode.CREATE ? i18n('AWS.amazonq.doc.answer.creating') : i18n('AWS.amazonq.doc.answer.updating')} - -${getIconForStep(DocGenerationStep.UPLOAD_TO_S3, currentStep)} ${i18n('AWS.amazonq.doc.answer.scanning')} - -${getIconForStep(DocGenerationStep.SUMMARIZING_FILES, currentStep)} ${i18n('AWS.amazonq.doc.answer.summarizing')} - -${getIconForStep(DocGenerationStep.GENERATING_ARTIFACTS, currentStep)} ${i18n('AWS.amazonq.doc.answer.generating')} - - -` - -export const docGenerationSuccessMessage = (mode: Mode) => - mode === Mode.CREATE ? i18n('AWS.amazonq.doc.answer.readmeCreated') : i18n('AWS.amazonq.doc.answer.readmeUpdated') - -export const docRejectConfirmation = 'Your changes have been discarded.' - -export const FolderSelectorFollowUps = [ - { - icon: 'ok' as MynahIcons, - pillText: 'Yes', - prompt: 'Yes', - status: 'success' as Status, - type: FollowUpTypes.ProceedFolderSelection, - }, - { - icon: 'refresh' as MynahIcons, - pillText: 'Change folder', - prompt: 'Change folder', - status: 'info' as Status, - type: FollowUpTypes.ChooseFolder, - }, - { - icon: 'cancel' as MynahIcons, - pillText: 'Cancel', - prompt: 'Cancel', - status: 'error' as Status, - type: FollowUpTypes.CancelFolderSelection, - }, -] - -export const CodeChangeFollowUps = [ - { - pillText: i18n('AWS.amazonq.doc.pillText.accept'), - prompt: i18n('AWS.amazonq.doc.pillText.accept'), - type: FollowUpTypes.AcceptChanges, - icon: 'ok' as MynahIcons, - status: 'success' as Status, - }, - { - pillText: i18n('AWS.amazonq.doc.pillText.makeChanges'), - prompt: i18n('AWS.amazonq.doc.pillText.makeChanges'), - type: FollowUpTypes.MakeChanges, - icon: 'refresh' as MynahIcons, - status: 'info' as Status, - }, - { - pillText: i18n('AWS.amazonq.doc.pillText.reject'), - prompt: i18n('AWS.amazonq.doc.pillText.reject'), - type: FollowUpTypes.RejectChanges, - icon: 'cancel' as MynahIcons, - status: 'error' as Status, - }, -] - -export const NewSessionFollowUps = [ - { - pillText: i18n('AWS.amazonq.doc.pillText.newTask'), - type: FollowUpTypes.NewTask, - status: 'info' as Status, - }, - { - pillText: i18n('AWS.amazonq.doc.pillText.closeSession'), - type: FollowUpTypes.CloseSession, - status: 'info' as Status, - }, -] - -export const SynchronizeDocumentation = { - pillText: i18n('AWS.amazonq.doc.pillText.update'), - prompt: i18n('AWS.amazonq.doc.pillText.update'), - type: FollowUpTypes.SynchronizeDocumentation, -} - -export const EditDocumentation = { - pillText: i18n('AWS.amazonq.doc.pillText.makeChange'), - prompt: i18n('AWS.amazonq.doc.pillText.makeChange'), - type: FollowUpTypes.EditDocumentation, -} - -export enum Mode { - NONE = 'None', - CREATE = 'Create', - SYNC = 'Sync', - EDIT = 'Edit', -} - -/** - * - * @param paths file paths - * @returns the path to a README.md, or undefined if none exist - */ -export const findReadmePath = (paths?: NewFileInfo[]) => { - return paths?.find((path) => /readme\.md$/i.test(path.relativePath)) -} diff --git a/packages/core/src/amazonqDoc/controllers/chat/controller.ts b/packages/core/src/amazonqDoc/controllers/chat/controller.ts deleted file mode 100644 index ab6045e75ce..00000000000 --- a/packages/core/src/amazonqDoc/controllers/chat/controller.ts +++ /dev/null @@ -1,715 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import * as vscode from 'vscode' -import { EventEmitter } from 'vscode' - -import { - DocGenerationStep, - EditDocumentation, - FolderSelectorFollowUps, - Mode, - NewSessionFollowUps, - SynchronizeDocumentation, - CodeChangeFollowUps, - docScheme, - featureName, - findReadmePath, -} from '../../constants' -import { AuthUtil } from '../../../codewhisperer/util/authUtil' -import { getLogger } from '../../../shared/logger/logger' - -import { Session } from '../../session/session' -import { i18n } from '../../../shared/i18n-helper' -import path from 'path' -import { createSingleFileDialog } from '../../../shared/ui/common/openDialog' - -import { - MonthlyConversationLimitError, - SelectedFolderNotInWorkspaceFolderError, - WorkspaceFolderNotFoundError, - createUserFacingErrorMessage, - getMetricResult, -} from '../../../amazonqFeatureDev/errors' -import { BaseChatSessionStorage } from '../../../amazonq/commons/baseChatStorage' -import { DocMessenger } from '../../messenger' -import { AuthController } from '../../../amazonq/auth/controller' -import { openUrl } from '../../../shared/utilities/vsCodeUtils' -import { createAmazonQUri, openDeletedDiff, openDiff } from '../../../amazonq/commons/diff' -import { - getWorkspaceFoldersByPrefixes, - getWorkspaceRelativePath, - isMultiRootWorkspace, -} from '../../../shared/utilities/workspaceUtils' -import { getPathsFromZipFilePath, SvgFileExtension } from '../../../amazonq/util/files' -import { FollowUpTypes } from '../../../amazonq/commons/types' -import { DocGenerationTask, DocGenerationTasks } from '../docGenerationTask' -import { normalize } from '../../../shared/utilities/pathUtils' -import { DevPhase, MetricDataOperationName, MetricDataResult } from '../../types' - -export interface ChatControllerEventEmitters { - readonly processHumanChatMessage: EventEmitter - readonly followUpClicked: EventEmitter - readonly openDiff: EventEmitter - readonly stopResponse: EventEmitter - readonly tabOpened: EventEmitter - readonly tabClosed: EventEmitter - readonly processChatItemVotedMessage: EventEmitter - readonly processChatItemFeedbackMessage: EventEmitter - readonly authClicked: EventEmitter - readonly processResponseBodyLinkClick: EventEmitter - readonly insertCodeAtPositionClicked: EventEmitter - readonly fileClicked: EventEmitter - readonly formActionClicked: EventEmitter -} - -export class DocController { - private readonly scheme = docScheme - private readonly messenger: DocMessenger - private readonly sessionStorage: BaseChatSessionStorage - private authController: AuthController - private docGenerationTasks: DocGenerationTasks - - public constructor( - private readonly chatControllerMessageListeners: ChatControllerEventEmitters, - messenger: DocMessenger, - sessionStorage: BaseChatSessionStorage, - _onDidChangeAmazonQVisibility: vscode.Event - ) { - this.messenger = messenger - this.sessionStorage = sessionStorage - this.authController = new AuthController() - this.docGenerationTasks = new DocGenerationTasks() - - this.chatControllerMessageListeners.processHumanChatMessage.event((data) => { - this.processUserChatMessage(data).catch((e) => { - getLogger().error('processUserChatMessage failed: %s', (e as Error).message) - }) - }) - this.chatControllerMessageListeners.formActionClicked.event((data) => { - return this.formActionClicked(data) - }) - - this.initializeFollowUps() - - this.chatControllerMessageListeners.stopResponse.event((data) => { - return this.stopResponse(data) - }) - this.chatControllerMessageListeners.tabOpened.event((data) => { - return this.tabOpened(data) - }) - this.chatControllerMessageListeners.tabClosed.event((data) => { - this.tabClosed(data) - }) - this.chatControllerMessageListeners.authClicked.event((data) => { - this.authClicked(data) - }) - this.chatControllerMessageListeners.processResponseBodyLinkClick.event((data) => { - this.processLink(data) - }) - this.chatControllerMessageListeners.fileClicked.event(async (data) => { - return await this.fileClicked(data) - }) - this.chatControllerMessageListeners.openDiff.event(async (data) => { - return await this.openDiff(data) - }) - AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { - this.sessionStorage.deleteAllSessions() - }) - } - - /** Prompts user to choose a folder in current workspace for README creation/update. - * After user chooses a folder, displays confirmation message to user with selected path. - * - */ - private async folderSelector(data: any) { - this.messenger.sendAnswer({ - type: 'answer', - tabID: data.tabID, - message: i18n('AWS.amazonq.doc.answer.chooseFolder'), - disableChatInput: true, - }) - - const uri = await createSingleFileDialog({ - canSelectFolders: true, - canSelectFiles: false, - }).prompt() - - const retryFollowUps = FolderSelectorFollowUps.filter( - (followUp) => followUp.type !== FollowUpTypes.ProceedFolderSelection - ) - - if (!(uri instanceof vscode.Uri)) { - this.messenger.sendAnswer({ - type: 'answer', - tabID: data.tabID, - message: i18n('AWS.amazonq.doc.error.noFolderSelected'), - followUps: retryFollowUps, - disableChatInput: true, - }) - // Check that selected folder is a subfolder of the current workspace - } else if (!vscode.workspace.getWorkspaceFolder(uri)) { - this.messenger.sendAnswer({ - type: 'answer', - tabID: data.tabID, - message: new SelectedFolderNotInWorkspaceFolderError().message, - followUps: retryFollowUps, - disableChatInput: true, - }) - } else { - let displayPath = '' - const relativePath = getWorkspaceRelativePath(uri.fsPath) - const docGenerationTask = this.docGenerationTasks.getTask(data.tabID) - if (relativePath) { - // Display path should always include workspace folder name - displayPath = path.join(relativePath.workspaceFolder.name, relativePath.relativePath) - // Only include workspace folder name in API call if multi-root workspace - docGenerationTask.folderPath = normalize( - isMultiRootWorkspace() ? displayPath : relativePath.relativePath - ) - - if (!relativePath.relativePath) { - docGenerationTask.folderLevel = 'ENTIRE_WORKSPACE' - } else { - docGenerationTask.folderLevel = 'SUB_FOLDER' - } - } - - this.messenger.sendFolderConfirmationMessage( - data.tabID, - docGenerationTask.mode === Mode.CREATE - ? i18n('AWS.amazonq.doc.answer.createReadme') - : i18n('AWS.amazonq.doc.answer.updateReadme'), - displayPath, - FolderSelectorFollowUps - ) - this.messenger.sendChatInputEnabled(data.tabID, false) - } - } - - private async openDiff(message: any) { - const tabId: string = message.tabID - const codeGenerationId: string = message.messageId - const zipFilePath: string = message.filePath - const session = await this.sessionStorage.getSession(tabId) - - const workspacePrefixMapping = getWorkspaceFoldersByPrefixes(session.config.workspaceFolders) - const pathInfos = getPathsFromZipFilePath(zipFilePath, workspacePrefixMapping, session.config.workspaceFolders) - - const extension = path.parse(message.filePath).ext - // Only open diffs on files, not directories - if (extension) { - if (message.deleted) { - const name = path.basename(pathInfos.relativePath) - await openDeletedDiff(pathInfos.absolutePath, name, tabId, this.scheme) - } else { - let uploadId = session.uploadId - if (session?.state?.uploadHistory && session.state.uploadHistory[codeGenerationId]) { - uploadId = session?.state?.uploadHistory[codeGenerationId].uploadId - } - const rightPath = path.join(uploadId, zipFilePath) - if (rightPath.toLowerCase().endsWith(SvgFileExtension)) { - const rightPathUri = createAmazonQUri(rightPath, tabId, this.scheme) - const infraDiagramContent = await vscode.workspace.openTextDocument(rightPathUri) - await vscode.window.showTextDocument(infraDiagramContent) - } else { - await openDiff(pathInfos.absolutePath, rightPath, tabId, this.scheme) - } - } - } - } - - private initializeFollowUps(): void { - this.chatControllerMessageListeners.followUpClicked.event(async (data) => { - const session: Session = await this.sessionStorage.getSession(data.tabID) - const docGenerationTask = this.docGenerationTasks.getTask(data.tabID) - - const workspaceFolders = vscode.workspace.workspaceFolders - if (workspaceFolders === undefined || workspaceFolders.length === 0) { - return - } - - const workspaceFolderName = vscode.workspace.workspaceFolders?.[0].name || '' - - const authState = await AuthUtil.instance.getChatAuthState() - - if (authState.amazonQ !== 'connected') { - await this.messenger.sendAuthNeededExceptionMessage(authState, data.tabID) - session.isAuthenticating = true - return - } - - const sendFolderConfirmationMessage = (message: string) => { - this.messenger.sendFolderConfirmationMessage( - data.tabID, - message, - workspaceFolderName, - FolderSelectorFollowUps - ) - } - - switch (data.followUp.type) { - case FollowUpTypes.Retry: - if (docGenerationTask.mode === Mode.EDIT) { - this.enableUserInput(data?.tabID) - } else { - await this.tabOpened(data) - } - break - case FollowUpTypes.NewTask: - this.messenger.sendAnswer({ - type: 'answer', - tabID: data?.tabID, - message: i18n('AWS.amazonq.featureDev.answer.newTaskChanges'), - disableChatInput: true, - }) - return this.newTask(data) - case FollowUpTypes.CloseSession: - return this.closeSession(data) - case FollowUpTypes.CreateDocumentation: - docGenerationTask.interactionType = 'GENERATE_README' - docGenerationTask.mode = Mode.CREATE - sendFolderConfirmationMessage(i18n('AWS.amazonq.doc.answer.createReadme')) - break - case FollowUpTypes.ChooseFolder: - await this.folderSelector(data) - break - case FollowUpTypes.SynchronizeDocumentation: - docGenerationTask.mode = Mode.SYNC - sendFolderConfirmationMessage(i18n('AWS.amazonq.doc.answer.updateReadme')) - break - case FollowUpTypes.UpdateDocumentation: - docGenerationTask.interactionType = 'UPDATE_README' - this.messenger.sendAnswer({ - type: 'answer', - tabID: data?.tabID, - followUps: [SynchronizeDocumentation, EditDocumentation], - disableChatInput: true, - }) - break - case FollowUpTypes.EditDocumentation: - docGenerationTask.interactionType = 'EDIT_README' - docGenerationTask.mode = Mode.EDIT - sendFolderConfirmationMessage(i18n('AWS.amazonq.doc.answer.updateReadme')) - break - case FollowUpTypes.MakeChanges: - docGenerationTask.mode = Mode.EDIT - this.enableUserInput(data.tabID) - break - case FollowUpTypes.AcceptChanges: - docGenerationTask.userDecision = 'ACCEPT' - await this.sendDocAcceptanceEvent(data) - await this.insertCode(data) - return - case FollowUpTypes.RejectChanges: - docGenerationTask.userDecision = 'REJECT' - await this.sendDocAcceptanceEvent(data) - this.messenger.sendAnswer({ - type: 'answer', - tabID: data?.tabID, - disableChatInput: true, - message: 'Your changes have been discarded.', - followUps: NewSessionFollowUps, - }) - break - case FollowUpTypes.ProceedFolderSelection: - // If a user did not change the folder in a multi-root workspace, default to the first workspace folder - if (docGenerationTask.folderPath === '' && isMultiRootWorkspace()) { - docGenerationTask.folderPath = workspaceFolderName - } - if (docGenerationTask.mode === Mode.EDIT) { - this.enableUserInput(data.tabID) - } else { - await this.generateDocumentation( - { - ...data, - message: - docGenerationTask.mode === Mode.CREATE - ? 'Create documentation for a specific folder' - : 'Sync documentation', - }, - session, - docGenerationTask - ) - } - break - case FollowUpTypes.CancelFolderSelection: - docGenerationTask.reset() - return this.tabOpened(data) - } - }) - } - - private enableUserInput(tabID: string) { - this.messenger.sendAnswer({ - type: 'answer', - tabID: tabID, - message: i18n('AWS.amazonq.doc.answer.editReadme'), - }) - this.messenger.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.doc.placeholder.editReadme')) - this.messenger.sendChatInputEnabled(tabID, true) - } - - private async fileClicked(message: any) { - const tabId: string = message.tabID - const messageId = message.messageId - const filePathToUpdate: string = message.filePath - - const session = await this.sessionStorage.getSession(tabId) - const filePathIndex = (session.state.filePaths ?? []).findIndex((obj) => obj.relativePath === filePathToUpdate) - if (filePathIndex !== -1 && session.state.filePaths) { - session.state.filePaths[filePathIndex].rejected = !session.state.filePaths[filePathIndex].rejected - } - const deletedFilePathIndex = (session.state.deletedFiles ?? []).findIndex( - (obj) => obj.relativePath === filePathToUpdate - ) - if (deletedFilePathIndex !== -1 && session.state.deletedFiles) { - session.state.deletedFiles[deletedFilePathIndex].rejected = - !session.state.deletedFiles[deletedFilePathIndex].rejected - } - - await session.updateFilesPaths( - tabId, - session.state.filePaths ?? [], - session.state.deletedFiles ?? [], - messageId, - true - ) - } - - private async formActionClicked(message: any) { - switch (message.action) { - case 'cancel-doc-generation': - // eslint-disable-next-line unicorn/no-null - await this.stopResponse(message) - - break - } - } - - private async newTask(message: any) { - // Old session for the tab is ending, delete it so we can create a new one for the message id - - this.docGenerationTasks.deleteTask(message.tabID) - this.sessionStorage.deleteSession(message.tabID) - - // Re-run the opening flow, where we check auth + create a session - await this.tabOpened(message) - } - - private async closeSession(message: any) { - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: i18n('AWS.amazonq.featureDev.answer.sessionClosed'), - disableChatInput: true, - }) - this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.featureDev.placeholder.sessionClosed')) - this.messenger.sendChatInputEnabled(message.tabID, false) - this.docGenerationTasks.deleteTask(message.tabID) - } - - private processErrorChatMessage = ( - err: any, - message: any, - session: Session | undefined, - docGenerationTask: DocGenerationTask - ) => { - const errorMessage = createUserFacingErrorMessage(`${err.cause?.message ?? err.message}`) - // eslint-disable-next-line unicorn/no-null - this.messenger.sendUpdatePromptProgress(message.tabID, null) - if (err.constructor.name === MonthlyConversationLimitError.name) { - this.messenger.sendMonthlyLimitError(message.tabID) - } else { - const enableUserInput = docGenerationTask.mode === Mode.EDIT && err.remainingIterations > 0 - - this.messenger.sendErrorMessage( - errorMessage, - message.tabID, - 0, - session?.conversationIdUnsafe, - false, - enableUserInput - ) - } - } - - private async generateDocumentation(message: any, session: any, docGenerationTask: DocGenerationTask) { - try { - await this.onDocsGeneration(session, message.message, message.tabID, docGenerationTask) - } catch (err: any) { - this.processErrorChatMessage(err, message, session, docGenerationTask) - } - } - - private async processUserChatMessage(message: any) { - if (message.message === undefined) { - this.messenger.sendErrorMessage('chatMessage should be set', message.tabID, 0, undefined) - return - } - - /** - * Don't attempt to process any chat messages when a workspace folder is not set. - * When the tab is first opened we will throw an error and lock the chat if the workspace - * folder is not found - */ - const workspaceFolders = vscode.workspace.workspaceFolders - if (workspaceFolders === undefined || workspaceFolders.length === 0) { - return - } - - const session: Session = await this.sessionStorage.getSession(message.tabID) - const docGenerationTask = this.docGenerationTasks.getTask(message.tabID) - - try { - getLogger().debug(`${featureName}: Processing message: ${message.message}`) - - const authState = await AuthUtil.instance.getChatAuthState() - if (authState.amazonQ !== 'connected') { - await this.messenger.sendAuthNeededExceptionMessage(authState, message.tabID) - session.isAuthenticating = true - return - } - - await this.generateDocumentation(message, session, docGenerationTask) - } catch (err: any) { - this.processErrorChatMessage(err, message, session, docGenerationTask) - } - } - - private async stopResponse(message: any) { - this.messenger.sendAnswer({ - message: i18n('AWS.amazonq.featureDev.pillText.stoppingCodeGeneration'), - type: 'answer-part', - tabID: message.tabID, - }) - // eslint-disable-next-line unicorn/no-null - this.messenger.sendUpdatePromptProgress(message.tabID, null) - this.messenger.sendChatInputEnabled(message.tabID, false) - - const session = await this.sessionStorage.getSession(message.tabID) - session.state.tokenSource?.cancel() - } - - private async tabOpened(message: any) { - let session: Session | undefined - try { - session = await this.sessionStorage.getSession(message.tabID) - const docGenerationTask = this.docGenerationTasks.getTask(message.tabID) - getLogger().debug(`${featureName}: Session created with id: ${session.tabID}`) - docGenerationTask.folderPath = '' - docGenerationTask.mode = Mode.NONE - - const authState = await AuthUtil.instance.getChatAuthState() - if (authState.amazonQ !== 'connected') { - void this.messenger.sendAuthNeededExceptionMessage(authState, message.tabID) - session.isAuthenticating = true - return - } - docGenerationTask.numberOfNavigations += 1 - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - followUps: [ - { - pillText: 'Create a README', - prompt: 'Create a README', - type: 'CreateDocumentation', - }, - { - pillText: 'Update an existing README', - prompt: 'Update an existing README', - type: 'UpdateDocumentation', - }, - ], - disableChatInput: true, - }) - this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.doc.pillText.selectOption')) - } catch (err: any) { - if (err instanceof WorkspaceFolderNotFoundError) { - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: err.message, - disableChatInput: true, - }) - } else { - this.messenger.sendErrorMessage( - createUserFacingErrorMessage(err.message), - message.tabID, - 0, - session?.conversationIdUnsafe - ) - } - } - } - - private async openMarkdownPreview(readmePath: vscode.Uri) { - await vscode.commands.executeCommand('vscode.open', readmePath) - await vscode.commands.executeCommand('markdown.showPreview') - } - - private async onDocsGeneration( - session: Session, - message: string, - tabID: string, - docGenerationTask: DocGenerationTask - ) { - this.messenger.sendDocProgress(tabID, DocGenerationStep.UPLOAD_TO_S3, 0, docGenerationTask.mode) - - await session.preloader(message) - - try { - await session.sendDocMetricData(MetricDataOperationName.StartDocGeneration, MetricDataResult.Success) - await session.send(message, docGenerationTask.mode, docGenerationTask.folderPath) - const filePaths = session.state.filePaths ?? [] - const deletedFiles = session.state.deletedFiles ?? [] - - // Only add the follow up accept/deny buttons when the tab hasn't been closed/request hasn't been cancelled - if (session?.state.tokenSource?.token.isCancellationRequested) { - return - } - - if (filePaths.length === 0 && deletedFiles.length === 0) { - this.messenger.sendAnswer({ - message: i18n('AWS.amazonq.featureDev.pillText.unableGenerateChanges'), - type: 'answer', - tabID: tabID, - canBeVoted: true, - disableChatInput: true, - }) - - return - } - - this.messenger.sendCodeResult( - filePaths, - deletedFiles, - session.state.references ?? [], - tabID, - session.uploadId, - session.state.codeGenerationId ?? '' - ) - - // Automatically open the README diff - const readmePath = findReadmePath(session.state.filePaths) - if (readmePath) { - await this.openDiff({ tabID, filePath: readmePath.zipFilePath }) - } - - const remainingIterations = session.state.codeGenerationRemainingIterationCount - const totalIterations = session.state.codeGenerationTotalIterationCount - - if (remainingIterations !== undefined && totalIterations !== undefined) { - this.messenger.sendAnswer({ - type: 'answer', - tabID: tabID, - message: `${docGenerationTask.mode === Mode.CREATE ? i18n('AWS.amazonq.doc.answer.readmeCreated') : i18n('AWS.amazonq.doc.answer.readmeUpdated')} ${remainingIterations > 0 ? i18n('AWS.amazonq.doc.answer.codeResult') : i18n('AWS.amazonq.doc.answer.acceptOrReject')}`, - disableChatInput: true, - }) - - this.messenger.sendAnswer({ - message: undefined, - type: 'system-prompt', - disableChatInput: true, - followUps: - remainingIterations > 0 - ? CodeChangeFollowUps - : CodeChangeFollowUps.filter((followUp) => followUp.type !== FollowUpTypes.MakeChanges), - tabID: tabID, - }) - } - if (session?.state.phase === DevPhase.CODEGEN) { - const docGenerationTask = this.docGenerationTasks.getTask(tabID) - const { totalGeneratedChars, totalGeneratedLines, totalGeneratedFiles } = - await session.countGeneratedContent(docGenerationTask.interactionType) - docGenerationTask.conversationId = session.conversationId - docGenerationTask.numberOfGeneratedChars = totalGeneratedChars - docGenerationTask.numberOfGeneratedLines = totalGeneratedLines - docGenerationTask.numberOfGeneratedFiles = totalGeneratedFiles - const docGenerationEvent = docGenerationTask.docGenerationEventBase() - - await session.sendDocTelemetryEvent(docGenerationEvent, 'generation') - } - } catch (err: any) { - getLogger().error(`${featureName}: Error during doc generation: ${err}`) - await session.sendDocMetricData(MetricDataOperationName.EndDocGeneration, getMetricResult(err)) - throw err - } finally { - if (session?.state?.tokenSource?.token.isCancellationRequested) { - await this.newTask({ tabID }) - } else { - this.messenger.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.doc.pillText.selectOption')) - - this.messenger.sendChatInputEnabled(tabID, false) - } - } - await session.sendDocMetricData(MetricDataOperationName.EndDocGeneration, MetricDataResult.Success) - } - - private authClicked(message: any) { - this.authController.handleAuth(message.authType) - - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: 'Follow instructions to re-authenticate ...', - disableChatInput: true, - }) - } - - private tabClosed(message: any) { - this.sessionStorage.deleteSession(message.tabID) - this.docGenerationTasks.deleteTask(message.tabID) - } - - private async insertCode(message: any) { - let session - try { - session = await this.sessionStorage.getSession(message.tabID) - - await session.insertChanges() - - const readmePath = findReadmePath(session.state.filePaths) - if (readmePath) { - await this.openMarkdownPreview( - vscode.Uri.file(path.join(readmePath.workspaceFolder.uri.fsPath, readmePath.relativePath)) - ) - } - - this.messenger.sendAnswer({ - type: 'answer', - disableChatInput: true, - tabID: message.tabID, - followUps: NewSessionFollowUps, - }) - - this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.doc.pillText.selectOption')) - } catch (err: any) { - this.messenger.sendErrorMessage( - createUserFacingErrorMessage(`Failed to insert code changes: ${err.message}`), - message.tabID, - 0, - session?.conversationIdUnsafe - ) - } - } - private async sendDocAcceptanceEvent(message: any) { - const session = await this.sessionStorage.getSession(message.tabID) - const docGenerationTask = this.docGenerationTasks.getTask(message.tabID) - docGenerationTask.conversationId = session.conversationId - const { totalAddedChars, totalAddedLines, totalAddedFiles } = await session.countAddedContent( - docGenerationTask.interactionType - ) - docGenerationTask.numberOfAddedChars = totalAddedChars - docGenerationTask.numberOfAddedLines = totalAddedLines - docGenerationTask.numberOfAddedFiles = totalAddedFiles - const docAcceptanceEvent = docGenerationTask.docAcceptanceEventBase() - - await session.sendDocTelemetryEvent(docAcceptanceEvent, 'acceptance') - } - private processLink(message: any) { - void openUrl(vscode.Uri.parse(message.link)) - } -} diff --git a/packages/core/src/amazonqDoc/controllers/docGenerationTask.ts b/packages/core/src/amazonqDoc/controllers/docGenerationTask.ts deleted file mode 100644 index fe5dc25981c..00000000000 --- a/packages/core/src/amazonqDoc/controllers/docGenerationTask.ts +++ /dev/null @@ -1,100 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import { - DocFolderLevel, - DocInteractionType, - DocUserDecision, - DocV2AcceptanceEvent, - DocV2GenerationEvent, -} from '../../codewhisperer/client/codewhispereruserclient' -import { getLogger } from '../../shared/logger/logger' -import { Mode } from '../constants' - -export class DocGenerationTasks { - private tasks: Map = new Map() - - public getTask(tabId: string): DocGenerationTask { - if (!this.tasks.has(tabId)) { - this.tasks.set(tabId, new DocGenerationTask()) - } - return this.tasks.get(tabId)! - } - - public deleteTask(tabId: string): void { - this.tasks.delete(tabId) - } -} - -export class DocGenerationTask { - public mode: Mode = Mode.NONE - public folderPath = '' - // Telemetry fields - public conversationId?: string - public numberOfAddedChars?: number - public numberOfAddedLines?: number - public numberOfAddedFiles?: number - public numberOfGeneratedChars?: number - public numberOfGeneratedLines?: number - public numberOfGeneratedFiles?: number - public userDecision?: DocUserDecision - public interactionType?: DocInteractionType - public numberOfNavigations = 0 - public folderLevel: DocFolderLevel = 'ENTIRE_WORKSPACE' - - public docGenerationEventBase() { - const undefinedProps = Object.entries(this) - .filter(([key, value]) => value === undefined) - .map(([key]) => key) - - if (undefinedProps.length > 0) { - getLogger().debug(`DocV2GenerationEvent has undefined properties: ${undefinedProps.join(', ')}`) - } - const event: DocV2GenerationEvent = { - conversationId: this.conversationId ?? '', - numberOfGeneratedChars: this.numberOfGeneratedChars ?? 0, - numberOfGeneratedLines: this.numberOfGeneratedLines ?? 0, - numberOfGeneratedFiles: this.numberOfGeneratedFiles ?? 0, - interactionType: this.interactionType, - numberOfNavigations: this.numberOfNavigations, - folderLevel: this.folderLevel, - } - return event - } - - public docAcceptanceEventBase() { - const undefinedProps = Object.entries(this) - .filter(([key, value]) => value === undefined) - .map(([key]) => key) - - if (undefinedProps.length > 0) { - getLogger().debug(`DocV2AcceptanceEvent has undefined properties: ${undefinedProps.join(', ')}`) - } - const event: DocV2AcceptanceEvent = { - conversationId: this.conversationId ?? '', - numberOfAddedChars: this.numberOfAddedChars ?? 0, - numberOfAddedLines: this.numberOfAddedLines ?? 0, - numberOfAddedFiles: this.numberOfAddedFiles ?? 0, - userDecision: this.userDecision ?? 'ACCEPTED', - interactionType: this.interactionType ?? 'GENERATE_README', - numberOfNavigations: this.numberOfNavigations ?? 0, - folderLevel: this.folderLevel, - } - return event - } - - public reset() { - this.conversationId = undefined - this.numberOfAddedChars = undefined - this.numberOfAddedLines = undefined - this.numberOfAddedFiles = undefined - this.numberOfGeneratedChars = undefined - this.numberOfGeneratedLines = undefined - this.numberOfGeneratedFiles = undefined - this.userDecision = undefined - this.interactionType = undefined - this.numberOfNavigations = 0 - this.folderLevel = 'ENTIRE_WORKSPACE' - } -} diff --git a/packages/core/src/amazonqDoc/errors.ts b/packages/core/src/amazonqDoc/errors.ts deleted file mode 100644 index fb1ffa033c2..00000000000 --- a/packages/core/src/amazonqDoc/errors.ts +++ /dev/null @@ -1,63 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ClientError, ContentLengthError as CommonContentLengthError } from '../shared/errors' -import { i18n } from '../shared/i18n-helper' - -export class DocClientError extends ClientError { - remainingIterations?: number - constructor(message: string, code: string, remainingIterations?: number) { - super(message, { code }) - this.remainingIterations = remainingIterations - } -} - -export class ReadmeTooLargeError extends DocClientError { - constructor() { - super(i18n('AWS.amazonq.doc.error.readmeTooLarge'), ReadmeTooLargeError.name) - } -} - -export class ReadmeUpdateTooLargeError extends DocClientError { - constructor(remainingIterations: number) { - super(i18n('AWS.amazonq.doc.error.readmeUpdateTooLarge'), ReadmeUpdateTooLargeError.name, remainingIterations) - } -} - -export class WorkspaceEmptyError extends DocClientError { - constructor() { - super(i18n('AWS.amazonq.doc.error.workspaceEmpty'), WorkspaceEmptyError.name) - } -} - -export class NoChangeRequiredException extends DocClientError { - constructor() { - super(i18n('AWS.amazonq.doc.error.noChangeRequiredException'), NoChangeRequiredException.name) - } -} - -export class PromptRefusalException extends DocClientError { - constructor(remainingIterations: number) { - super(i18n('AWS.amazonq.doc.error.promptRefusal'), PromptRefusalException.name, remainingIterations) - } -} - -export class ContentLengthError extends CommonContentLengthError { - constructor() { - super(i18n('AWS.amazonq.doc.error.contentLengthError'), { code: ContentLengthError.name }) - } -} - -export class PromptTooVagueError extends DocClientError { - constructor(remainingIterations: number) { - super(i18n('AWS.amazonq.doc.error.promptTooVague'), PromptTooVagueError.name, remainingIterations) - } -} - -export class PromptUnrelatedError extends DocClientError { - constructor(remainingIterations: number) { - super(i18n('AWS.amazonq.doc.error.promptUnrelated'), PromptUnrelatedError.name, remainingIterations) - } -} diff --git a/packages/core/src/amazonqDoc/index.ts b/packages/core/src/amazonqDoc/index.ts deleted file mode 100644 index 7ba22e4b351..00000000000 --- a/packages/core/src/amazonqDoc/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -export * from './types' -export * from './session/sessionState' -export * from './constants' -export { Session } from './session/session' -export { ChatControllerEventEmitters, DocController } from './controllers/chat/controller' diff --git a/packages/core/src/amazonqDoc/messenger.ts b/packages/core/src/amazonqDoc/messenger.ts deleted file mode 100644 index 3c6abfdf15f..00000000000 --- a/packages/core/src/amazonqDoc/messenger.ts +++ /dev/null @@ -1,65 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import { Messenger } from '../amazonq/commons/connector/baseMessenger' -import { AppToWebViewMessageDispatcher } from '../amazonq/commons/connector/connectorMessages' -import { messageWithConversationId } from '../amazonqFeatureDev/userFacingText' -import { i18n } from '../shared/i18n-helper' -import { docGenerationProgressMessage, DocGenerationStep, Mode, NewSessionFollowUps } from './constants' -import { inProgress } from './types' - -export class DocMessenger extends Messenger { - public constructor(dispatcher: AppToWebViewMessageDispatcher, sender: string) { - super(dispatcher, sender) - } - - /** Sends a message in the chat and displays a prompt input progress bar to communicate the doc generation progress. - * The text in the progress bar matches the current step shown in the message. - * - */ - public sendDocProgress(tabID: string, step: DocGenerationStep, progress: number, mode: Mode) { - // Hide prompt input progress bar once all steps are completed - if (step > DocGenerationStep.GENERATING_ARTIFACTS) { - // eslint-disable-next-line unicorn/no-null - this.sendUpdatePromptProgress(tabID, null) - } else { - const progressText = - step === DocGenerationStep.UPLOAD_TO_S3 - ? `${i18n('AWS.amazonq.doc.answer.scanning')}...` - : step === DocGenerationStep.SUMMARIZING_FILES - ? `${i18n('AWS.amazonq.doc.answer.summarizing')}...` - : `${i18n('AWS.amazonq.doc.answer.generating')}...` - this.sendUpdatePromptProgress(tabID, inProgress(progress, progressText)) - } - - // The first step is answer-stream type, subequent updates are answer-part - this.sendAnswer({ - type: step === DocGenerationStep.UPLOAD_TO_S3 ? 'answer-stream' : 'answer-part', - tabID: tabID, - disableChatInput: true, - message: docGenerationProgressMessage(step, mode), - }) - } - - public override sendErrorMessage( - errorMessage: string, - tabID: string, - _retries: number, - conversationId?: string, - _showDefaultMessage?: boolean, - enableUserInput?: boolean - ) { - if (enableUserInput) { - this.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.doc.placeholder.editReadme')) - this.sendChatInputEnabled(tabID, true) - } - this.sendAnswer({ - type: 'answer', - tabID: tabID, - message: errorMessage + messageWithConversationId(conversationId), - followUps: enableUserInput ? [] : NewSessionFollowUps, - disableChatInput: !enableUserInput, - }) - } -} diff --git a/packages/core/src/amazonqDoc/session/session.ts b/packages/core/src/amazonqDoc/session/session.ts deleted file mode 100644 index e3eb29d6d32..00000000000 --- a/packages/core/src/amazonqDoc/session/session.ts +++ /dev/null @@ -1,371 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { docScheme, featureName, Mode } from '../constants' -import { DeletedFileInfo, Interaction, NewFileInfo, SessionState, SessionStateConfig } from '../types' -import { DocPrepareCodeGenState } from './sessionState' -import { telemetry } from '../../shared/telemetry/telemetry' -import { AuthUtil } from '../../codewhisperer/util/authUtil' -import { SessionConfig } from '../../amazonq/commons/session/sessionConfigFactory' -import path from 'path' -import { FeatureDevClient } from '../../amazonqFeatureDev/client/featureDev' -import { TelemetryHelper } from '../../amazonq/util/telemetryHelper' -import { ConversationNotStartedState } from '../../amazonqFeatureDev/session/sessionState' -import { logWithConversationId } from '../../amazonqFeatureDev/userFacingText' -import { ConversationIdNotFoundError, IllegalStateError } from '../../amazonqFeatureDev/errors' -import { referenceLogText } from '../../amazonq/commons/model' -import { - DocInteractionType, - DocV2AcceptanceEvent, - DocV2GenerationEvent, - SendTelemetryEventRequest, - MetricData, -} from '../../codewhisperer/client/codewhispereruserclient' -import { getClientId, getOperatingSystem, getOptOutPreference } from '../../shared/telemetry/util' -import { DocMessenger } from '../messenger' -import { computeDiff } from '../../amazonq/commons/diff' -import { ReferenceLogViewProvider } from '../../codewhisperer/service/referenceLogViewProvider' -import fs from '../../shared/fs/fs' -import globals from '../../shared/extensionGlobals' -import { extensionVersion } from '../../shared/vscode/env' -import { getLogger } from '../../shared/logger/logger' -import { ContentLengthError as CommonContentLengthError } from '../../shared/errors' -import { ContentLengthError } from '../errors' - -export class Session { - private _state?: SessionState | Omit - private task: string = '' - private proxyClient: FeatureDevClient - private _conversationId?: string - private preloaderFinished = false - private _latestMessage: string = '' - private _telemetry: TelemetryHelper - - // Used to keep track of whether or not the current session is currently authenticating/needs authenticating - public isAuthenticating: boolean - private _reportedDocChanges: { [key: string]: string } = {} - - constructor( - public readonly config: SessionConfig, - private messenger: DocMessenger, - public readonly tabID: string, - initialState: Omit = new ConversationNotStartedState(tabID), - proxyClient: FeatureDevClient = new FeatureDevClient() - ) { - this._state = initialState - this.proxyClient = proxyClient - - this._telemetry = new TelemetryHelper() - this.isAuthenticating = false - } - - /** - * Preload any events that have to run before a chat message can be sent - */ - async preloader(msg: string) { - if (!this.preloaderFinished) { - await this.setupConversation(msg) - this.preloaderFinished = true - } - } - - get state() { - if (!this._state) { - throw new IllegalStateError("State should be initialized before it's read") - } - return this._state - } - - /** - * setupConversation - * - * Starts a conversation with the backend and uploads the repo for the LLMs to be able to use it. - */ - private async setupConversation(msg: string) { - // Store the initial message when setting up the conversation so that if it fails we can retry with this message - this._latestMessage = msg - - await telemetry.amazonq_startConversationInvoke.run(async (span) => { - this._conversationId = await this.proxyClient.createConversation() - getLogger().info(logWithConversationId(this.conversationId)) - - span.record({ amazonqConversationId: this._conversationId, credentialStartUrl: AuthUtil.instance.startUrl }) - }) - - this._state = new DocPrepareCodeGenState( - { - ...this.getSessionStateConfig(), - conversationId: this.conversationId, - uploadId: '', - currentCodeGenerationId: undefined, - }, - [], - [], - [], - this.tabID, - 0 - ) - } - - private getSessionStateConfig(): Omit { - return { - workspaceRoots: this.config.workspaceRoots, - workspaceFolders: this.config.workspaceFolders, - proxyClient: this.proxyClient, - conversationId: this.conversationId, - } - } - - async send(msg: string, mode: Mode, folderPath?: string): Promise { - // When the task/"thing to do" hasn't been set yet, we want it to be the incoming message - if (this.task === '' && msg) { - this.task = msg - } - - this._latestMessage = msg - - return this.nextInteraction(msg, mode, folderPath) - } - private async nextInteraction(msg: string, mode: Mode, folderPath?: string) { - try { - const resp = await this.state.interact({ - task: this.task, - msg, - fs: this.config.fs, - mode: mode, - folderPath: folderPath, - messenger: this.messenger, - telemetry: this.telemetry, - tokenSource: this.state.tokenSource, - uploadHistory: this.state.uploadHistory, - }) - - if (resp.nextState) { - if (!this.state?.tokenSource?.token.isCancellationRequested) { - this.state?.tokenSource?.cancel() - } - - // Move to the next state - this._state = resp.nextState - } - - return resp.interaction - } catch (e) { - if (e instanceof CommonContentLengthError) { - getLogger().debug(`Content length validation failed: ${e.message}`) - throw new ContentLengthError() - } - throw e - } - } - - public async updateFilesPaths( - tabID: string, - filePaths: NewFileInfo[], - deletedFiles: DeletedFileInfo[], - messageId: string, - disableFileActions: boolean - ) { - this.messenger.updateFileComponent(tabID, filePaths, deletedFiles, messageId, disableFileActions) - } - - public async insertChanges() { - for (const filePath of this.state.filePaths?.filter((i) => !i.rejected) ?? []) { - const absolutePath = path.join(filePath.workspaceFolder.uri.fsPath, filePath.relativePath) - - const uri = filePath.virtualMemoryUri - const content = await this.config.fs.readFile(uri) - const decodedContent = new TextDecoder().decode(content) - - await fs.mkdir(path.dirname(absolutePath)) - await fs.writeFile(absolutePath, decodedContent) - } - - for (const filePath of this.state.deletedFiles?.filter((i) => !i.rejected) ?? []) { - const absolutePath = path.join(filePath.workspaceFolder.uri.fsPath, filePath.relativePath) - await fs.delete(absolutePath) - } - - for (const ref of this.state.references ?? []) { - ReferenceLogViewProvider.instance.addReferenceLog(referenceLogText(ref)) - } - } - - private getFromReportedChanges(filepath: NewFileInfo) { - const key = `${filepath.workspaceFolder.uri.fsPath}/${filepath.relativePath}` - return this._reportedDocChanges[key] - } - - private addToReportedChanges(filepath: NewFileInfo) { - const key = `${filepath.workspaceFolder.uri.fsPath}/${filepath.relativePath}` - this._reportedDocChanges[key] = filepath.fileContent - } - - public async countGeneratedContent(interactionType?: DocInteractionType) { - let totalGeneratedChars = 0 - let totalGeneratedLines = 0 - let totalGeneratedFiles = 0 - const filePaths = this.state.filePaths ?? [] - - for (const filePath of filePaths) { - const reportedDocChange = this.getFromReportedChanges(filePath) - if (interactionType === 'GENERATE_README') { - if (reportedDocChange) { - const { charsAdded, linesAdded } = await this.computeFilePathDiff(filePath, reportedDocChange) - totalGeneratedChars += charsAdded - totalGeneratedLines += linesAdded - } else { - // If no changes are reported, this is the initial README generation and no comparison with existing files is needed - const fileContent = filePath.fileContent - totalGeneratedChars += fileContent.length - totalGeneratedLines += fileContent.split('\n').length - } - } else { - const { charsAdded, linesAdded } = await this.computeFilePathDiff(filePath, reportedDocChange) - totalGeneratedChars += charsAdded - totalGeneratedLines += linesAdded - } - this.addToReportedChanges(filePath) - totalGeneratedFiles += 1 - } - return { - totalGeneratedChars, - totalGeneratedLines, - totalGeneratedFiles, - } - } - - public async countAddedContent(interactionType?: DocInteractionType) { - let totalAddedChars = 0 - let totalAddedLines = 0 - let totalAddedFiles = 0 - const newFilePaths = - this.state.filePaths?.filter((filePath) => !filePath.rejected && !filePath.changeApplied) ?? [] - - for (const filePath of newFilePaths) { - if (interactionType === 'GENERATE_README') { - const fileContent = filePath.fileContent - totalAddedChars += fileContent.length - totalAddedLines += fileContent.split('\n').length - } else { - const { charsAdded, linesAdded } = await this.computeFilePathDiff(filePath) - totalAddedChars += charsAdded - totalAddedLines += linesAdded - } - totalAddedFiles += 1 - } - return { - totalAddedChars, - totalAddedLines, - totalAddedFiles, - } - } - - public async computeFilePathDiff(filePath: NewFileInfo, reportedChanges?: string) { - const leftPath = `${filePath.workspaceFolder.uri.fsPath}/${filePath.relativePath}` - const rightPath = filePath.virtualMemoryUri.path - const diff = await computeDiff(leftPath, rightPath, this.tabID, docScheme, reportedChanges) - return { leftPath, rightPath, ...diff } - } - - public async sendDocMetricData(operationName: string, result: string) { - const metricData = { - metricName: 'Operation', - metricValue: 1, - timestamp: new Date(), - product: 'DocGeneration', - dimensions: [ - { - name: 'operationName', - value: operationName, - }, - { - name: 'result', - value: result, - }, - ], - } - await this.sendDocTelemetryEvent(metricData, 'metric') - } - - public async sendDocTelemetryEvent( - telemetryEvent: DocV2GenerationEvent | DocV2AcceptanceEvent | MetricData, - eventType: 'generation' | 'acceptance' | 'metric' - ) { - const client = await this.proxyClient.getClient() - const telemetryEventKey = { - generation: 'docV2GenerationEvent', - acceptance: 'docV2AcceptanceEvent', - metric: 'metricData', - }[eventType] - try { - const params: SendTelemetryEventRequest = { - telemetryEvent: { - [telemetryEventKey]: telemetryEvent, - }, - optOutPreference: getOptOutPreference(), - userContext: { - ideCategory: 'VSCODE', - operatingSystem: getOperatingSystem(), - product: 'DocGeneration', // Should be the same as in JetBrains - clientId: getClientId(globals.globalState), - ideVersion: extensionVersion, - }, - profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, - } - - const response = await client.sendTelemetryEvent(params).promise() - if (eventType === 'metric') { - getLogger().debug( - `${featureName}: successfully sent metricData: RequestId: ${response.$response.requestId}` - ) - } else { - getLogger().debug( - `${featureName}: successfully sent docV2${eventType === 'generation' ? 'GenerationEvent' : 'AcceptanceEvent'}: ` + - `ConversationId: ${(telemetryEvent as DocV2GenerationEvent | DocV2AcceptanceEvent).conversationId} ` + - `RequestId: ${response.$response.requestId}` - ) - } - } catch (e) { - const error = e as Error - const eventTypeString = eventType === 'metric' ? 'metricData' : `doc ${eventType}` - getLogger().error( - `${featureName}: failed to send ${eventTypeString} telemetry: ${error.name}: ${error.message} ` + - `RequestId: ${(e as any).$response?.requestId}` - ) - } - } - - get currentCodeGenerationId() { - return this.state.currentCodeGenerationId - } - - get uploadId() { - if (!('uploadId' in this.state)) { - throw new IllegalStateError("UploadId has to be initialized before it's read") - } - return this.state.uploadId - } - - get conversationId() { - if (!this._conversationId) { - throw new ConversationIdNotFoundError() - } - return this._conversationId - } - - // Used for cases where it is not needed to have conversationId - get conversationIdUnsafe() { - return this._conversationId - } - - get latestMessage() { - return this._latestMessage - } - - get telemetry() { - return this._telemetry - } -} diff --git a/packages/core/src/amazonqDoc/session/sessionState.ts b/packages/core/src/amazonqDoc/session/sessionState.ts deleted file mode 100644 index e95bc48f772..00000000000 --- a/packages/core/src/amazonqDoc/session/sessionState.ts +++ /dev/null @@ -1,165 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { DocGenerationStep, docScheme, getFileSummaryPercentage, Mode } from '../constants' - -import { i18n } from '../../shared/i18n-helper' - -import { CurrentWsFolders, NewFileInfo, SessionState, SessionStateAction, SessionStateConfig } from '../types' -import { - ContentLengthError, - NoChangeRequiredException, - PromptRefusalException, - PromptTooVagueError, - PromptUnrelatedError, - ReadmeTooLargeError, - ReadmeUpdateTooLargeError, - WorkspaceEmptyError, -} from '../errors' -import { ApiClientError, ApiServiceError } from '../../amazonqFeatureDev/errors' -import { DocMessenger } from '../messenger' -import { BaseCodeGenState, BasePrepareCodeGenState, CreateNextStateParams } from '../../amazonq/session/sessionState' -import { Intent } from '../../amazonq/commons/types' -import { AmazonqCreateUpload, Span } from '../../shared/telemetry/telemetry' -import { prepareRepoData, PrepareRepoDataOptions } from '../../amazonq/util/files' -import { LlmError } from '../../amazonq/errors' - -export class DocCodeGenState extends BaseCodeGenState { - protected handleProgress(messenger: DocMessenger, action: SessionStateAction, detail?: string): void { - if (detail) { - const progress = getFileSummaryPercentage(detail) - messenger.sendDocProgress( - this.tabID, - progress === 100 ? DocGenerationStep.GENERATING_ARTIFACTS : DocGenerationStep.SUMMARIZING_FILES, - progress, - action.mode - ) - } - } - - protected getScheme(): string { - return docScheme - } - - protected getTimeoutErrorCode(): string { - return 'DocGenerationTimeout' - } - - protected handleGenerationComplete( - messenger: DocMessenger, - newFileInfo: NewFileInfo[], - action: SessionStateAction - ): void { - messenger.sendDocProgress(this.tabID, DocGenerationStep.GENERATING_ARTIFACTS + 1, 100, action.mode) - } - - protected handleError(messenger: DocMessenger, codegenResult: any): Error { - // eslint-disable-next-line unicorn/no-null - messenger.sendUpdatePromptProgress(this.tabID, null) - - switch (true) { - case codegenResult.codeGenerationStatusDetail?.includes('README_TOO_LARGE'): { - return new ReadmeTooLargeError() - } - case codegenResult.codeGenerationStatusDetail?.includes('README_UPDATE_TOO_LARGE'): { - return new ReadmeUpdateTooLargeError(codegenResult.codeGenerationRemainingIterationCount || 0) - } - case codegenResult.codeGenerationStatusDetail?.includes('WORKSPACE_TOO_LARGE'): { - return new ContentLengthError() - } - case codegenResult.codeGenerationStatusDetail?.includes('WORKSPACE_EMPTY'): { - return new WorkspaceEmptyError() - } - case codegenResult.codeGenerationStatusDetail?.includes('PROMPT_UNRELATED'): { - return new PromptUnrelatedError(codegenResult.codeGenerationRemainingIterationCount || 0) - } - case codegenResult.codeGenerationStatusDetail?.includes('PROMPT_TOO_VAGUE'): { - return new PromptTooVagueError(codegenResult.codeGenerationRemainingIterationCount || 0) - } - case codegenResult.codeGenerationStatusDetail?.includes('PROMPT_REFUSAL'): { - return new PromptRefusalException(codegenResult.codeGenerationRemainingIterationCount || 0) - } - case codegenResult.codeGenerationStatusDetail?.includes('Guardrails'): { - return new ApiClientError( - i18n('AWS.amazonq.doc.error.docGen.default'), - 'GetTaskAssistCodeGeneration', - 'GuardrailsException', - 400 - ) - } - case codegenResult.codeGenerationStatusDetail?.includes('EmptyPatch'): { - if (codegenResult.codeGenerationStatusDetail?.includes('NO_CHANGE_REQUIRED')) { - return new NoChangeRequiredException() - } - - return new LlmError(i18n('AWS.amazonq.doc.error.docGen.default'), { - code: 'EmptyPatchException', - }) - } - case codegenResult.codeGenerationStatusDetail?.includes('Throttling'): { - return new ApiClientError( - i18n('AWS.amazonq.featureDev.error.throttling'), - 'GetTaskAssistCodeGeneration', - 'ThrottlingException', - 429 - ) - } - default: { - return new ApiServiceError( - i18n('AWS.amazonq.doc.error.docGen.default'), - 'GetTaskAssistCodeGeneration', - 'UnknownException', - 500 - ) - } - } - } - - protected async startCodeGeneration(action: SessionStateAction, codeGenerationId: string): Promise { - if (!action.tokenSource?.token.isCancellationRequested) { - action.messenger.sendDocProgress(this.tabID, DocGenerationStep.SUMMARIZING_FILES, 0, action.mode as Mode) - } - - await this.config.proxyClient.startCodeGeneration( - this.config.conversationId, - this.config.uploadId, - action.msg, - Intent.DOC, - codeGenerationId, - undefined, - action.folderPath ? { documentation: { type: 'README', scope: action.folderPath } } : undefined - ) - } - - protected override createNextState(config: SessionStateConfig, params: CreateNextStateParams): SessionState { - return super.createNextState(config, params, DocPrepareCodeGenState) - } -} - -export class DocPrepareCodeGenState extends BasePrepareCodeGenState { - protected preUpload(action: SessionStateAction): void { - // Do nothing - } - - protected postUpload(action: SessionStateAction): void { - // Do nothing - } - - protected override createNextState(config: SessionStateConfig): SessionState { - return super.createNextState(config, DocCodeGenState) - } - - protected override async prepareProjectZip( - workspaceRoots: string[], - workspaceFolders: CurrentWsFolders, - span: Span, - options: PrepareRepoDataOptions - ) { - return await prepareRepoData(workspaceRoots, workspaceFolders, span, { - ...options, - isIncludeInfraDiagram: true, - }) - } -} diff --git a/packages/core/src/amazonqDoc/storages/chatSession.ts b/packages/core/src/amazonqDoc/storages/chatSession.ts deleted file mode 100644 index 34fb9f5404e..00000000000 --- a/packages/core/src/amazonqDoc/storages/chatSession.ts +++ /dev/null @@ -1,23 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { BaseChatSessionStorage } from '../../amazonq/commons/baseChatStorage' -import { createSessionConfig } from '../../amazonq/commons/session/sessionConfigFactory' -import { docScheme } from '../constants' -import { DocMessenger } from '../messenger' -import { Session } from '../session/session' - -export class DocChatSessionStorage extends BaseChatSessionStorage { - constructor(protected readonly messenger: DocMessenger) { - super() - } - - override async createSession(tabID: string): Promise { - const sessionConfig = await createSessionConfig(docScheme) - const session = new Session(sessionConfig, this.messenger, tabID) - this.sessions.set(tabID, session) - return session - } -} diff --git a/packages/core/src/amazonqDoc/types.ts b/packages/core/src/amazonqDoc/types.ts deleted file mode 100644 index 005353d0e23..00000000000 --- a/packages/core/src/amazonqDoc/types.ts +++ /dev/null @@ -1,83 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ChatItemButton, MynahIcons, ProgressField } from '@aws/mynah-ui' -import { - LLMResponseType, - SessionStorage, - SessionInfo, - DeletedFileInfo, - NewFileInfo, - NewFileZipContents, - SessionStateConfig, - SessionStatePhase, - DevPhase, - Interaction, - CurrentWsFolders, - CodeGenerationStatus, - SessionState as FeatureDevSessionState, - SessionStateAction as FeatureDevSessionStateAction, - SessionStateInteraction as FeatureDevSessionStateInteraction, -} from '../amazonq/commons/types' - -import { Mode } from './constants' -import { DocMessenger } from './messenger' - -export const cancelDocGenButton: ChatItemButton = { - id: 'cancel-doc-generation', - text: 'Cancel', - icon: 'cancel' as MynahIcons, -} - -export const inProgress = (progress: number, text: string): ProgressField => { - return { - status: 'default', - text, - value: progress === 100 ? -1 : progress, - actions: [cancelDocGenButton], - } -} - -export interface SessionStateInteraction extends FeatureDevSessionStateInteraction { - nextState: SessionState | Omit | undefined - interaction: Interaction -} - -export interface SessionState extends FeatureDevSessionState { - interact(action: SessionStateAction): Promise -} - -export interface SessionStateAction extends FeatureDevSessionStateAction { - messenger: DocMessenger - mode: Mode - folderPath?: string -} - -export enum MetricDataOperationName { - StartDocGeneration = 'StartDocGeneration', - EndDocGeneration = 'EndDocGeneration', -} - -export enum MetricDataResult { - Success = 'Success', - Fault = 'Fault', - Error = 'Error', - LlmFailure = 'LLMFailure', -} - -export { - LLMResponseType, - SessionStorage, - SessionInfo, - DeletedFileInfo, - NewFileInfo, - NewFileZipContents, - SessionStateConfig, - SessionStatePhase, - DevPhase, - Interaction, - CodeGenerationStatus, - CurrentWsFolders, -} diff --git a/packages/core/src/amazonqDoc/views/actions/uiMessageListener.ts b/packages/core/src/amazonqDoc/views/actions/uiMessageListener.ts deleted file mode 100644 index c6960b15fcc..00000000000 --- a/packages/core/src/amazonqDoc/views/actions/uiMessageListener.ts +++ /dev/null @@ -1,168 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ChatControllerEventEmitters } from '../../controllers/chat/controller' -import { MessageListener } from '../../../amazonq/messages/messageListener' -import { ExtensionMessage } from '../../../amazonq/webview/ui/commands' - -export interface UIMessageListenerProps { - readonly chatControllerEventEmitters: ChatControllerEventEmitters - readonly webViewMessageListener: MessageListener -} - -export class UIMessageListener { - private docGenerationControllerEventsEmitters: ChatControllerEventEmitters | undefined - private webViewMessageListener: MessageListener - - constructor(props: UIMessageListenerProps) { - this.docGenerationControllerEventsEmitters = props.chatControllerEventEmitters - this.webViewMessageListener = props.webViewMessageListener - - // Now we are listening to events that get sent from amazonq/webview/actions/actionListener (e.g. the tab) - this.webViewMessageListener.onMessage((msg) => { - this.handleMessage(msg) - }) - } - - private handleMessage(msg: ExtensionMessage) { - switch (msg.command) { - case 'chat-prompt': - this.processChatMessage(msg) - break - case 'follow-up-was-clicked': - this.followUpClicked(msg) - break - case 'open-diff': - this.openDiff(msg) - break - case 'chat-item-voted': - this.chatItemVoted(msg) - break - case 'chat-item-feedback': - this.chatItemFeedback(msg) - break - case 'stop-response': - this.stopResponse(msg) - break - case 'new-tab-was-created': - this.tabOpened(msg) - break - case 'tab-was-removed': - this.tabClosed(msg) - break - case 'auth-follow-up-was-clicked': - this.authClicked(msg) - break - case 'response-body-link-click': - this.processResponseBodyLinkClick(msg) - break - case 'insert_code_at_cursor_position': - this.insertCodeAtPosition(msg) - break - case 'file-click': - this.fileClicked(msg) - break - case 'form-action-click': - this.formActionClicked(msg) - break - } - } - - private chatItemVoted(msg: any) { - this.docGenerationControllerEventsEmitters?.processChatItemVotedMessage.fire({ - tabID: msg.tabID, - command: msg.command, - vote: msg.vote, - messageId: msg.messageId, - }) - } - - private chatItemFeedback(msg: any) { - this.docGenerationControllerEventsEmitters?.processChatItemFeedbackMessage.fire(msg) - } - - private processChatMessage(msg: any) { - this.docGenerationControllerEventsEmitters?.processHumanChatMessage.fire({ - message: msg.chatMessage, - tabID: msg.tabID, - }) - } - - private followUpClicked(msg: any) { - this.docGenerationControllerEventsEmitters?.followUpClicked.fire({ - followUp: msg.followUp, - tabID: msg.tabID, - }) - } - - private formActionClicked(msg: any) { - this.docGenerationControllerEventsEmitters?.formActionClicked.fire({ - ...msg, - }) - } - - private fileClicked(msg: any) { - this.docGenerationControllerEventsEmitters?.fileClicked.fire({ - tabID: msg.tabID, - filePath: msg.filePath, - actionName: msg.actionName, - messageId: msg.messageId, - }) - } - - private openDiff(msg: any) { - this.docGenerationControllerEventsEmitters?.openDiff.fire({ - tabID: msg.tabID, - filePath: msg.filePath, - deleted: msg.deleted, - messageId: msg.messageId, - }) - } - - private stopResponse(msg: any) { - this.docGenerationControllerEventsEmitters?.stopResponse.fire({ - tabID: msg.tabID, - }) - } - - private tabOpened(msg: any) { - this.docGenerationControllerEventsEmitters?.tabOpened.fire({ - tabID: msg.tabID, - }) - } - - private tabClosed(msg: any) { - this.docGenerationControllerEventsEmitters?.tabClosed.fire({ - tabID: msg.tabID, - }) - } - - private authClicked(msg: any) { - this.docGenerationControllerEventsEmitters?.authClicked.fire({ - tabID: msg.tabID, - authType: msg.authType, - }) - } - - private processResponseBodyLinkClick(msg: any) { - this.docGenerationControllerEventsEmitters?.processResponseBodyLinkClick.fire({ - command: msg.command, - messageId: msg.messageId, - tabID: msg.tabID, - link: msg.link, - }) - } - - private insertCodeAtPosition(msg: any) { - this.docGenerationControllerEventsEmitters?.insertCodeAtPositionClicked.fire({ - command: msg.command, - messageId: msg.messageId, - tabID: msg.tabID, - code: msg.code, - insertionTargetType: msg.insertionTargetType, - codeReference: msg.codeReference, - }) - } -} diff --git a/packages/core/src/amazonqFeatureDev/app.ts b/packages/core/src/amazonqFeatureDev/app.ts deleted file mode 100644 index a016d2ba481..00000000000 --- a/packages/core/src/amazonqFeatureDev/app.ts +++ /dev/null @@ -1,112 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import { UIMessageListener } from './views/actions/uiMessageListener' -import { ChatControllerEventEmitters, FeatureDevController } from './controllers/chat/controller' -import { AmazonQAppInitContext } from '../amazonq/apps/initContext' -import { MessagePublisher } from '../amazonq/messages/messagePublisher' -import { MessageListener } from '../amazonq/messages/messageListener' -import { fromQueryToParameters } from '../shared/utilities/uriUtils' -import { getLogger } from '../shared/logger/logger' -import { TabIdNotFoundError } from './errors' -import { featureDevChat, featureDevScheme } from './constants' -import globals from '../shared/extensionGlobals' -import { FeatureDevChatSessionStorage } from './storages/chatSession' -import { AuthUtil } from '../codewhisperer/util/authUtil' -import { debounce } from 'lodash' -import { Messenger } from '../amazonq/commons/connector/baseMessenger' -import { AppToWebViewMessageDispatcher } from '../amazonq/commons/connector/connectorMessages' - -export function init(appContext: AmazonQAppInitContext) { - const featureDevChatControllerEventEmitters: ChatControllerEventEmitters = { - 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(), - } - - const messenger = new Messenger( - new AppToWebViewMessageDispatcher(appContext.getAppsToWebViewMessagePublisher()), - featureDevChat - ) - const sessionStorage = new FeatureDevChatSessionStorage(messenger) - - new FeatureDevController( - featureDevChatControllerEventEmitters, - messenger, - sessionStorage, - appContext.onDidChangeAmazonQVisibility.event - ) - - const featureDevProvider = new (class implements vscode.TextDocumentContentProvider { - async provideTextDocumentContent(uri: vscode.Uri): Promise { - const params = fromQueryToParameters(uri.query) - - const tabID = params.get('tabID') - if (!tabID) { - getLogger().error(`Unable to find tabID from ${uri.toString()}`) - throw new TabIdNotFoundError() - } - - const session = await sessionStorage.getSession(tabID) - const content = await session.config.fs.readFile(uri) - const decodedContent = new TextDecoder().decode(content) - return decodedContent - } - })() - - const textDocumentProvider = vscode.workspace.registerTextDocumentContentProvider( - featureDevScheme, - featureDevProvider - ) - - globals.context.subscriptions.push(textDocumentProvider) - - const featureDevChatUIInputEventEmitter = new vscode.EventEmitter() - - new UIMessageListener({ - chatControllerEventEmitters: featureDevChatControllerEventEmitters, - webViewMessageListener: new MessageListener(featureDevChatUIInputEventEmitter), - }) - - appContext.registerWebViewToAppMessagePublisher( - new MessagePublisher(featureDevChatUIInputEventEmitter), - 'featuredev' - ) - - const debouncedEvent = debounce(async () => { - const authenticated = (await AuthUtil.instance.getChatAuthState()).amazonQ === 'connected' - let authenticatingSessionIDs: string[] = [] - if (authenticated) { - const authenticatingSessions = sessionStorage.getAuthenticatingSessions() - - authenticatingSessionIDs = authenticatingSessions.map((session) => session.tabID) - - // We've already authenticated these sessions - for (const session of authenticatingSessions) { - session.isAuthenticating = false - } - } - - messenger.sendAuthenticationUpdate(authenticated, authenticatingSessionIDs) - }, 500) - - AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(() => { - return debouncedEvent() - }) - AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { - return debouncedEvent() - }) -} diff --git a/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json b/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json deleted file mode 100644 index 812bbd4fd69..00000000000 --- a/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json +++ /dev/null @@ -1,5640 +0,0 @@ -{ - "version": "2.0", - "metadata": { - "apiVersion": "2022-11-11", - "auth": ["smithy.api#httpBearerAuth"], - "endpointPrefix": "amazoncodewhispererservice", - "jsonVersion": "1.0", - "protocol": "json", - "protocols": ["json"], - "serviceFullName": "Amazon CodeWhisperer", - "serviceId": "CodeWhispererRuntime", - "signatureVersion": "bearer", - "signingName": "amazoncodewhispererservice", - "targetPrefix": "AmazonCodeWhispererService", - "uid": "codewhispererruntime-2022-11-11" - }, - "operations": { - "CreateArtifactUploadUrl": { - "name": "CreateArtifactUploadUrl", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "CreateUploadUrlRequest" - }, - "output": { - "shape": "CreateUploadUrlResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ], - "idempotent": true - }, - "CreateTaskAssistConversation": { - "name": "CreateTaskAssistConversation", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "CreateTaskAssistConversationRequest" - }, - "output": { - "shape": "CreateTaskAssistConversationResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ServiceQuotaExceededException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "CreateUploadUrl": { - "name": "CreateUploadUrl", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "CreateUploadUrlRequest" - }, - "output": { - "shape": "CreateUploadUrlResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ConflictException" - }, - { - "shape": "ServiceQuotaExceededException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ], - "idempotent": true - }, - "CreateUserMemoryEntry": { - "name": "CreateUserMemoryEntry", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "CreateUserMemoryEntryInput" - }, - "output": { - "shape": "CreateUserMemoryEntryOutput" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ], - "idempotent": true - }, - "CreateWorkspace": { - "name": "CreateWorkspace", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "CreateWorkspaceRequest" - }, - "output": { - "shape": "CreateWorkspaceResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ConflictException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "DeleteTaskAssistConversation": { - "name": "DeleteTaskAssistConversation", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "DeleteTaskAssistConversationRequest" - }, - "output": { - "shape": "DeleteTaskAssistConversationResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "DeleteUserMemoryEntry": { - "name": "DeleteUserMemoryEntry", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "DeleteUserMemoryEntryInput" - }, - "output": { - "shape": "DeleteUserMemoryEntryOutput" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ], - "idempotent": true - }, - "DeleteWorkspace": { - "name": "DeleteWorkspace", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "DeleteWorkspaceRequest" - }, - "output": { - "shape": "DeleteWorkspaceResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "GenerateCompletions": { - "name": "GenerateCompletions", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "GenerateCompletionsRequest" - }, - "output": { - "shape": "GenerateCompletionsResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "GetCodeAnalysis": { - "name": "GetCodeAnalysis", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "GetCodeAnalysisRequest" - }, - "output": { - "shape": "GetCodeAnalysisResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "GetCodeFixJob": { - "name": "GetCodeFixJob", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "GetCodeFixJobRequest" - }, - "output": { - "shape": "GetCodeFixJobResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "GetTaskAssistCodeGeneration": { - "name": "GetTaskAssistCodeGeneration", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "GetTaskAssistCodeGenerationRequest" - }, - "output": { - "shape": "GetTaskAssistCodeGenerationResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ConflictException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "GetTestGeneration": { - "name": "GetTestGeneration", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "GetTestGenerationRequest" - }, - "output": { - "shape": "GetTestGenerationResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "GetTransformation": { - "name": "GetTransformation", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "GetTransformationRequest" - }, - "output": { - "shape": "GetTransformationResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "GetTransformationPlan": { - "name": "GetTransformationPlan", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "GetTransformationPlanRequest" - }, - "output": { - "shape": "GetTransformationPlanResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "ListAvailableCustomizations": { - "name": "ListAvailableCustomizations", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "ListAvailableCustomizationsRequest" - }, - "output": { - "shape": "ListAvailableCustomizationsResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "ListAvailableProfiles": { - "name": "ListAvailableProfiles", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "ListAvailableProfilesRequest" - }, - "output": { - "shape": "ListAvailableProfilesResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "ListCodeAnalysisFindings": { - "name": "ListCodeAnalysisFindings", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "ListCodeAnalysisFindingsRequest" - }, - "output": { - "shape": "ListCodeAnalysisFindingsResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "ListEvents": { - "name": "ListEvents", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "ListEventsRequest" - }, - "output": { - "shape": "ListEventsResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "ListFeatureEvaluations": { - "name": "ListFeatureEvaluations", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "ListFeatureEvaluationsRequest" - }, - "output": { - "shape": "ListFeatureEvaluationsResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "ListUserMemoryEntries": { - "name": "ListUserMemoryEntries", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "ListUserMemoryEntriesInput" - }, - "output": { - "shape": "ListUserMemoryEntriesOutput" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "ListWorkspaceMetadata": { - "name": "ListWorkspaceMetadata", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "ListWorkspaceMetadataRequest" - }, - "output": { - "shape": "ListWorkspaceMetadataResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "ResumeTransformation": { - "name": "ResumeTransformation", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "ResumeTransformationRequest" - }, - "output": { - "shape": "ResumeTransformationResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "SendTelemetryEvent": { - "name": "SendTelemetryEvent", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "SendTelemetryEventRequest" - }, - "output": { - "shape": "SendTelemetryEventResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ], - "idempotent": true - }, - "StartCodeAnalysis": { - "name": "StartCodeAnalysis", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "StartCodeAnalysisRequest" - }, - "output": { - "shape": "StartCodeAnalysisResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ConflictException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ], - "idempotent": true - }, - "StartCodeFixJob": { - "name": "StartCodeFixJob", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "StartCodeFixJobRequest" - }, - "output": { - "shape": "StartCodeFixJobResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "StartTaskAssistCodeGeneration": { - "name": "StartTaskAssistCodeGeneration", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "StartTaskAssistCodeGenerationRequest" - }, - "output": { - "shape": "StartTaskAssistCodeGenerationResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ConflictException" - }, - { - "shape": "ServiceQuotaExceededException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "StartTestGeneration": { - "name": "StartTestGeneration", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "StartTestGenerationRequest" - }, - "output": { - "shape": "StartTestGenerationResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ConflictException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ], - "idempotent": true - }, - "StartTransformation": { - "name": "StartTransformation", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "StartTransformationRequest" - }, - "output": { - "shape": "StartTransformationResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ConflictException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "StopTransformation": { - "name": "StopTransformation", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "StopTransformationRequest" - }, - "output": { - "shape": "StopTransformationResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - } - }, - "shapes": { - "AccessDeniedException": { - "type": "structure", - "required": ["message"], - "members": { - "message": { - "shape": "String" - }, - "reason": { - "shape": "AccessDeniedExceptionReason" - } - }, - "exception": true - }, - "AccessDeniedExceptionReason": { - "type": "string", - "enum": ["UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS"] - }, - "ActiveFunctionalityList": { - "type": "list", - "member": { - "shape": "FunctionalityName" - }, - "max": 10, - "min": 0 - }, - "AdditionalContentEntry": { - "type": "structure", - "required": ["name", "description"], - "members": { - "name": { - "shape": "AdditionalContentEntryNameString" - }, - "description": { - "shape": "AdditionalContentEntryDescriptionString" - }, - "innerContext": { - "shape": "AdditionalContentEntryInnerContextString" - } - } - }, - "AdditionalContentEntryDescriptionString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "AdditionalContentEntryInnerContextString": { - "type": "string", - "max": 8192, - "min": 1, - "sensitive": true - }, - "AdditionalContentEntryNameString": { - "type": "string", - "max": 1024, - "min": 1, - "pattern": "[a-z]+(?:-[a-z0-9]+)*", - "sensitive": true - }, - "AdditionalContentList": { - "type": "list", - "member": { - "shape": "AdditionalContentEntry" - }, - "max": 20, - "min": 0 - }, - "AppStudioState": { - "type": "structure", - "required": ["namespace", "propertyName", "propertyContext"], - "members": { - "namespace": { - "shape": "AppStudioStateNamespaceString" - }, - "propertyName": { - "shape": "AppStudioStatePropertyNameString" - }, - "propertyValue": { - "shape": "AppStudioStatePropertyValueString" - }, - "propertyContext": { - "shape": "AppStudioStatePropertyContextString" - } - } - }, - "AppStudioStateNamespaceString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "AppStudioStatePropertyContextString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "AppStudioStatePropertyNameString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "AppStudioStatePropertyValueString": { - "type": "string", - "max": 10240, - "min": 0, - "sensitive": true - }, - "ApplicationProperties": { - "type": "structure", - "required": ["tenantId", "applicationArn", "tenantUrl", "applicationType"], - "members": { - "tenantId": { - "shape": "TenantId" - }, - "applicationArn": { - "shape": "ResourceArn" - }, - "tenantUrl": { - "shape": "Url" - }, - "applicationType": { - "shape": "FunctionalityName" - } - } - }, - "ApplicationPropertiesList": { - "type": "list", - "member": { - "shape": "ApplicationProperties" - } - }, - "ArtifactId": { - "type": "string", - "max": 126, - "min": 1, - "pattern": "[a-zA-Z0-9-_]+" - }, - "ArtifactMap": { - "type": "map", - "key": { - "shape": "ArtifactType" - }, - "value": { - "shape": "UploadId" - }, - "max": 64, - "min": 1 - }, - "ArtifactType": { - "type": "string", - "enum": ["SourceCode", "BuiltJars"] - }, - "AssistantResponseMessage": { - "type": "structure", - "required": ["content"], - "members": { - "messageId": { - "shape": "MessageId" - }, - "content": { - "shape": "AssistantResponseMessageContentString" - }, - "supplementaryWebLinks": { - "shape": "SupplementaryWebLinks" - }, - "references": { - "shape": "References" - }, - "followupPrompt": { - "shape": "FollowupPrompt" - }, - "toolUses": { - "shape": "ToolUses" - } - } - }, - "AssistantResponseMessageContentString": { - "type": "string", - "max": 100000, - "min": 0, - "sensitive": true - }, - "AttributesMap": { - "type": "map", - "key": { - "shape": "AttributesMapKeyString" - }, - "value": { - "shape": "StringList" - } - }, - "AttributesMapKeyString": { - "type": "string", - "max": 128, - "min": 1 - }, - "Base64EncodedPaginationToken": { - "type": "string", - "max": 2048, - "min": 1, - "pattern": "(?:[A-Za-z0-9\\+/]{4})*(?:[A-Za-z0-9\\+/]{2}\\=\\=|[A-Za-z0-9\\+/]{3}\\=)?" - }, - "Boolean": { - "type": "boolean", - "box": true - }, - "ByUserAnalytics": { - "type": "structure", - "required": ["toggle"], - "members": { - "s3Uri": { - "shape": "S3Uri" - }, - "toggle": { - "shape": "OptInFeatureToggle" - } - } - }, - "ChatAddMessageEvent": { - "type": "structure", - "required": ["conversationId", "messageId"], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "messageId": { - "shape": "MessageId" - }, - "customizationArn": { - "shape": "CustomizationArn" - }, - "userIntent": { - "shape": "UserIntent" - }, - "hasCodeSnippet": { - "shape": "Boolean" - }, - "programmingLanguage": { - "shape": "ProgrammingLanguage" - }, - "activeEditorTotalCharacters": { - "shape": "Integer" - }, - "timeToFirstChunkMilliseconds": { - "shape": "Double" - }, - "timeBetweenChunks": { - "shape": "timeBetweenChunks" - }, - "fullResponselatency": { - "shape": "Double" - }, - "requestLength": { - "shape": "Integer" - }, - "responseLength": { - "shape": "Integer" - }, - "numberOfCodeBlocks": { - "shape": "Integer" - }, - "hasProjectLevelContext": { - "shape": "Boolean" - } - } - }, - "ChatHistory": { - "type": "list", - "member": { - "shape": "ChatMessage" - }, - "max": 250, - "min": 0 - }, - "ChatInteractWithMessageEvent": { - "type": "structure", - "required": ["conversationId", "messageId"], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "messageId": { - "shape": "MessageId" - }, - "customizationArn": { - "shape": "CustomizationArn" - }, - "interactionType": { - "shape": "ChatMessageInteractionType" - }, - "interactionTarget": { - "shape": "ChatInteractWithMessageEventInteractionTargetString" - }, - "acceptedCharacterCount": { - "shape": "Integer" - }, - "acceptedLineCount": { - "shape": "Integer" - }, - "acceptedSnippetHasReference": { - "shape": "Boolean" - }, - "hasProjectLevelContext": { - "shape": "Boolean" - }, - "userIntent": { - "shape": "UserIntent" - }, - "addedIdeDiagnostics": { - "shape": "IdeDiagnosticList" - }, - "removedIdeDiagnostics": { - "shape": "IdeDiagnosticList" - } - } - }, - "ChatInteractWithMessageEventInteractionTargetString": { - "type": "string", - "max": 1024, - "min": 1 - }, - "ChatMessage": { - "type": "structure", - "members": { - "userInputMessage": { - "shape": "UserInputMessage" - }, - "assistantResponseMessage": { - "shape": "AssistantResponseMessage" - } - }, - "union": true - }, - "ChatMessageInteractionType": { - "type": "string", - "enum": [ - "INSERT_AT_CURSOR", - "COPY_SNIPPET", - "COPY", - "CLICK_LINK", - "CLICK_BODY_LINK", - "CLICK_FOLLOW_UP", - "HOVER_REFERENCE", - "UPVOTE", - "DOWNVOTE" - ] - }, - "ChatTriggerType": { - "type": "string", - "enum": ["MANUAL", "DIAGNOSTIC", "INLINE_CHAT"] - }, - "ChatUserModificationEvent": { - "type": "structure", - "required": ["conversationId", "messageId", "modificationPercentage"], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "customizationArn": { - "shape": "CustomizationArn" - }, - "messageId": { - "shape": "MessageId" - }, - "programmingLanguage": { - "shape": "ProgrammingLanguage" - }, - "modificationPercentage": { - "shape": "Double" - }, - "hasProjectLevelContext": { - "shape": "Boolean" - } - } - }, - "ClientId": { - "type": "string", - "max": 255, - "min": 1 - }, - "CodeAnalysisFindingsSchema": { - "type": "string", - "enum": ["codeanalysis/findings/1.0"] - }, - "CodeAnalysisScope": { - "type": "string", - "enum": ["FILE", "PROJECT"] - }, - "CodeAnalysisStatus": { - "type": "string", - "enum": ["Completed", "Pending", "Failed"] - }, - "CodeAnalysisUploadContext": { - "type": "structure", - "required": ["codeScanName"], - "members": { - "codeScanName": { - "shape": "CodeScanName" - } - } - }, - "CodeCoverageEvent": { - "type": "structure", - "required": ["programmingLanguage", "acceptedCharacterCount", "totalCharacterCount", "timestamp"], - "members": { - "customizationArn": { - "shape": "CustomizationArn" - }, - "programmingLanguage": { - "shape": "ProgrammingLanguage" - }, - "acceptedCharacterCount": { - "shape": "PrimitiveInteger" - }, - "totalCharacterCount": { - "shape": "PrimitiveInteger" - }, - "timestamp": { - "shape": "Timestamp" - }, - "unmodifiedAcceptedCharacterCount": { - "shape": "PrimitiveInteger" - }, - "totalNewCodeCharacterCount": { - "shape": "PrimitiveInteger" - }, - "totalNewCodeLineCount": { - "shape": "PrimitiveInteger" - }, - "userWrittenCodeCharacterCount": { - "shape": "CodeCoverageEventUserWrittenCodeCharacterCountInteger" - }, - "userWrittenCodeLineCount": { - "shape": "CodeCoverageEventUserWrittenCodeLineCountInteger" - } - } - }, - "CodeCoverageEventUserWrittenCodeCharacterCountInteger": { - "type": "integer", - "min": 0 - }, - "CodeCoverageEventUserWrittenCodeLineCountInteger": { - "type": "integer", - "min": 0 - }, - "CodeDescription": { - "type": "structure", - "required": ["href"], - "members": { - "href": { - "shape": "CodeDescriptionHrefString" - } - } - }, - "CodeDescriptionHrefString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "CodeFixAcceptanceEvent": { - "type": "structure", - "required": ["jobId"], - "members": { - "jobId": { - "shape": "String" - }, - "ruleId": { - "shape": "String" - }, - "detectorId": { - "shape": "String" - }, - "findingId": { - "shape": "String" - }, - "programmingLanguage": { - "shape": "ProgrammingLanguage" - }, - "linesOfCodeAccepted": { - "shape": "Integer" - }, - "charsOfCodeAccepted": { - "shape": "Integer" - } - } - }, - "CodeFixGenerationEvent": { - "type": "structure", - "required": ["jobId"], - "members": { - "jobId": { - "shape": "String" - }, - "ruleId": { - "shape": "String" - }, - "detectorId": { - "shape": "String" - }, - "findingId": { - "shape": "String" - }, - "programmingLanguage": { - "shape": "ProgrammingLanguage" - }, - "linesOfCodeGenerated": { - "shape": "Integer" - }, - "charsOfCodeGenerated": { - "shape": "Integer" - } - } - }, - "CodeFixJobStatus": { - "type": "string", - "enum": ["Succeeded", "InProgress", "Failed"] - }, - "CodeFixName": { - "type": "string", - "max": 128, - "min": 1, - "pattern": "[a-zA-Z0-9-_$:.]*" - }, - "CodeFixUploadContext": { - "type": "structure", - "required": ["codeFixName"], - "members": { - "codeFixName": { - "shape": "CodeFixName" - } - } - }, - "CodeGenerationId": { - "type": "string", - "max": 128, - "min": 1 - }, - "CodeGenerationStatus": { - "type": "structure", - "required": ["status", "currentStage"], - "members": { - "status": { - "shape": "CodeGenerationWorkflowStatus" - }, - "currentStage": { - "shape": "CodeGenerationWorkflowStage" - } - } - }, - "CodeGenerationStatusDetail": { - "type": "string", - "sensitive": true - }, - "CodeGenerationWorkflowStage": { - "type": "string", - "enum": ["InitialCodeGeneration", "CodeRefinement"] - }, - "CodeGenerationWorkflowStatus": { - "type": "string", - "enum": ["InProgress", "Complete", "Failed"] - }, - "CodeScanEvent": { - "type": "structure", - "required": ["programmingLanguage", "codeScanJobId", "timestamp"], - "members": { - "programmingLanguage": { - "shape": "ProgrammingLanguage" - }, - "codeScanJobId": { - "shape": "CodeScanJobId" - }, - "timestamp": { - "shape": "Timestamp" - }, - "codeAnalysisScope": { - "shape": "CodeAnalysisScope" - } - } - }, - "CodeScanFailedEvent": { - "type": "structure", - "required": ["programmingLanguage", "codeScanJobId", "timestamp"], - "members": { - "programmingLanguage": { - "shape": "ProgrammingLanguage" - }, - "codeScanJobId": { - "shape": "CodeScanJobId" - }, - "timestamp": { - "shape": "Timestamp" - }, - "codeAnalysisScope": { - "shape": "CodeAnalysisScope" - } - } - }, - "CodeScanJobId": { - "type": "string", - "max": 128, - "min": 1 - }, - "CodeScanName": { - "type": "string", - "max": 128, - "min": 1 - }, - "CodeScanRemediationsEvent": { - "type": "structure", - "members": { - "programmingLanguage": { - "shape": "ProgrammingLanguage" - }, - "CodeScanRemediationsEventType": { - "shape": "CodeScanRemediationsEventType" - }, - "timestamp": { - "shape": "Timestamp" - }, - "detectorId": { - "shape": "String" - }, - "findingId": { - "shape": "String" - }, - "ruleId": { - "shape": "String" - }, - "component": { - "shape": "String" - }, - "reason": { - "shape": "String" - }, - "result": { - "shape": "String" - }, - "includesFix": { - "shape": "Boolean" - } - } - }, - "CodeScanRemediationsEventType": { - "type": "string", - "enum": ["CODESCAN_ISSUE_HOVER", "CODESCAN_ISSUE_APPLY_FIX", "CODESCAN_ISSUE_VIEW_DETAILS"] - }, - "CodeScanSucceededEvent": { - "type": "structure", - "required": ["programmingLanguage", "codeScanJobId", "timestamp", "numberOfFindings"], - "members": { - "programmingLanguage": { - "shape": "ProgrammingLanguage" - }, - "codeScanJobId": { - "shape": "CodeScanJobId" - }, - "timestamp": { - "shape": "Timestamp" - }, - "numberOfFindings": { - "shape": "PrimitiveInteger" - }, - "codeAnalysisScope": { - "shape": "CodeAnalysisScope" - } - } - }, - "Completion": { - "type": "structure", - "required": ["content"], - "members": { - "content": { - "shape": "CompletionContentString" - }, - "references": { - "shape": "References" - }, - "mostRelevantMissingImports": { - "shape": "Imports" - } - } - }, - "CompletionContentString": { - "type": "string", - "max": 5120, - "min": 1, - "sensitive": true - }, - "CompletionType": { - "type": "string", - "enum": ["BLOCK", "LINE"] - }, - "Completions": { - "type": "list", - "member": { - "shape": "Completion" - }, - "max": 10, - "min": 0 - }, - "ConflictException": { - "type": "structure", - "required": ["message"], - "members": { - "message": { - "shape": "String" - }, - "reason": { - "shape": "ConflictExceptionReason" - } - }, - "exception": true - }, - "ConflictExceptionReason": { - "type": "string", - "enum": ["CUSTOMER_KMS_KEY_INVALID_KEY_POLICY", "CUSTOMER_KMS_KEY_DISABLED", "MISMATCHED_KMS_KEY"] - }, - "ConsoleState": { - "type": "structure", - "members": { - "region": { - "shape": "String" - }, - "consoleUrl": { - "shape": "SensitiveString" - }, - "serviceId": { - "shape": "String" - }, - "serviceConsolePage": { - "shape": "String" - }, - "serviceSubconsolePage": { - "shape": "String" - }, - "taskName": { - "shape": "SensitiveString" - } - } - }, - "ContentChecksumType": { - "type": "string", - "enum": ["SHA_256"] - }, - "ContextTruncationScheme": { - "type": "string", - "enum": ["ANALYSIS", "GUMBY"] - }, - "ConversationId": { - "type": "string", - "max": 128, - "min": 1 - }, - "ConversationState": { - "type": "structure", - "required": ["currentMessage", "chatTriggerType"], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "history": { - "shape": "ChatHistory" - }, - "currentMessage": { - "shape": "ChatMessage" - }, - "chatTriggerType": { - "shape": "ChatTriggerType" - }, - "customizationArn": { - "shape": "ResourceArn" - } - } - }, - "CreateTaskAssistConversationRequest": { - "type": "structure", - "members": { - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "CreateTaskAssistConversationResponse": { - "type": "structure", - "required": ["conversationId"], - "members": { - "conversationId": { - "shape": "ConversationId" - } - } - }, - "CreateUploadUrlRequest": { - "type": "structure", - "members": { - "contentMd5": { - "shape": "CreateUploadUrlRequestContentMd5String" - }, - "contentChecksum": { - "shape": "CreateUploadUrlRequestContentChecksumString" - }, - "contentChecksumType": { - "shape": "ContentChecksumType" - }, - "contentLength": { - "shape": "CreateUploadUrlRequestContentLengthLong" - }, - "artifactType": { - "shape": "ArtifactType" - }, - "uploadIntent": { - "shape": "UploadIntent" - }, - "uploadContext": { - "shape": "UploadContext" - }, - "uploadId": { - "shape": "UploadId" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "CreateUploadUrlRequestContentChecksumString": { - "type": "string", - "max": 512, - "min": 1, - "sensitive": true - }, - "CreateUploadUrlRequestContentLengthLong": { - "type": "long", - "box": true, - "min": 1 - }, - "CreateUploadUrlRequestContentMd5String": { - "type": "string", - "max": 128, - "min": 1, - "sensitive": true - }, - "CreateUploadUrlResponse": { - "type": "structure", - "required": ["uploadId", "uploadUrl"], - "members": { - "uploadId": { - "shape": "UploadId" - }, - "uploadUrl": { - "shape": "PreSignedUrl" - }, - "kmsKeyArn": { - "shape": "ResourceArn" - }, - "requestHeaders": { - "shape": "RequestHeaders" - } - } - }, - "CreateUserMemoryEntryInput": { - "type": "structure", - "required": ["memoryEntryString", "origin"], - "members": { - "memoryEntryString": { - "shape": "CreateUserMemoryEntryInputMemoryEntryStringString" - }, - "origin": { - "shape": "Origin" - }, - "profileArn": { - "shape": "CreateUserMemoryEntryInputProfileArnString" - }, - "clientToken": { - "shape": "String", - "idempotencyToken": true - } - } - }, - "CreateUserMemoryEntryInputMemoryEntryStringString": { - "type": "string", - "min": 1, - "sensitive": true - }, - "CreateUserMemoryEntryInputProfileArnString": { - "type": "string", - "min": 1, - "pattern": "arn:aws:codewhisperer:[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" - }, - "CreateUserMemoryEntryOutput": { - "type": "structure", - "required": ["memoryEntry"], - "members": { - "memoryEntry": { - "shape": "MemoryEntry" - } - } - }, - "CreateWorkspaceRequest": { - "type": "structure", - "required": ["workspaceRoot"], - "members": { - "workspaceRoot": { - "shape": "CreateWorkspaceRequestWorkspaceRootString" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "CreateWorkspaceRequestWorkspaceRootString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "CreateWorkspaceResponse": { - "type": "structure", - "required": ["workspace"], - "members": { - "workspace": { - "shape": "WorkspaceMetadata" - } - } - }, - "CursorState": { - "type": "structure", - "members": { - "position": { - "shape": "Position" - }, - "range": { - "shape": "Range" - } - }, - "union": true - }, - "Customization": { - "type": "structure", - "required": ["arn"], - "members": { - "arn": { - "shape": "CustomizationArn" - }, - "name": { - "shape": "CustomizationName" - }, - "description": { - "shape": "Description" - } - } - }, - "CustomizationArn": { - "type": "string", - "max": 950, - "min": 0, - "pattern": "arn:[-.a-z0-9]{1,63}:codewhisperer:([-.a-z0-9]{0,63}:){2}([a-zA-Z0-9-_:/]){1,1023}" - }, - "CustomizationName": { - "type": "string", - "max": 100, - "min": 1, - "pattern": "[a-zA-Z][a-zA-Z0-9_-]*" - }, - "Customizations": { - "type": "list", - "member": { - "shape": "Customization" - } - }, - "DashboardAnalytics": { - "type": "structure", - "required": ["toggle"], - "members": { - "toggle": { - "shape": "OptInFeatureToggle" - } - } - }, - "DeleteTaskAssistConversationRequest": { - "type": "structure", - "required": ["conversationId"], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "DeleteTaskAssistConversationResponse": { - "type": "structure", - "required": ["conversationId"], - "members": { - "conversationId": { - "shape": "ConversationId" - } - } - }, - "DeleteUserMemoryEntryInput": { - "type": "structure", - "required": ["id"], - "members": { - "id": { - "shape": "DeleteUserMemoryEntryInputIdString" - }, - "profileArn": { - "shape": "DeleteUserMemoryEntryInputProfileArnString" - } - } - }, - "DeleteUserMemoryEntryInputIdString": { - "type": "string", - "max": 36, - "min": 36, - "pattern": "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" - }, - "DeleteUserMemoryEntryInputProfileArnString": { - "type": "string", - "min": 1, - "pattern": "arn:aws:codewhisperer:[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" - }, - "DeleteUserMemoryEntryOutput": { - "type": "structure", - "members": {} - }, - "DeleteWorkspaceRequest": { - "type": "structure", - "required": ["workspaceId"], - "members": { - "workspaceId": { - "shape": "UUID" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "DeleteWorkspaceResponse": { - "type": "structure", - "members": {} - }, - "Description": { - "type": "string", - "max": 256, - "min": 0, - "pattern": "[\\sa-zA-Z0-9_-]*" - }, - "Diagnostic": { - "type": "structure", - "members": { - "textDocumentDiagnostic": { - "shape": "TextDocumentDiagnostic" - }, - "runtimeDiagnostic": { - "shape": "RuntimeDiagnostic" - } - }, - "union": true - }, - "DiagnosticLocation": { - "type": "structure", - "required": ["uri", "range"], - "members": { - "uri": { - "shape": "DiagnosticLocationUriString" - }, - "range": { - "shape": "Range" - } - } - }, - "DiagnosticLocationUriString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "DiagnosticRelatedInformation": { - "type": "structure", - "required": ["location", "message"], - "members": { - "location": { - "shape": "DiagnosticLocation" - }, - "message": { - "shape": "DiagnosticRelatedInformationMessageString" - } - } - }, - "DiagnosticRelatedInformationList": { - "type": "list", - "member": { - "shape": "DiagnosticRelatedInformation" - }, - "max": 1024, - "min": 0 - }, - "DiagnosticRelatedInformationMessageString": { - "type": "string", - "max": 1024, - "min": 0, - "sensitive": true - }, - "DiagnosticSeverity": { - "type": "string", - "enum": ["ERROR", "WARNING", "INFORMATION", "HINT"] - }, - "DiagnosticTag": { - "type": "string", - "enum": ["UNNECESSARY", "DEPRECATED"] - }, - "DiagnosticTagList": { - "type": "list", - "member": { - "shape": "DiagnosticTag" - }, - "max": 1024, - "min": 0 - }, - "Dimension": { - "type": "structure", - "members": { - "name": { - "shape": "DimensionNameString" - }, - "value": { - "shape": "DimensionValueString" - } - } - }, - "DimensionList": { - "type": "list", - "member": { - "shape": "Dimension" - }, - "max": 30, - "min": 0 - }, - "DimensionNameString": { - "type": "string", - "max": 255, - "min": 1, - "pattern": "[-a-zA-Z0-9._]*" - }, - "DimensionValueString": { - "type": "string", - "max": 1024, - "min": 1, - "pattern": "[-a-zA-Z0-9._]*" - }, - "DocFolderLevel": { - "type": "string", - "enum": ["SUB_FOLDER", "ENTIRE_WORKSPACE"] - }, - "DocGenerationEvent": { - "type": "structure", - "required": ["conversationId"], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "numberOfAddChars": { - "shape": "PrimitiveInteger" - }, - "numberOfAddLines": { - "shape": "PrimitiveInteger" - }, - "numberOfAddFiles": { - "shape": "PrimitiveInteger" - }, - "userDecision": { - "shape": "DocUserDecision" - }, - "interactionType": { - "shape": "DocInteractionType" - }, - "userIdentity": { - "shape": "String" - }, - "numberOfNavigation": { - "shape": "PrimitiveInteger" - }, - "folderLevel": { - "shape": "DocFolderLevel" - } - } - }, - "DocInteractionType": { - "type": "string", - "enum": ["GENERATE_README", "UPDATE_README", "EDIT_README"] - }, - "DocUserDecision": { - "type": "string", - "enum": ["ACCEPT", "REJECT"] - }, - "DocV2AcceptanceEvent": { - "type": "structure", - "required": [ - "conversationId", - "numberOfAddedChars", - "numberOfAddedLines", - "numberOfAddedFiles", - "userDecision", - "interactionType", - "numberOfNavigations", - "folderLevel" - ], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "numberOfAddedChars": { - "shape": "DocV2AcceptanceEventNumberOfAddedCharsInteger" - }, - "numberOfAddedLines": { - "shape": "DocV2AcceptanceEventNumberOfAddedLinesInteger" - }, - "numberOfAddedFiles": { - "shape": "DocV2AcceptanceEventNumberOfAddedFilesInteger" - }, - "userDecision": { - "shape": "DocUserDecision" - }, - "interactionType": { - "shape": "DocInteractionType" - }, - "numberOfNavigations": { - "shape": "DocV2AcceptanceEventNumberOfNavigationsInteger" - }, - "folderLevel": { - "shape": "DocFolderLevel" - } - } - }, - "DocV2AcceptanceEventNumberOfAddedCharsInteger": { - "type": "integer", - "min": 0 - }, - "DocV2AcceptanceEventNumberOfAddedFilesInteger": { - "type": "integer", - "min": 0 - }, - "DocV2AcceptanceEventNumberOfAddedLinesInteger": { - "type": "integer", - "min": 0 - }, - "DocV2AcceptanceEventNumberOfNavigationsInteger": { - "type": "integer", - "min": 0 - }, - "DocV2GenerationEvent": { - "type": "structure", - "required": [ - "conversationId", - "numberOfGeneratedChars", - "numberOfGeneratedLines", - "numberOfGeneratedFiles" - ], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "numberOfGeneratedChars": { - "shape": "DocV2GenerationEventNumberOfGeneratedCharsInteger" - }, - "numberOfGeneratedLines": { - "shape": "DocV2GenerationEventNumberOfGeneratedLinesInteger" - }, - "numberOfGeneratedFiles": { - "shape": "DocV2GenerationEventNumberOfGeneratedFilesInteger" - }, - "interactionType": { - "shape": "DocInteractionType" - }, - "numberOfNavigations": { - "shape": "DocV2GenerationEventNumberOfNavigationsInteger" - }, - "folderLevel": { - "shape": "DocFolderLevel" - } - } - }, - "DocV2GenerationEventNumberOfGeneratedCharsInteger": { - "type": "integer", - "min": 0 - }, - "DocV2GenerationEventNumberOfGeneratedFilesInteger": { - "type": "integer", - "min": 0 - }, - "DocV2GenerationEventNumberOfGeneratedLinesInteger": { - "type": "integer", - "min": 0 - }, - "DocV2GenerationEventNumberOfNavigationsInteger": { - "type": "integer", - "min": 0 - }, - "DocumentSymbol": { - "type": "structure", - "required": ["name", "type"], - "members": { - "name": { - "shape": "DocumentSymbolNameString" - }, - "type": { - "shape": "SymbolType" - }, - "source": { - "shape": "DocumentSymbolSourceString" - } - } - }, - "DocumentSymbolNameString": { - "type": "string", - "max": 256, - "min": 1 - }, - "DocumentSymbolSourceString": { - "type": "string", - "max": 256, - "min": 1 - }, - "DocumentSymbols": { - "type": "list", - "member": { - "shape": "DocumentSymbol" - }, - "max": 1000, - "min": 0 - }, - "DocumentationIntentContext": { - "type": "structure", - "required": ["type"], - "members": { - "scope": { - "shape": "DocumentationIntentContextScopeString" - }, - "type": { - "shape": "DocumentationType" - } - } - }, - "DocumentationIntentContextScopeString": { - "type": "string", - "max": 4096, - "min": 1, - "sensitive": true - }, - "DocumentationType": { - "type": "string", - "enum": ["README"] - }, - "Double": { - "type": "double", - "box": true - }, - "EditorState": { - "type": "structure", - "members": { - "document": { - "shape": "TextDocument" - }, - "cursorState": { - "shape": "CursorState" - }, - "relevantDocuments": { - "shape": "RelevantDocumentList" - }, - "useRelevantDocuments": { - "shape": "Boolean" - }, - "workspaceFolders": { - "shape": "WorkspaceFolderList" - } - } - }, - "EnvState": { - "type": "structure", - "members": { - "operatingSystem": { - "shape": "EnvStateOperatingSystemString" - }, - "currentWorkingDirectory": { - "shape": "EnvStateCurrentWorkingDirectoryString" - }, - "environmentVariables": { - "shape": "EnvironmentVariables" - }, - "timezoneOffset": { - "shape": "EnvStateTimezoneOffsetInteger" - } - } - }, - "EnvStateCurrentWorkingDirectoryString": { - "type": "string", - "max": 256, - "min": 1, - "sensitive": true - }, - "EnvStateOperatingSystemString": { - "type": "string", - "max": 32, - "min": 1, - "pattern": "(macos|linux|windows)" - }, - "EnvStateTimezoneOffsetInteger": { - "type": "integer", - "box": true, - "max": 1440, - "min": -1440 - }, - "EnvironmentVariable": { - "type": "structure", - "members": { - "key": { - "shape": "EnvironmentVariableKeyString" - }, - "value": { - "shape": "EnvironmentVariableValueString" - } - } - }, - "EnvironmentVariableKeyString": { - "type": "string", - "max": 256, - "min": 1, - "sensitive": true - }, - "EnvironmentVariableValueString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "EnvironmentVariables": { - "type": "list", - "member": { - "shape": "EnvironmentVariable" - }, - "max": 100, - "min": 0 - }, - "ErrorDetails": { - "type": "string", - "max": 2048, - "min": 0 - }, - "Event": { - "type": "structure", - "required": ["eventId", "generationId", "eventTimestamp", "eventType", "eventBlob"], - "members": { - "eventId": { - "shape": "UUID" - }, - "generationId": { - "shape": "UUID" - }, - "eventTimestamp": { - "shape": "SyntheticTimestamp_date_time" - }, - "eventType": { - "shape": "EventType" - }, - "eventBlob": { - "shape": "EventBlob" - } - } - }, - "EventBlob": { - "type": "blob", - "max": 400000, - "min": 1, - "sensitive": true - }, - "EventList": { - "type": "list", - "member": { - "shape": "Event" - }, - "max": 10, - "min": 1 - }, - "EventType": { - "type": "string", - "max": 100, - "min": 1 - }, - "ExternalIdentityDetails": { - "type": "structure", - "members": { - "issuerUrl": { - "shape": "IssuerUrl" - }, - "clientId": { - "shape": "ClientId" - }, - "scimEndpoint": { - "shape": "String" - } - } - }, - "FeatureDevCodeAcceptanceEvent": { - "type": "structure", - "required": ["conversationId", "linesOfCodeAccepted", "charactersOfCodeAccepted"], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "linesOfCodeAccepted": { - "shape": "FeatureDevCodeAcceptanceEventLinesOfCodeAcceptedInteger" - }, - "charactersOfCodeAccepted": { - "shape": "FeatureDevCodeAcceptanceEventCharactersOfCodeAcceptedInteger" - }, - "programmingLanguage": { - "shape": "ProgrammingLanguage" - } - } - }, - "FeatureDevCodeAcceptanceEventCharactersOfCodeAcceptedInteger": { - "type": "integer", - "min": 0 - }, - "FeatureDevCodeAcceptanceEventLinesOfCodeAcceptedInteger": { - "type": "integer", - "min": 0 - }, - "FeatureDevCodeGenerationEvent": { - "type": "structure", - "required": ["conversationId", "linesOfCodeGenerated", "charactersOfCodeGenerated"], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "linesOfCodeGenerated": { - "shape": "FeatureDevCodeGenerationEventLinesOfCodeGeneratedInteger" - }, - "charactersOfCodeGenerated": { - "shape": "FeatureDevCodeGenerationEventCharactersOfCodeGeneratedInteger" - }, - "programmingLanguage": { - "shape": "ProgrammingLanguage" - } - } - }, - "FeatureDevCodeGenerationEventCharactersOfCodeGeneratedInteger": { - "type": "integer", - "min": 0 - }, - "FeatureDevCodeGenerationEventLinesOfCodeGeneratedInteger": { - "type": "integer", - "min": 0 - }, - "FeatureDevEvent": { - "type": "structure", - "required": ["conversationId"], - "members": { - "conversationId": { - "shape": "ConversationId" - } - } - }, - "FeatureEvaluation": { - "type": "structure", - "required": ["feature", "variation", "value"], - "members": { - "feature": { - "shape": "FeatureName" - }, - "variation": { - "shape": "FeatureVariation" - }, - "value": { - "shape": "FeatureValue" - } - } - }, - "FeatureEvaluationsList": { - "type": "list", - "member": { - "shape": "FeatureEvaluation" - }, - "max": 50, - "min": 0 - }, - "FeatureName": { - "type": "string", - "max": 128, - "min": 1, - "pattern": "[-a-zA-Z0-9._]*" - }, - "FeatureValue": { - "type": "structure", - "members": { - "boolValue": { - "shape": "Boolean" - }, - "doubleValue": { - "shape": "Double" - }, - "longValue": { - "shape": "Long" - }, - "stringValue": { - "shape": "FeatureValueStringType" - } - }, - "union": true - }, - "FeatureValueStringType": { - "type": "string", - "max": 512, - "min": 0 - }, - "FeatureVariation": { - "type": "string", - "max": 128, - "min": 1, - "pattern": "[-a-zA-Z0-9._]*" - }, - "FileContext": { - "type": "structure", - "required": ["leftFileContent", "rightFileContent", "filename", "programmingLanguage"], - "members": { - "leftFileContent": { - "shape": "FileContextLeftFileContentString" - }, - "rightFileContent": { - "shape": "FileContextRightFileContentString" - }, - "filename": { - "shape": "FileContextFilenameString" - }, - "programmingLanguage": { - "shape": "ProgrammingLanguage" - } - } - }, - "FileContextFilenameString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "FileContextLeftFileContentString": { - "type": "string", - "max": 10240, - "min": 0, - "sensitive": true - }, - "FileContextRightFileContentString": { - "type": "string", - "max": 10240, - "min": 0, - "sensitive": true - }, - "FollowupPrompt": { - "type": "structure", - "required": ["content"], - "members": { - "content": { - "shape": "FollowupPromptContentString" - }, - "userIntent": { - "shape": "UserIntent" - } - } - }, - "FollowupPromptContentString": { - "type": "string", - "max": 4096, - "min": 0, - "sensitive": true - }, - "FunctionalityName": { - "type": "string", - "enum": [ - "COMPLETIONS", - "ANALYSIS", - "CONVERSATIONS", - "TASK_ASSIST", - "TRANSFORMATIONS", - "CHAT_CUSTOMIZATION", - "TRANSFORMATIONS_WEBAPP", - "FEATURE_DEVELOPMENT" - ], - "max": 64, - "min": 1 - }, - "GenerateCompletionsRequest": { - "type": "structure", - "required": ["fileContext"], - "members": { - "fileContext": { - "shape": "FileContext" - }, - "maxResults": { - "shape": "GenerateCompletionsRequestMaxResultsInteger" - }, - "nextToken": { - "shape": "GenerateCompletionsRequestNextTokenString" - }, - "referenceTrackerConfiguration": { - "shape": "ReferenceTrackerConfiguration" - }, - "supplementalContexts": { - "shape": "SupplementalContextList" - }, - "customizationArn": { - "shape": "CustomizationArn" - }, - "optOutPreference": { - "shape": "OptOutPreference" - }, - "userContext": { - "shape": "UserContext" - }, - "profileArn": { - "shape": "ProfileArn" - }, - "workspaceId": { - "shape": "UUID" - } - } - }, - "GenerateCompletionsRequestMaxResultsInteger": { - "type": "integer", - "box": true, - "max": 10, - "min": 1 - }, - "GenerateCompletionsRequestNextTokenString": { - "type": "string", - "max": 2048, - "min": 0, - "pattern": "(?:[A-Za-z0-9\\+/]{4})*(?:[A-Za-z0-9\\+/]{2}\\=\\=|[A-Za-z0-9\\+/]{3}\\=)?", - "sensitive": true - }, - "GenerateCompletionsResponse": { - "type": "structure", - "members": { - "completions": { - "shape": "Completions" - }, - "nextToken": { - "shape": "SensitiveString" - } - } - }, - "GetCodeAnalysisRequest": { - "type": "structure", - "required": ["jobId"], - "members": { - "jobId": { - "shape": "GetCodeAnalysisRequestJobIdString" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "GetCodeAnalysisRequestJobIdString": { - "type": "string", - "max": 256, - "min": 1 - }, - "GetCodeAnalysisResponse": { - "type": "structure", - "required": ["status"], - "members": { - "status": { - "shape": "CodeAnalysisStatus" - }, - "errorMessage": { - "shape": "SensitiveString" - } - } - }, - "GetCodeFixJobRequest": { - "type": "structure", - "required": ["jobId"], - "members": { - "jobId": { - "shape": "GetCodeFixJobRequestJobIdString" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "GetCodeFixJobRequestJobIdString": { - "type": "string", - "max": 256, - "min": 1, - "pattern": ".*[A-Za-z0-9-:]+.*" - }, - "GetCodeFixJobResponse": { - "type": "structure", - "members": { - "jobStatus": { - "shape": "CodeFixJobStatus" - }, - "suggestedFix": { - "shape": "SuggestedFix" - } - } - }, - "GetTaskAssistCodeGenerationRequest": { - "type": "structure", - "required": ["conversationId", "codeGenerationId"], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "codeGenerationId": { - "shape": "CodeGenerationId" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "GetTaskAssistCodeGenerationResponse": { - "type": "structure", - "required": ["conversationId", "codeGenerationStatus"], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "codeGenerationStatus": { - "shape": "CodeGenerationStatus" - }, - "codeGenerationStatusDetail": { - "shape": "CodeGenerationStatusDetail" - }, - "codeGenerationRemainingIterationCount": { - "shape": "Integer" - }, - "codeGenerationTotalIterationCount": { - "shape": "Integer" - } - } - }, - "GetTestGenerationRequest": { - "type": "structure", - "required": ["testGenerationJobGroupName", "testGenerationJobId"], - "members": { - "testGenerationJobGroupName": { - "shape": "TestGenerationJobGroupName" - }, - "testGenerationJobId": { - "shape": "UUID" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "GetTestGenerationResponse": { - "type": "structure", - "members": { - "testGenerationJob": { - "shape": "TestGenerationJob" - } - } - }, - "GetTransformationPlanRequest": { - "type": "structure", - "required": ["transformationJobId"], - "members": { - "transformationJobId": { - "shape": "TransformationJobId" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "GetTransformationPlanResponse": { - "type": "structure", - "required": ["transformationPlan"], - "members": { - "transformationPlan": { - "shape": "TransformationPlan" - } - } - }, - "GetTransformationRequest": { - "type": "structure", - "required": ["transformationJobId"], - "members": { - "transformationJobId": { - "shape": "TransformationJobId" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "GetTransformationResponse": { - "type": "structure", - "required": ["transformationJob"], - "members": { - "transformationJob": { - "shape": "TransformationJob" - } - } - }, - "GitState": { - "type": "structure", - "members": { - "status": { - "shape": "GitStateStatusString" - } - } - }, - "GitStateStatusString": { - "type": "string", - "max": 4096, - "min": 0, - "sensitive": true - }, - "IdeCategory": { - "type": "string", - "enum": ["JETBRAINS", "VSCODE", "CLI", "JUPYTER_MD", "JUPYTER_SM", "ECLIPSE", "VISUAL_STUDIO"], - "max": 64, - "min": 1 - }, - "IdeDiagnostic": { - "type": "structure", - "required": ["ideDiagnosticType"], - "members": { - "range": { - "shape": "Range" - }, - "source": { - "shape": "IdeDiagnosticSourceString" - }, - "severity": { - "shape": "DiagnosticSeverity" - }, - "ideDiagnosticType": { - "shape": "IdeDiagnosticType" - } - } - }, - "IdeDiagnosticList": { - "type": "list", - "member": { - "shape": "IdeDiagnostic" - }, - "max": 1024, - "min": 0 - }, - "IdeDiagnosticSourceString": { - "type": "string", - "max": 1024, - "min": 0, - "sensitive": true - }, - "IdeDiagnosticType": { - "type": "string", - "enum": ["SYNTAX_ERROR", "TYPE_ERROR", "REFERENCE_ERROR", "BEST_PRACTICE", "SECURITY", "OTHER"] - }, - "IdempotencyToken": { - "type": "string", - "max": 256, - "min": 1 - }, - "IdentityDetails": { - "type": "structure", - "members": { - "ssoIdentityDetails": { - "shape": "SSOIdentityDetails" - }, - "externalIdentityDetails": { - "shape": "ExternalIdentityDetails" - } - }, - "union": true - }, - "ImageBlock": { - "type": "structure", - "required": ["format", "source"], - "members": { - "format": { - "shape": "ImageFormat" - }, - "source": { - "shape": "ImageSource" - } - } - }, - "ImageBlocks": { - "type": "list", - "member": { - "shape": "ImageBlock" - }, - "max": 10, - "min": 0 - }, - "ImageFormat": { - "type": "string", - "enum": ["png", "jpeg", "gif", "webp"] - }, - "ImageSource": { - "type": "structure", - "members": { - "bytes": { - "shape": "ImageSourceBytesBlob" - } - }, - "sensitive": true, - "union": true - }, - "ImageSourceBytesBlob": { - "type": "blob", - "max": 1500000, - "min": 1 - }, - "Import": { - "type": "structure", - "members": { - "statement": { - "shape": "ImportStatementString" - } - } - }, - "ImportStatementString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "Imports": { - "type": "list", - "member": { - "shape": "Import" - }, - "max": 10, - "min": 0 - }, - "InlineChatEvent": { - "type": "structure", - "required": ["requestId", "timestamp"], - "members": { - "requestId": { - "shape": "UUID" - }, - "timestamp": { - "shape": "Timestamp" - }, - "inputLength": { - "shape": "PrimitiveInteger" - }, - "numSelectedLines": { - "shape": "PrimitiveInteger" - }, - "numSuggestionAddChars": { - "shape": "PrimitiveInteger" - }, - "numSuggestionAddLines": { - "shape": "PrimitiveInteger" - }, - "numSuggestionDelChars": { - "shape": "PrimitiveInteger" - }, - "numSuggestionDelLines": { - "shape": "PrimitiveInteger" - }, - "codeIntent": { - "shape": "Boolean" - }, - "userDecision": { - "shape": "InlineChatUserDecision" - }, - "responseStartLatency": { - "shape": "Double" - }, - "responseEndLatency": { - "shape": "Double" - }, - "programmingLanguage": { - "shape": "ProgrammingLanguage" - } - } - }, - "InlineChatUserDecision": { - "type": "string", - "enum": ["ACCEPT", "REJECT", "DISMISS"] - }, - "Integer": { - "type": "integer", - "box": true - }, - "Intent": { - "type": "string", - "enum": ["DEV", "DOC"] - }, - "IntentContext": { - "type": "structure", - "members": { - "documentation": { - "shape": "DocumentationIntentContext" - } - }, - "union": true - }, - "InternalServerException": { - "type": "structure", - "required": ["message"], - "members": { - "message": { - "shape": "String" - } - }, - "exception": true, - "fault": true, - "retryable": { - "throttling": false - } - }, - "IssuerUrl": { - "type": "string", - "max": 255, - "min": 1 - }, - "LineRangeList": { - "type": "list", - "member": { - "shape": "Range" - } - }, - "ListAvailableCustomizationsRequest": { - "type": "structure", - "members": { - "maxResults": { - "shape": "ListAvailableCustomizationsRequestMaxResultsInteger" - }, - "nextToken": { - "shape": "Base64EncodedPaginationToken" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "ListAvailableCustomizationsRequestMaxResultsInteger": { - "type": "integer", - "box": true, - "max": 100, - "min": 1 - }, - "ListAvailableCustomizationsResponse": { - "type": "structure", - "required": ["customizations"], - "members": { - "customizations": { - "shape": "Customizations" - }, - "nextToken": { - "shape": "Base64EncodedPaginationToken" - } - } - }, - "ListAvailableProfilesRequest": { - "type": "structure", - "members": { - "maxResults": { - "shape": "ListAvailableProfilesRequestMaxResultsInteger" - }, - "nextToken": { - "shape": "Base64EncodedPaginationToken" - } - } - }, - "ListAvailableProfilesRequestMaxResultsInteger": { - "type": "integer", - "box": true, - "max": 10, - "min": 1 - }, - "ListAvailableProfilesResponse": { - "type": "structure", - "required": ["profiles"], - "members": { - "profiles": { - "shape": "ProfileList" - }, - "nextToken": { - "shape": "Base64EncodedPaginationToken" - } - } - }, - "ListCodeAnalysisFindingsRequest": { - "type": "structure", - "required": ["jobId", "codeAnalysisFindingsSchema"], - "members": { - "jobId": { - "shape": "ListCodeAnalysisFindingsRequestJobIdString" - }, - "nextToken": { - "shape": "PaginationToken" - }, - "codeAnalysisFindingsSchema": { - "shape": "CodeAnalysisFindingsSchema" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "ListCodeAnalysisFindingsRequestJobIdString": { - "type": "string", - "max": 256, - "min": 1 - }, - "ListCodeAnalysisFindingsResponse": { - "type": "structure", - "required": ["codeAnalysisFindings"], - "members": { - "nextToken": { - "shape": "PaginationToken" - }, - "codeAnalysisFindings": { - "shape": "SensitiveString" - } - } - }, - "ListEventsRequest": { - "type": "structure", - "required": ["conversationId"], - "members": { - "conversationId": { - "shape": "UUID" - }, - "maxResults": { - "shape": "ListEventsRequestMaxResultsInteger" - }, - "nextToken": { - "shape": "NextToken" - } - } - }, - "ListEventsRequestMaxResultsInteger": { - "type": "integer", - "box": true, - "max": 50, - "min": 1 - }, - "ListEventsResponse": { - "type": "structure", - "required": ["conversationId", "events"], - "members": { - "conversationId": { - "shape": "UUID" - }, - "events": { - "shape": "EventList" - }, - "nextToken": { - "shape": "NextToken" - } - } - }, - "ListFeatureEvaluationsRequest": { - "type": "structure", - "required": ["userContext"], - "members": { - "userContext": { - "shape": "UserContext" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "ListFeatureEvaluationsResponse": { - "type": "structure", - "required": ["featureEvaluations"], - "members": { - "featureEvaluations": { - "shape": "FeatureEvaluationsList" - } - } - }, - "ListUserMemoryEntriesInput": { - "type": "structure", - "members": { - "maxResults": { - "shape": "ListUserMemoryEntriesInputMaxResultsInteger" - }, - "profileArn": { - "shape": "ListUserMemoryEntriesInputProfileArnString" - }, - "nextToken": { - "shape": "ListUserMemoryEntriesInputNextTokenString" - } - } - }, - "ListUserMemoryEntriesInputMaxResultsInteger": { - "type": "integer", - "box": true, - "max": 100, - "min": 1 - }, - "ListUserMemoryEntriesInputNextTokenString": { - "type": "string", - "min": 1 - }, - "ListUserMemoryEntriesInputProfileArnString": { - "type": "string", - "min": 1, - "pattern": "arn:aws:codewhisperer:[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" - }, - "ListUserMemoryEntriesOutput": { - "type": "structure", - "required": ["memoryEntries"], - "members": { - "memoryEntries": { - "shape": "MemoryEntryList" - }, - "nextToken": { - "shape": "ListUserMemoryEntriesOutputNextTokenString" - } - } - }, - "ListUserMemoryEntriesOutputNextTokenString": { - "type": "string", - "min": 1 - }, - "ListWorkspaceMetadataRequest": { - "type": "structure", - "members": { - "workspaceRoot": { - "shape": "ListWorkspaceMetadataRequestWorkspaceRootString" - }, - "nextToken": { - "shape": "String" - }, - "maxResults": { - "shape": "Integer" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "ListWorkspaceMetadataRequestWorkspaceRootString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "ListWorkspaceMetadataResponse": { - "type": "structure", - "required": ["workspaces"], - "members": { - "workspaces": { - "shape": "WorkspaceList" - }, - "nextToken": { - "shape": "String" - } - } - }, - "Long": { - "type": "long", - "box": true - }, - "MemoryEntry": { - "type": "structure", - "required": ["id", "memoryEntryString", "metadata"], - "members": { - "id": { - "shape": "MemoryEntryIdString" - }, - "memoryEntryString": { - "shape": "MemoryEntryMemoryEntryStringString" - }, - "metadata": { - "shape": "MemoryEntryMetadata" - } - } - }, - "MemoryEntryIdString": { - "type": "string", - "max": 36, - "min": 36, - "pattern": "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" - }, - "MemoryEntryList": { - "type": "list", - "member": { - "shape": "MemoryEntry" - } - }, - "MemoryEntryMemoryEntryStringString": { - "type": "string", - "max": 500, - "min": 1, - "sensitive": true - }, - "MemoryEntryMetadata": { - "type": "structure", - "required": ["origin", "createdAt", "updatedAt"], - "members": { - "origin": { - "shape": "Origin" - }, - "attributes": { - "shape": "AttributesMap" - }, - "createdAt": { - "shape": "Timestamp" - }, - "updatedAt": { - "shape": "Timestamp" - } - } - }, - "MessageId": { - "type": "string", - "max": 128, - "min": 0 - }, - "MetricData": { - "type": "structure", - "required": ["metricName", "metricValue", "timestamp", "product"], - "members": { - "metricName": { - "shape": "MetricDataMetricNameString" - }, - "metricValue": { - "shape": "Double" - }, - "timestamp": { - "shape": "Timestamp" - }, - "product": { - "shape": "MetricDataProductString" - }, - "dimensions": { - "shape": "DimensionList" - } - } - }, - "MetricDataMetricNameString": { - "type": "string", - "max": 1024, - "min": 1, - "pattern": "[-a-zA-Z0-9._]*" - }, - "MetricDataProductString": { - "type": "string", - "max": 128, - "min": 1, - "pattern": "[-a-zA-Z0-9._]*" - }, - "NextToken": { - "type": "string", - "max": 1000, - "min": 0 - }, - "Notifications": { - "type": "list", - "member": { - "shape": "NotificationsFeature" - }, - "max": 10, - "min": 0 - }, - "NotificationsFeature": { - "type": "structure", - "required": ["feature", "toggle"], - "members": { - "feature": { - "shape": "FeatureName" - }, - "toggle": { - "shape": "OptInFeatureToggle" - } - } - }, - "OperatingSystem": { - "type": "string", - "enum": ["MAC", "WINDOWS", "LINUX"], - "max": 64, - "min": 1 - }, - "OptInFeatureToggle": { - "type": "string", - "enum": ["ON", "OFF"] - }, - "OptInFeatures": { - "type": "structure", - "members": { - "promptLogging": { - "shape": "PromptLogging" - }, - "byUserAnalytics": { - "shape": "ByUserAnalytics" - }, - "dashboardAnalytics": { - "shape": "DashboardAnalytics" - }, - "notifications": { - "shape": "Notifications" - }, - "workspaceContext": { - "shape": "WorkspaceContext" - } - } - }, - "OptOutPreference": { - "type": "string", - "enum": ["OPTIN", "OPTOUT"] - }, - "Origin": { - "type": "string", - "enum": [ - "CHATBOT", - "CONSOLE", - "DOCUMENTATION", - "MARKETING", - "MOBILE", - "SERVICE_INTERNAL", - "UNIFIED_SEARCH", - "UNKNOWN", - "MD", - "IDE", - "SAGE_MAKER", - "CLI", - "AI_EDITOR", - "OPENSEARCH_DASHBOARD", - "GITLAB" - ] - }, - "PackageInfo": { - "type": "structure", - "members": { - "executionCommand": { - "shape": "SensitiveString" - }, - "buildCommand": { - "shape": "SensitiveString" - }, - "buildOrder": { - "shape": "PackageInfoBuildOrderInteger" - }, - "testFramework": { - "shape": "String" - }, - "packageSummary": { - "shape": "PackageInfoPackageSummaryString" - }, - "packagePlan": { - "shape": "PackageInfoPackagePlanString" - }, - "targetFileInfoList": { - "shape": "TargetFileInfoList" - } - } - }, - "PackageInfoBuildOrderInteger": { - "type": "integer", - "box": true, - "min": 0 - }, - "PackageInfoList": { - "type": "list", - "member": { - "shape": "PackageInfo" - } - }, - "PackageInfoPackagePlanString": { - "type": "string", - "max": 30720, - "min": 0, - "sensitive": true - }, - "PackageInfoPackageSummaryString": { - "type": "string", - "max": 30720, - "min": 0, - "sensitive": true - }, - "PaginationToken": { - "type": "string", - "max": 2048, - "min": 1, - "pattern": "\\S+" - }, - "Position": { - "type": "structure", - "required": ["line", "character"], - "members": { - "line": { - "shape": "Integer" - }, - "character": { - "shape": "Integer" - } - } - }, - "PreSignedUrl": { - "type": "string", - "max": 2048, - "min": 1, - "sensitive": true - }, - "PrimitiveInteger": { - "type": "integer" - }, - "Profile": { - "type": "structure", - "required": ["arn", "profileName"], - "members": { - "arn": { - "shape": "ProfileArn" - }, - "identityDetails": { - "shape": "IdentityDetails" - }, - "profileName": { - "shape": "ProfileName" - }, - "description": { - "shape": "ProfileDescription" - }, - "referenceTrackerConfiguration": { - "shape": "ReferenceTrackerConfiguration" - }, - "kmsKeyArn": { - "shape": "ResourceArn" - }, - "activeFunctionalities": { - "shape": "ActiveFunctionalityList" - }, - "status": { - "shape": "ProfileStatus" - }, - "errorDetails": { - "shape": "ErrorDetails" - }, - "resourcePolicy": { - "shape": "ResourcePolicy" - }, - "profileType": { - "shape": "ProfileType" - }, - "optInFeatures": { - "shape": "OptInFeatures" - }, - "permissionUpdateRequired": { - "shape": "Boolean" - }, - "applicationProperties": { - "shape": "ApplicationPropertiesList" - } - } - }, - "ProfileArn": { - "type": "string", - "max": 950, - "min": 0, - "pattern": "arn:aws:codewhisperer:[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" - }, - "ProfileDescription": { - "type": "string", - "max": 256, - "min": 1, - "pattern": "[\\sa-zA-Z0-9_-]*" - }, - "ProfileList": { - "type": "list", - "member": { - "shape": "Profile" - } - }, - "ProfileName": { - "type": "string", - "max": 100, - "min": 1, - "pattern": "[a-zA-Z][a-zA-Z0-9_-]*" - }, - "ProfileStatus": { - "type": "string", - "enum": ["ACTIVE", "CREATING", "CREATE_FAILED", "UPDATING", "UPDATE_FAILED", "DELETING", "DELETE_FAILED"] - }, - "ProfileType": { - "type": "string", - "enum": ["Q_DEVELOPER", "CODEWHISPERER"] - }, - "ProgrammingLanguage": { - "type": "structure", - "required": ["languageName"], - "members": { - "languageName": { - "shape": "ProgrammingLanguageLanguageNameString" - } - } - }, - "ProgrammingLanguageLanguageNameString": { - "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)" - }, - "ProgressUpdates": { - "type": "list", - "member": { - "shape": "TransformationProgressUpdate" - } - }, - "PromptLogging": { - "type": "structure", - "required": ["s3Uri", "toggle"], - "members": { - "s3Uri": { - "shape": "S3Uri" - }, - "toggle": { - "shape": "OptInFeatureToggle" - } - } - }, - "Range": { - "type": "structure", - "required": ["start", "end"], - "members": { - "start": { - "shape": "Position" - }, - "end": { - "shape": "Position" - } - } - }, - "RecommendationsWithReferencesPreference": { - "type": "string", - "enum": ["BLOCK", "ALLOW"] - }, - "Reference": { - "type": "structure", - "members": { - "licenseName": { - "shape": "ReferenceLicenseNameString" - }, - "repository": { - "shape": "ReferenceRepositoryString" - }, - "url": { - "shape": "ReferenceUrlString" - }, - "recommendationContentSpan": { - "shape": "Span" - } - } - }, - "ReferenceLicenseNameString": { - "type": "string", - "max": 1024, - "min": 1 - }, - "ReferenceRepositoryString": { - "type": "string", - "max": 1024, - "min": 1 - }, - "ReferenceTrackerConfiguration": { - "type": "structure", - "required": ["recommendationsWithReferences"], - "members": { - "recommendationsWithReferences": { - "shape": "RecommendationsWithReferencesPreference" - } - } - }, - "ReferenceUrlString": { - "type": "string", - "max": 1024, - "min": 1 - }, - "References": { - "type": "list", - "member": { - "shape": "Reference" - }, - "max": 10, - "min": 0 - }, - "RelevantDocumentList": { - "type": "list", - "member": { - "shape": "RelevantTextDocument" - }, - "max": 30, - "min": 0 - }, - "RelevantTextDocument": { - "type": "structure", - "required": ["relativeFilePath"], - "members": { - "relativeFilePath": { - "shape": "RelevantTextDocumentRelativeFilePathString" - }, - "programmingLanguage": { - "shape": "ProgrammingLanguage" - }, - "text": { - "shape": "RelevantTextDocumentTextString" - }, - "documentSymbols": { - "shape": "DocumentSymbols" - } - } - }, - "RelevantTextDocumentRelativeFilePathString": { - "type": "string", - "max": 4096, - "min": 1, - "sensitive": true - }, - "RelevantTextDocumentTextString": { - "type": "string", - "max": 40960, - "min": 0, - "sensitive": true - }, - "RequestHeaderKey": { - "type": "string", - "max": 64, - "min": 1 - }, - "RequestHeaderValue": { - "type": "string", - "max": 256, - "min": 1 - }, - "RequestHeaders": { - "type": "map", - "key": { - "shape": "RequestHeaderKey" - }, - "value": { - "shape": "RequestHeaderValue" - }, - "max": 16, - "min": 1, - "sensitive": true - }, - "ResourceArn": { - "type": "string", - "max": 1224, - "min": 0, - "pattern": "arn:([-.a-z0-9]{1,63}:){2}([-.a-z0-9]{0,63}:){2}([a-zA-Z0-9-_:/]){1,1023}" - }, - "ResourceNotFoundException": { - "type": "structure", - "required": ["message"], - "members": { - "message": { - "shape": "String" - } - }, - "exception": true - }, - "ResourcePolicy": { - "type": "structure", - "required": ["effect"], - "members": { - "effect": { - "shape": "ResourcePolicyEffect" - } - } - }, - "ResourcePolicyEffect": { - "type": "string", - "enum": ["ALLOW", "DENY"] - }, - "ResumeTransformationRequest": { - "type": "structure", - "required": ["transformationJobId"], - "members": { - "transformationJobId": { - "shape": "TransformationJobId" - }, - "userActionStatus": { - "shape": "TransformationUserActionStatus" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "ResumeTransformationResponse": { - "type": "structure", - "required": ["transformationStatus"], - "members": { - "transformationStatus": { - "shape": "TransformationStatus" - } - } - }, - "RuntimeDiagnostic": { - "type": "structure", - "required": ["source", "severity", "message"], - "members": { - "source": { - "shape": "RuntimeDiagnosticSourceString" - }, - "severity": { - "shape": "DiagnosticSeverity" - }, - "message": { - "shape": "RuntimeDiagnosticMessageString" - } - } - }, - "RuntimeDiagnosticMessageString": { - "type": "string", - "max": 1024, - "min": 0, - "sensitive": true - }, - "RuntimeDiagnosticSourceString": { - "type": "string", - "max": 1024, - "min": 0, - "sensitive": true - }, - "S3Uri": { - "type": "string", - "max": 1024, - "min": 1, - "pattern": "s3://((?!xn--)[a-z0-9](?![^/]*[.]{2})[a-z0-9-.]{1,61}[a-z0-9](?): Promise { - const bearerToken = await AuthUtil.instance.getBearerToken() - const cwsprConfig = getCodewhispererConfig() - return (await globals.sdkClientBuilder.createAwsService( - Service, - { - apiConfig: apiConfig, - region: cwsprConfig.region, - endpoint: cwsprConfig.endpoint, - token: new Token({ token: bearerToken }), - httpOptions: { - connectTimeout: 10000, // 10 seconds, 3 times P99 API latency - }, - ...options, - } as ServiceOptions, - undefined - )) as FeatureDevProxyClient -} - -export class FeatureDevClient implements FeatureClient { - public async getClient(options?: Partial) { - // Should not be stored for the whole session. - // Client has to be reinitialized for each request so we always have a fresh bearerToken - return await createFeatureDevProxyClient(options) - } - - public async createConversation() { - try { - const client = await this.getClient(writeAPIRetryOptions) - getLogger().debug(`Executing createTaskAssistConversation with {}`) - const { conversationId, $response } = await client - .createTaskAssistConversation({ - profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, - }) - .promise() - getLogger().debug(`${featureName}: Created conversation: %O`, { - conversationId, - requestId: $response.requestId, - }) - return conversationId - } catch (e) { - if (isAwsError(e)) { - getLogger().error( - `${featureName}: failed to start conversation: ${e.message} RequestId: ${e.requestId}` - ) - // BE service will throw ServiceQuota if conversation limit is reached. API Front-end will throw Throttling with this message if conversation limit is reached - if ( - e.code === 'ServiceQuotaExceededException' || - (e.code === 'ThrottlingException' && e.message.includes('reached for this month.')) - ) { - throw new MonthlyConversationLimitError(e.message) - } - throw ApiError.of(e.message, 'CreateConversation', e.code, e.statusCode ?? 500) - } - - throw new UnknownApiError(e instanceof Error ? e.message : 'Unknown error', 'CreateConversation') - } - } - - public async createUploadUrl( - conversationId: string, - contentChecksumSha256: string, - contentLength: number, - uploadId: string - ) { - try { - const client = await this.getClient(writeAPIRetryOptions) - const params: CreateUploadUrlRequest = { - uploadContext: { - taskAssistPlanningUploadContext: { - conversationId, - }, - }, - uploadId, - contentChecksum: contentChecksumSha256, - contentChecksumType: 'SHA_256', - artifactType: 'SourceCode', - uploadIntent: 'TASK_ASSIST_PLANNING', - contentLength, - profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, - } - getLogger().debug(`Executing createUploadUrl with %O`, omit(params, 'contentChecksum')) - const response = await client.createUploadUrl(params).promise() - getLogger().debug(`${featureName}: Created upload url: %O`, { - uploadId: uploadId, - requestId: response.$response.requestId, - }) - return response - } catch (e) { - if (isAwsError(e)) { - getLogger().error( - `${featureName}: failed to generate presigned url: ${e.message} RequestId: ${e.requestId}` - ) - if (e.code === 'ValidationException' && e.message.includes('Invalid contentLength')) { - throw new ContentLengthError() - } - throw ApiError.of(e.message, 'CreateUploadUrl', e.code, e.statusCode ?? 500) - } - - throw new UnknownApiError(e instanceof Error ? e.message : 'Unknown error', 'CreateUploadUrl') - } - } - - public async startCodeGeneration( - conversationId: string, - uploadId: string, - message: string, - intent: FeatureDevProxyClient.Intent, - codeGenerationId: string, - currentCodeGenerationId?: string, - intentContext?: FeatureDevProxyClient.IntentContext - ) { - try { - const client = await this.getClient(writeAPIRetryOptions) - const params: StartTaskAssistCodeGenerationRequest = { - codeGenerationId, - conversationState: { - conversationId, - currentMessage: { - userInputMessage: { content: message }, - }, - chatTriggerType: 'MANUAL', - }, - workspaceState: { - uploadId, - programmingLanguage: { languageName: 'javascript' }, - }, - intent, - profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, - } - if (currentCodeGenerationId) { - params.currentCodeGenerationId = currentCodeGenerationId - } - if (intentContext) { - params.intentContext = intentContext - } - getLogger().debug(`Executing startTaskAssistCodeGeneration with %O`, params) - const response = await client.startTaskAssistCodeGeneration(params).promise() - - return response - } catch (e) { - getLogger().error( - `${featureName}: failed to start code generation: ${(e as Error).message} RequestId: ${ - (e as any).requestId - }` - ) - if (isAwsError(e)) { - // API Front-end will throw Throttling if conversation limit is reached. API Front-end monitors StartCodeGeneration for throttling - if (e.code === 'ThrottlingException' && e.message.includes(startTaskAssistLimitReachedMessage)) { - throw new MonthlyConversationLimitError(e.message) - } - // BE service will throw ServiceQuota if code generation iteration limit is reached - else if ( - e.code === 'ServiceQuotaExceededException' || - (e.code === 'ThrottlingException' && - e.message.includes('limit for number of iterations on a code generation')) - ) { - throw new CodeIterationLimitError() - } - throw ApiError.of(e.message, 'StartTaskAssistCodeGeneration', e.code, e.statusCode ?? 500) - } - - throw new UnknownApiError(e instanceof Error ? e.message : 'Unknown error', 'StartTaskAssistCodeGeneration') - } - } - - public async getCodeGeneration(conversationId: string, codeGenerationId: string) { - try { - const client = await this.getClient() - const params: GetTaskAssistCodeGenerationRequest = { - codeGenerationId, - conversationId, - profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, - } - getLogger().debug(`Executing getTaskAssistCodeGeneration with %O`, params) - const response = await client.getTaskAssistCodeGeneration(params).promise() - - return response - } catch (e) { - getLogger().error( - `${featureName}: failed to start get code generation results: ${(e as Error).message} RequestId: ${ - (e as any).requestId - }` - ) - - if (isAwsError(e)) { - throw ApiError.of(e.message, 'GetTaskAssistCodeGeneration', e.code, e.statusCode ?? 500) - } - - throw new UnknownApiError(e instanceof Error ? e.message : 'Unknown error', 'GetTaskAssistCodeGeneration') - } - } - - public async exportResultArchive(conversationId: string) { - const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile - try { - const streamingClient = await createCodeWhispererChatStreamingClient() - const params = { - exportId: conversationId, - exportIntent: 'TASK_ASSIST', - profileArn: profile?.arn, - } satisfies ExportResultArchiveCommandInput - getLogger().debug(`Executing exportResultArchive with %O`, params) - const archiveResponse = await streamingClient.exportResultArchive(params) - const buffer: number[] = [] - if (archiveResponse.body === undefined) { - throw new ApiServiceError( - 'Empty response from CodeWhisperer Streaming service.', - 'ExportResultArchive', - 'EmptyResponse', - 500 - ) - } - for await (const chunk of archiveResponse.body) { - if (chunk.internalServerException !== undefined) { - throw chunk.internalServerException - } - buffer.push(...(chunk.binaryPayloadEvent?.bytes ?? [])) - } - - const { - code_generation_result: { - new_file_contents: newFiles = {}, - deleted_files: deletedFiles = [], - references = [], - }, - } = JSON.parse(new TextDecoder().decode(Buffer.from(buffer))) as { - // eslint-disable-next-line @typescript-eslint/naming-convention - code_generation_result: { - // eslint-disable-next-line @typescript-eslint/naming-convention - new_file_contents?: Record - // eslint-disable-next-line @typescript-eslint/naming-convention - deleted_files?: string[] - references?: CodeReference[] - } - } - UserWrittenCodeTracker.instance.onQFeatureInvoked() - - const newFileContents: { zipFilePath: string; fileContent: string }[] = [] - for (const [filePath, fileContent] of Object.entries(newFiles)) { - newFileContents.push({ zipFilePath: filePath, fileContent }) - } - - return { newFileContents, deletedFiles, references } - } catch (e) { - getLogger().error( - `${featureName}: failed to export archive result: ${(e as Error).message} RequestId: ${ - (e as any).requestId - }` - ) - - if (isAwsError(e)) { - throw ApiError.of(e.message, 'ExportResultArchive', e.code, e.statusCode ?? 500) - } - - throw new FeatureDevServiceError(e instanceof Error ? e.message : 'Unknown error', 'ExportResultArchive') - } - } - - /** - * This event is specific to ABTesting purposes. - * - * No need to fail currently if the event fails in the request. In addition, currently there is no need for a return value. - * - * @param conversationId - */ - public async sendFeatureDevTelemetryEvent(conversationId: string) { - await this.sendFeatureDevEvent('featureDevEvent', { - conversationId, - }) - } - - public async sendFeatureDevCodeGenerationEvent(event: FeatureDevCodeGenerationEvent) { - getLogger().debug( - `featureDevCodeGenerationEvent: conversationId: ${event.conversationId} charactersOfCodeGenerated: ${event.charactersOfCodeGenerated} linesOfCodeGenerated: ${event.linesOfCodeGenerated}` - ) - await this.sendFeatureDevEvent('featureDevCodeGenerationEvent', event) - } - - public async sendFeatureDevCodeAcceptanceEvent(event: FeatureDevCodeAcceptanceEvent) { - getLogger().debug( - `featureDevCodeAcceptanceEvent: conversationId: ${event.conversationId} charactersOfCodeAccepted: ${event.charactersOfCodeAccepted} linesOfCodeAccepted: ${event.linesOfCodeAccepted}` - ) - await this.sendFeatureDevEvent('featureDevCodeAcceptanceEvent', event) - } - - public async sendMetricData(event: MetricData) { - getLogger().debug(`featureDevCodeGenerationMetricData: dimensions: ${event.dimensions}`) - await this.sendFeatureDevEvent('metricData', event) - } - - public async sendFeatureDevEvent( - eventName: T, - event: NonNullable - ) { - try { - const client = await this.getClient() - const params: FeatureDevProxyClient.SendTelemetryEventRequest = { - telemetryEvent: { - [eventName]: event, - }, - optOutPreference: getOptOutPreference(), - userContext: { - ideCategory: 'VSCODE', - operatingSystem: getOperatingSystem(), - product: 'FeatureDev', // Should be the same as in JetBrains - clientId: getClientId(globals.globalState), - ideVersion: extensionVersion, - }, - profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, - } - const response = await client.sendTelemetryEvent(params).promise() - getLogger().debug( - `${featureName}: successfully sent ${eventName} telemetryEvent:${'conversationId' in event ? ' ConversationId: ' + event.conversationId : ''} RequestId: ${response.$response.requestId}` - ) - } catch (e) { - getLogger().error( - `${featureName}: failed to send ${eventName} telemetry: ${(e as Error).name}: ${ - (e as Error).message - } RequestId: ${(e as any).requestId}` - ) - } - } -} diff --git a/packages/core/src/amazonqFeatureDev/constants.ts b/packages/core/src/amazonqFeatureDev/constants.ts deleted file mode 100644 index 78cae972cc3..00000000000 --- a/packages/core/src/amazonqFeatureDev/constants.ts +++ /dev/null @@ -1,33 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { CodeReference } from '../amazonq/webview/ui/connector' -import { LicenseUtil } from '../codewhisperer/util/licenseUtil' - -// The Scheme name of the virtual documents. -export const featureDevScheme = 'aws-featureDev' - -// For uniquely identifiying which chat messages should be routed to FeatureDev -export const featureDevChat = 'featureDevChat' - -export const featureName = 'Amazon Q Developer Agent for software development' - -export const generateDevFilePrompt = - "generate a devfile in my repository. Note that you should only use devfile version 2.0.0 and the only supported commands are install, build and test (are all optional). so you may have to bundle some commands together using '&&'. also you can use ”public.ecr.aws/aws-mde/universal-image:latest” as universal image if you aren’t sure which image to use. here is an example for a node repository (but don't assume it's always a node project. look at the existing repository structure before generating the devfile): schemaVersion: 2.0.0 components: - name: dev container: image: public.ecr.aws/aws-mde/universal-image:latest commands: - id: install exec: component: dev commandLine: ”npm install” - id: build exec: component: dev commandLine: ”npm run build” - id: test exec: component: dev commandLine: ”npm run test”" - -// Max allowed size for file collection -export const maxRepoSizeBytes = 200 * 1024 * 1024 - -export const startCodeGenClientErrorMessages = ['Improperly formed request', 'Resource not found'] -export const startTaskAssistLimitReachedMessage = 'StartTaskAssistCodeGeneration reached for this month.' -export const clientErrorMessages = [ - 'The folder you chose did not contain any source files in a supported language. Choose another folder and try again.', -] - -// License text that's used in the file view -export const licenseText = (reference: CodeReference) => - `${ - reference.licenseName - } license from repository ${reference.repository}` diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts deleted file mode 100644 index bdf73eada07..00000000000 --- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts +++ /dev/null @@ -1,1089 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ChatItemAction, MynahIcons } from '@aws/mynah-ui' -import * as path from 'path' -import * as vscode from 'vscode' -import { EventEmitter } from 'vscode' -import { telemetry } from '../../../shared/telemetry/telemetry' -import { createSingleFileDialog } from '../../../shared/ui/common/openDialog' -import { - CodeIterationLimitError, - ContentLengthError, - createUserFacingErrorMessage, - denyListedErrors, - FeatureDevServiceError, - getMetricResult, - MonthlyConversationLimitError, - NoChangeRequiredException, - PrepareRepoFailedError, - PromptRefusalException, - SelectedFolderNotInWorkspaceFolderError, - TabIdNotFoundError, - UploadCodeError, - UploadURLExpired, - UserMessageNotFoundError, - WorkspaceFolderNotFoundError, - ZipFileError, -} from '../../errors' -import { codeGenRetryLimit, defaultRetryLimit } from '../../limits' -import { Session } from '../../session/session' -import { featureDevScheme, featureName, generateDevFilePrompt } from '../../constants' -import { - DeletedFileInfo, - DevPhase, - MetricDataOperationName, - MetricDataResult, - type NewFileInfo, -} from '../../../amazonq/commons/types' -import { AuthUtil } from '../../../codewhisperer/util/authUtil' -import { AuthController } from '../../../amazonq/auth/controller' -import { getLogger } from '../../../shared/logger/logger' -import { submitFeedback } from '../../../feedback/vue/submitFeedback' -import { Commands, placeholder } from '../../../shared/vscode/commands2' -import { EditorContentController } from '../../../amazonq/commons/controllers/contentController' -import { openUrl } from '../../../shared/utilities/vsCodeUtils' -import { checkForDevFile, getPathsFromZipFilePath } from '../../../amazonq/util/files' -import { examples, messageWithConversationId } from '../../userFacingText' -import { getWorkspaceFoldersByPrefixes } from '../../../shared/utilities/workspaceUtils' -import { openDeletedDiff, openDiff } from '../../../amazonq/commons/diff' -import { i18n } from '../../../shared/i18n-helper' -import globals from '../../../shared/extensionGlobals' -import { CodeWhispererSettings } from '../../../codewhisperer/util/codewhispererSettings' -import { randomUUID } from '../../../shared/crypto' -import { FollowUpTypes } from '../../../amazonq/commons/types' -import { Messenger } from '../../../amazonq/commons/connector/baseMessenger' -import { BaseChatSessionStorage } from '../../../amazonq/commons/baseChatStorage' - -export interface ChatControllerEventEmitters { - readonly processHumanChatMessage: EventEmitter - readonly followUpClicked: EventEmitter - readonly openDiff: EventEmitter - readonly stopResponse: EventEmitter - readonly tabOpened: EventEmitter - readonly tabClosed: EventEmitter - readonly processChatItemVotedMessage: EventEmitter - readonly processChatItemFeedbackMessage: EventEmitter - readonly authClicked: EventEmitter - readonly processResponseBodyLinkClick: EventEmitter - readonly insertCodeAtPositionClicked: EventEmitter - readonly fileClicked: EventEmitter - readonly storeCodeResultMessageId: EventEmitter -} - -type OpenDiffMessage = { - tabID: string - messageId: string - // currently the zip file path - filePath: string - deleted: boolean - codeGenerationId: string -} - -type fileClickedMessage = { - tabID: string - messageId: string - filePath: string - actionName: string -} - -type StoreMessageIdMessage = { - tabID: string - messageId: string -} - -export class FeatureDevController { - private readonly scheme: string = featureDevScheme - private readonly messenger: Messenger - private readonly sessionStorage: BaseChatSessionStorage - private isAmazonQVisible: boolean - private authController: AuthController - private contentController: EditorContentController - - public constructor( - private readonly chatControllerMessageListeners: ChatControllerEventEmitters, - messenger: Messenger, - sessionStorage: BaseChatSessionStorage, - onDidChangeAmazonQVisibility: vscode.Event - ) { - this.messenger = messenger - this.sessionStorage = sessionStorage - this.authController = new AuthController() - this.contentController = new EditorContentController() - - /** - * defaulted to true because onDidChangeAmazonQVisibility doesn't get fire'd until after - * the view is opened - */ - this.isAmazonQVisible = true - - onDidChangeAmazonQVisibility((visible) => { - this.isAmazonQVisible = visible - }) - - this.chatControllerMessageListeners.processHumanChatMessage.event((data) => { - this.processUserChatMessage(data).catch((e) => { - getLogger().error('processUserChatMessage failed: %s', (e as Error).message) - }) - }) - this.chatControllerMessageListeners.processChatItemVotedMessage.event((data) => { - this.processChatItemVotedMessage(data.tabID, data.vote).catch((e) => { - getLogger().error('processChatItemVotedMessage failed: %s', (e as Error).message) - }) - }) - this.chatControllerMessageListeners.processChatItemFeedbackMessage.event((data) => { - this.processChatItemFeedbackMessage(data).catch((e) => { - getLogger().error('processChatItemFeedbackMessage failed: %s', (e as Error).message) - }) - }) - this.chatControllerMessageListeners.followUpClicked.event((data) => { - switch (data.followUp.type) { - case FollowUpTypes.InsertCode: - return this.insertCode(data) - case FollowUpTypes.ProvideFeedbackAndRegenerateCode: - return this.provideFeedbackAndRegenerateCode(data) - case FollowUpTypes.Retry: - return this.retryRequest(data) - case FollowUpTypes.ModifyDefaultSourceFolder: - return this.modifyDefaultSourceFolder(data) - case FollowUpTypes.DevExamples: - this.initialExamples(data) - break - case FollowUpTypes.NewTask: - this.messenger.sendAnswer({ - type: 'answer', - tabID: data?.tabID, - message: i18n('AWS.amazonq.featureDev.answer.newTaskChanges'), - }) - return this.newTask(data) - case FollowUpTypes.CloseSession: - return this.closeSession(data) - case FollowUpTypes.SendFeedback: - this.sendFeedback() - break - case FollowUpTypes.AcceptAutoBuild: - return this.processAutoBuildSetting(true, data) - case FollowUpTypes.DenyAutoBuild: - return this.processAutoBuildSetting(false, data) - case FollowUpTypes.GenerateDevFile: - this.messenger.sendAnswer({ - type: 'system-prompt', - tabID: data?.tabID, - message: i18n('AWS.amazonq.featureDev.pillText.generateDevFile'), - }) - return this.newTask(data, generateDevFilePrompt) - } - }) - this.chatControllerMessageListeners.openDiff.event((data) => { - return this.openDiff(data) - }) - this.chatControllerMessageListeners.stopResponse.event((data) => { - return this.stopResponse(data) - }) - this.chatControllerMessageListeners.tabOpened.event((data) => { - return this.tabOpened(data) - }) - this.chatControllerMessageListeners.tabClosed.event((data) => { - this.tabClosed(data) - }) - this.chatControllerMessageListeners.authClicked.event((data) => { - this.authClicked(data) - }) - this.chatControllerMessageListeners.processResponseBodyLinkClick.event((data) => { - this.processLink(data) - }) - this.chatControllerMessageListeners.insertCodeAtPositionClicked.event((data) => { - this.insertCodeAtPosition(data) - }) - this.chatControllerMessageListeners.fileClicked.event(async (data) => { - return await this.fileClicked(data) - }) - this.chatControllerMessageListeners.storeCodeResultMessageId.event(async (data) => { - return await this.storeCodeResultMessageId(data) - }) - AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { - this.sessionStorage.deleteAllSessions() - }) - } - - private async processChatItemVotedMessage(tabId: string, vote: string) { - const session = await this.sessionStorage.getSession(tabId) - - if (vote === 'upvote') { - telemetry.amazonq_codeGenerationThumbsUp.emit({ - amazonqConversationId: session?.conversationId, - value: 1, - result: 'Succeeded', - credentialStartUrl: AuthUtil.instance.startUrl, - }) - } else if (vote === 'downvote') { - telemetry.amazonq_codeGenerationThumbsDown.emit({ - amazonqConversationId: session?.conversationId, - value: 1, - result: 'Succeeded', - credentialStartUrl: AuthUtil.instance.startUrl, - }) - } - } - - private async processChatItemFeedbackMessage(message: any) { - const session = await this.sessionStorage.getSession(message.tabId) - - await globals.telemetry.postFeedback({ - comment: `${JSON.stringify({ - type: 'featuredev-chat-answer-feedback', - conversationId: session?.conversationId ?? '', - messageId: message?.messageId, - reason: message?.selectedOption, - userComment: message?.comment, - })}`, - sentiment: 'Negative', // The chat UI reports only negative feedback currently. - }) - } - - private processErrorChatMessage = (err: any, message: any, session: Session | undefined) => { - const errorMessage = createUserFacingErrorMessage( - `${featureName} request failed: ${err.cause?.message ?? err.message}` - ) - - let defaultMessage - const isDenyListedError = denyListedErrors.some((denyListedError) => err.message.includes(denyListedError)) - - switch (err.constructor.name) { - case ContentLengthError.name: - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: err.message + messageWithConversationId(session?.conversationIdUnsafe), - canBeVoted: true, - }) - this.messenger.sendAnswer({ - type: 'system-prompt', - tabID: message.tabID, - followUps: [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.modifyDefaultSourceFolder'), - type: 'ModifyDefaultSourceFolder', - status: 'info', - }, - ], - }) - break - case MonthlyConversationLimitError.name: - this.messenger.sendMonthlyLimitError(message.tabID) - break - case FeatureDevServiceError.name: - case UploadCodeError.name: - case UserMessageNotFoundError.name: - case TabIdNotFoundError.name: - case PrepareRepoFailedError.name: - this.messenger.sendErrorMessage( - errorMessage, - message.tabID, - this.retriesRemaining(session), - session?.conversationIdUnsafe - ) - break - case PromptRefusalException.name: - case ZipFileError.name: - this.messenger.sendErrorMessage(errorMessage, message.tabID, 0, session?.conversationIdUnsafe, true) - break - case NoChangeRequiredException.name: - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: err.message, - canBeVoted: true, - }) - // Allow users to re-work the task description. - return this.newTask(message) - case CodeIterationLimitError.name: - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: err.message + messageWithConversationId(session?.conversationIdUnsafe), - canBeVoted: true, - }) - this.messenger.sendAnswer({ - type: 'system-prompt', - tabID: message.tabID, - followUps: [ - { - pillText: - session?.getInsertCodePillText([ - ...(session?.state.filePaths ?? []), - ...(session?.state.deletedFiles ?? []), - ]) ?? i18n('AWS.amazonq.featureDev.pillText.acceptAllChanges'), - type: FollowUpTypes.InsertCode, - icon: 'ok' as MynahIcons, - status: 'success', - }, - ], - }) - break - case UploadURLExpired.name: - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: err.message, - canBeVoted: true, - }) - break - default: - if (isDenyListedError || this.retriesRemaining(session) === 0) { - defaultMessage = i18n('AWS.amazonq.featureDev.error.codeGen.denyListedError') - } else { - defaultMessage = i18n('AWS.amazonq.featureDev.error.codeGen.default') - } - - this.messenger.sendErrorMessage( - defaultMessage ? defaultMessage : errorMessage, - message.tabID, - this.retriesRemaining(session), - session?.conversationIdUnsafe, - !!defaultMessage - ) - - break - } - } - - /** - * - * This function dispose cancellation token to free resources and provide a new token. - * Since user can abort a call in the same session, when the processing ends, we need provide a new one - * to start with the new prompt and allow the ability to stop again. - * - * @param session - */ - - private disposeToken(session: Session | undefined) { - if (session?.state?.tokenSource?.token.isCancellationRequested) { - session?.state.tokenSource?.dispose() - if (session?.state?.tokenSource) { - session.state.tokenSource = new vscode.CancellationTokenSource() - } - getLogger().debug('Request cancelled, skipping further processing') - } - } - - // TODO add type - private async processUserChatMessage(message: any) { - if (message.message === undefined) { - this.messenger.sendErrorMessage('chatMessage should be set', message.tabID, 0, undefined) - return - } - - /** - * Don't attempt to process any chat messages when a workspace folder is not set. - * When the tab is first opened we will throw an error and lock the chat if the workspace - * folder is not found - */ - const workspaceFolders = vscode.workspace.workspaceFolders - if (workspaceFolders === undefined || workspaceFolders.length === 0) { - return - } - - let session - try { - getLogger().debug(`${featureName}: Processing message: ${message.message}`) - - session = await this.sessionStorage.getSession(message.tabID) - // set latestMessage in session as retry would lose context if function returns early - session.latestMessage = message.message - - await session.disableFileList() - const authState = await AuthUtil.instance.getChatAuthState() - if (authState.amazonQ !== 'connected') { - await this.messenger.sendAuthNeededExceptionMessage(authState, message.tabID) - session.isAuthenticating = true - return - } - - const root = session.getWorkspaceRoot() - const autoBuildProjectSetting = CodeWhispererSettings.instance.getAutoBuildSetting() - const hasDevfile = await checkForDevFile(root) - const isPromptedForAutoBuildFeature = Object.keys(autoBuildProjectSetting).includes(root) - - if (hasDevfile && !isPromptedForAutoBuildFeature) { - await this.promptAllowQCommandsConsent(message.tabID) - return - } - - await session.preloader() - - if (session.state.phase === DevPhase.CODEGEN) { - await this.onCodeGeneration(session, message.message, message.tabID) - } - } catch (err: any) { - this.disposeToken(session) - await this.processErrorChatMessage(err, message, session) - // Lock the chat input until they explicitly click one of the follow ups - this.messenger.sendChatInputEnabled(message.tabID, false) - } - } - - private async promptAllowQCommandsConsent(tabID: string) { - this.messenger.sendAnswer({ - tabID: tabID, - message: i18n('AWS.amazonq.featureDev.answer.devFileInRepository'), - type: 'answer', - }) - - this.messenger.sendAnswer({ - message: undefined, - type: 'system-prompt', - followUps: [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.acceptForProject'), - type: FollowUpTypes.AcceptAutoBuild, - status: 'success', - }, - { - pillText: i18n('AWS.amazonq.featureDev.pillText.declineForProject'), - type: FollowUpTypes.DenyAutoBuild, - status: 'error', - }, - ], - tabID: tabID, - }) - } - - /** - * Handle a regular incoming message when a user is in the code generation phase - */ - private async onCodeGeneration(session: Session, message: string, tabID: string) { - // lock the UI/show loading bubbles - this.messenger.sendAsyncEventProgress( - tabID, - true, - session.retries === codeGenRetryLimit - ? i18n('AWS.amazonq.featureDev.pillText.awaitMessage') - : i18n('AWS.amazonq.featureDev.pillText.awaitMessageRetry') - ) - - try { - this.messenger.sendAnswer({ - message: i18n('AWS.amazonq.featureDev.pillText.requestingChanges'), - type: 'answer-stream', - tabID, - canBeVoted: true, - }) - this.messenger.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.featureDev.pillText.generatingCode')) - await session.sendMetricDataTelemetry(MetricDataOperationName.StartCodeGeneration, MetricDataResult.Success) - await session.send(message) - const filePaths = session.state.filePaths ?? [] - const deletedFiles = session.state.deletedFiles ?? [] - // Only add the follow up accept/deny buttons when the tab hasn't been closed/request hasn't been cancelled - if (session?.state?.tokenSource?.token.isCancellationRequested) { - return - } - - if (filePaths.length === 0 && deletedFiles.length === 0) { - this.messenger.sendAnswer({ - message: i18n('AWS.amazonq.featureDev.pillText.unableGenerateChanges'), - type: 'answer', - tabID: tabID, - canBeVoted: true, - }) - this.messenger.sendAnswer({ - type: 'system-prompt', - tabID: tabID, - followUps: - this.retriesRemaining(session) > 0 - ? [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.retry'), - type: FollowUpTypes.Retry, - status: 'warning', - }, - ] - : [], - }) - // Lock the chat input until they explicitly click retry - this.messenger.sendChatInputEnabled(tabID, false) - return - } - - this.messenger.sendCodeResult( - filePaths, - deletedFiles, - session.state.references ?? [], - tabID, - session.uploadId, - session.state.codeGenerationId ?? '' - ) - - const remainingIterations = session.state.codeGenerationRemainingIterationCount - const totalIterations = session.state.codeGenerationTotalIterationCount - - if (remainingIterations !== undefined && totalIterations !== undefined) { - this.messenger.sendAnswer({ - type: 'answer' as const, - tabID: tabID, - message: (() => { - 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?' - } - })(), - }) - } - - if (session?.state.phase === DevPhase.CODEGEN) { - const messageId = randomUUID() - session.updateAcceptCodeMessageId(messageId) - session.updateAcceptCodeTelemetrySent(false) - // need to add the followUps with an extra update here, or it will double-render them - this.messenger.sendAnswer({ - message: undefined, - type: 'system-prompt', - followUps: [], - tabID: tabID, - messageId, - }) - await session.updateChatAnswer(tabID, i18n('AWS.amazonq.featureDev.pillText.acceptAllChanges')) - await session.sendLinesOfCodeGeneratedTelemetry() - } - this.messenger.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.featureDev.pillText.selectOption')) - } catch (err: any) { - getLogger().error(`${featureName}: Error during code generation: ${err}`) - await session.sendMetricDataTelemetry(MetricDataOperationName.EndCodeGeneration, getMetricResult(err)) - throw err - } finally { - // Finish processing the event - - if (session?.state?.tokenSource?.token.isCancellationRequested) { - await this.workOnNewTask( - session.tabID, - session.state.codeGenerationRemainingIterationCount, - session.state.codeGenerationTotalIterationCount, - session?.state?.tokenSource?.token.isCancellationRequested - ) - this.disposeToken(session) - } else { - this.messenger.sendAsyncEventProgress(tabID, false, undefined) - - // Lock the chat input until they explicitly click one of the follow ups - this.messenger.sendChatInputEnabled(tabID, false) - - if (!this.isAmazonQVisible) { - const open = 'Open chat' - const resp = await vscode.window.showInformationMessage( - i18n('AWS.amazonq.featureDev.answer.qGeneratedCode'), - open - ) - if (resp === open) { - await Commands.tryExecute('aws.amazonq.AmazonQChatView.focus') - // TODO add focusing on the specific tab once that's implemented - } - } - } - } - await session.sendMetricDataTelemetry(MetricDataOperationName.EndCodeGeneration, MetricDataResult.Success) - } - - private sendUpdateCodeMessage(tabID: string) { - this.messenger.sendAnswer({ - type: 'answer', - tabID, - message: i18n('AWS.amazonq.featureDev.answer.updateCode'), - canBeVoted: true, - }) - } - - private async workOnNewTask( - tabID: string, - remainingIterations: number = 0, - totalIterations?: number, - isStoppedGeneration: boolean = false - ) { - const hasDevFile = await checkForDevFile((await this.sessionStorage.getSession(tabID)).getWorkspaceRoot()) - - if (isStoppedGeneration) { - this.messenger.sendAnswer({ - message: ((remainingIterations) => { - if (totalIterations !== undefined) { - if (remainingIterations <= 0) { - return "I stopped generating your code. You don't have more iterations left, however, you can start a new session." - } else if (remainingIterations <= 2) { - return `I stopped generating your code. If you want to continue working on this task, provide another description. You have ${remainingIterations} out of ${totalIterations} code generations left.` - } - } - return 'I stopped generating your code. If you want to continue working on this task, provide another description.' - })(remainingIterations), - type: 'answer-part', - tabID, - }) - } - - if ((remainingIterations <= 0 && isStoppedGeneration) || !isStoppedGeneration) { - const followUps: Array = [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.newTask'), - type: FollowUpTypes.NewTask, - status: 'info', - }, - { - pillText: i18n('AWS.amazonq.featureDev.pillText.closeSession'), - type: FollowUpTypes.CloseSession, - status: 'info', - }, - ] - - if (!hasDevFile) { - followUps.push({ - pillText: i18n('AWS.amazonq.featureDev.pillText.generateDevFile'), - type: FollowUpTypes.GenerateDevFile, - status: 'info', - }) - - this.messenger.sendAnswer({ - type: 'answer', - tabID, - message: i18n('AWS.amazonq.featureDev.answer.devFileSuggestion'), - }) - } - - this.messenger.sendAnswer({ - type: 'system-prompt', - tabID, - followUps, - }) - this.messenger.sendChatInputEnabled(tabID, false) - this.messenger.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.featureDev.pillText.selectOption')) - return - } - - // Ensure that chat input is enabled so that they can provide additional iterations if they choose - this.messenger.sendChatInputEnabled(tabID, true) - this.messenger.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.featureDev.placeholder.additionalImprovements')) - } - - private async processAutoBuildSetting(setting: boolean, msg: any) { - const root = (await this.sessionStorage.getSession(msg.tabID)).getWorkspaceRoot() - await CodeWhispererSettings.instance.updateAutoBuildSetting(root, setting) - - this.messenger.sendAnswer({ - message: i18n('AWS.amazonq.featureDev.answer.settingUpdated'), - tabID: msg.tabID, - type: 'answer', - }) - - await this.retryRequest(msg) - } - - // TODO add type - private async insertCode(message: any) { - let session - try { - session = await this.sessionStorage.getSession(message.tabID) - - const acceptedFiles = (paths?: { rejected: boolean }[]) => (paths || []).filter((i) => !i.rejected).length - - const filesAccepted = acceptedFiles(session.state.filePaths) + acceptedFiles(session.state.deletedFiles) - - this.sendAcceptCodeTelemetry(session, filesAccepted) - - await session.insertChanges() - - if (session.acceptCodeMessageId) { - this.sendUpdateCodeMessage(message.tabID) - await this.workOnNewTask( - message.tabID, - session.state.codeGenerationRemainingIterationCount, - session.state.codeGenerationTotalIterationCount - ) - await this.clearAcceptCodeMessageId(message.tabID) - } - } catch (err: any) { - this.messenger.sendErrorMessage( - createUserFacingErrorMessage(`Failed to insert code changes: ${err.message}`), - message.tabID, - this.retriesRemaining(session), - session?.conversationIdUnsafe - ) - } - } - - private async provideFeedbackAndRegenerateCode(message: any) { - const session = await this.sessionStorage.getSession(message.tabID) - telemetry.amazonq_isProvideFeedbackForCodeGen.emit({ - amazonqConversationId: session.conversationId, - enabled: true, - result: 'Succeeded', - credentialStartUrl: AuthUtil.instance.startUrl, - }) - // Unblock the message button - this.messenger.sendAsyncEventProgress(message.tabID, false, undefined) - - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: i18n('AWS.amazonq.featureDev.answer.howCodeCanBeImproved'), - canBeVoted: true, - }) - - this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.featureDev.placeholder.feedback')) - } - - private async retryRequest(message: any) { - let session - try { - this.messenger.sendAsyncEventProgress(message.tabID, true, undefined) - - session = await this.sessionStorage.getSession(message.tabID) - - // Decrease retries before making this request, just in case this one fails as well - session.decreaseRetries() - - // Sending an empty message will re-run the last state with the previous values - await this.processUserChatMessage({ - message: session.latestMessage, - tabID: message.tabID, - }) - } catch (err: any) { - this.messenger.sendErrorMessage( - createUserFacingErrorMessage(`Failed to retry request: ${err.message}`), - message.tabID, - this.retriesRemaining(session), - session?.conversationIdUnsafe - ) - } finally { - // Finish processing the event - this.messenger.sendAsyncEventProgress(message.tabID, false, undefined) - } - } - - private async modifyDefaultSourceFolder(message: any) { - const session = await this.sessionStorage.getSession(message.tabID) - - const uri = await createSingleFileDialog({ - canSelectFolders: true, - canSelectFiles: false, - }).prompt() - - let metricData: { result: 'Succeeded' } | { result: 'Failed'; reason: string } | undefined - - if (!(uri instanceof vscode.Uri)) { - this.messenger.sendAnswer({ - tabID: message.tabID, - type: 'system-prompt', - followUps: [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.selectFiles'), - type: 'ModifyDefaultSourceFolder', - status: 'info', - }, - ], - }) - metricData = { result: 'Failed', reason: 'ClosedBeforeSelection' } - } else if (!vscode.workspace.getWorkspaceFolder(uri)) { - this.messenger.sendAnswer({ - tabID: message.tabID, - type: 'answer', - message: new SelectedFolderNotInWorkspaceFolderError().message, - canBeVoted: true, - }) - this.messenger.sendAnswer({ - tabID: message.tabID, - type: 'system-prompt', - followUps: [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.selectFiles'), - type: 'ModifyDefaultSourceFolder', - status: 'info', - }, - ], - }) - metricData = { result: 'Failed', reason: 'NotInWorkspaceFolder' } - } else { - session.updateWorkspaceRoot(uri.fsPath) - metricData = { result: 'Succeeded' } - this.messenger.sendAnswer({ - message: `Changed source root to: ${uri.fsPath}`, - type: 'answer', - tabID: message.tabID, - canBeVoted: true, - }) - this.messenger.sendAnswer({ - message: undefined, - type: 'system-prompt', - followUps: [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.retry'), - type: FollowUpTypes.Retry, - status: 'warning', - }, - ], - tabID: message.tabID, - }) - this.messenger.sendChatInputEnabled(message.tabID, true) - this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.featureDev.pillText.writeNewPrompt')) - } - - telemetry.amazonq_modifySourceFolder.emit({ - credentialStartUrl: AuthUtil.instance.startUrl, - amazonqConversationId: session.conversationId, - ...metricData, - }) - } - - private initialExamples(message: any) { - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: examples, - canBeVoted: true, - }) - } - - private async fileClicked(message: fileClickedMessage) { - // TODO: add Telemetry here - const tabId: string = message.tabID - const messageId = message.messageId - const filePathToUpdate: string = message.filePath - const action = message.actionName - - const session = await this.sessionStorage.getSession(tabId) - const filePathIndex = (session.state.filePaths ?? []).findIndex((obj) => obj.relativePath === filePathToUpdate) - const deletedFilePathIndex = (session.state.deletedFiles ?? []).findIndex( - (obj) => obj.relativePath === filePathToUpdate - ) - - if (filePathIndex !== -1 && session.state.filePaths) { - if (action === 'accept-change') { - this.sendAcceptCodeTelemetry(session, 1) - await session.insertNewFiles([session.state.filePaths[filePathIndex]]) - await session.insertCodeReferenceLogs(session.state.references ?? []) - await this.openFile(session.state.filePaths[filePathIndex], tabId) - } else { - session.state.filePaths[filePathIndex].rejected = !session.state.filePaths[filePathIndex].rejected - } - } - if (deletedFilePathIndex !== -1 && session.state.deletedFiles) { - if (action === 'accept-change') { - this.sendAcceptCodeTelemetry(session, 1) - await session.applyDeleteFiles([session.state.deletedFiles[deletedFilePathIndex]]) - await session.insertCodeReferenceLogs(session.state.references ?? []) - } else { - session.state.deletedFiles[deletedFilePathIndex].rejected = - !session.state.deletedFiles[deletedFilePathIndex].rejected - } - } - - await session.updateFilesPaths({ - tabID: tabId, - filePaths: session.state.filePaths ?? [], - deletedFiles: session.state.deletedFiles ?? [], - messageId, - }) - - if (session.acceptCodeMessageId) { - const allFilePathsAccepted = session.state.filePaths?.every( - (filePath: NewFileInfo) => !filePath.rejected && filePath.changeApplied - ) - const allDeletedFilePathsAccepted = session.state.deletedFiles?.every( - (filePath: DeletedFileInfo) => !filePath.rejected && filePath.changeApplied - ) - if (allFilePathsAccepted && allDeletedFilePathsAccepted) { - this.sendUpdateCodeMessage(tabId) - await this.workOnNewTask( - tabId, - session.state.codeGenerationRemainingIterationCount, - session.state.codeGenerationTotalIterationCount - ) - await this.clearAcceptCodeMessageId(tabId) - } - } - } - - private async storeCodeResultMessageId(message: StoreMessageIdMessage) { - const tabId: string = message.tabID - const messageId = message.messageId - const session = await this.sessionStorage.getSession(tabId) - - session.updateCodeResultMessageId(messageId) - } - - private async openDiff(message: OpenDiffMessage) { - const tabId: string = message.tabID - const codeGenerationId: string = message.messageId - const zipFilePath: string = message.filePath - const session = await this.sessionStorage.getSession(tabId) - telemetry.amazonq_isReviewedChanges.emit({ - amazonqConversationId: session.conversationId, - enabled: true, - result: 'Succeeded', - credentialStartUrl: AuthUtil.instance.startUrl, - }) - - const workspacePrefixMapping = getWorkspaceFoldersByPrefixes(session.config.workspaceFolders) - const pathInfos = getPathsFromZipFilePath(zipFilePath, workspacePrefixMapping, session.config.workspaceFolders) - - if (message.deleted) { - const name = path.basename(pathInfos.relativePath) - await openDeletedDiff(pathInfos.absolutePath, name, tabId, this.scheme) - } else { - let uploadId = session.uploadId - if (session?.state?.uploadHistory && session.state.uploadHistory[codeGenerationId]) { - uploadId = session?.state?.uploadHistory[codeGenerationId].uploadId - } - const rightPath = path.join(uploadId, zipFilePath) - await openDiff(pathInfos.absolutePath, rightPath, tabId, this.scheme) - } - } - - private async openFile(filePath: NewFileInfo, tabId: string) { - const leftPath = path.join(filePath.workspaceFolder.uri.fsPath, filePath.relativePath) - const rightPath = filePath.virtualMemoryUri.path - await openDiff(leftPath, rightPath, tabId, this.scheme) - } - - private async stopResponse(message: any) { - telemetry.ui_click.emit({ elementId: 'amazonq_stopCodeGeneration' }) - this.messenger.sendAnswer({ - message: i18n('AWS.amazonq.featureDev.pillText.stoppingCodeGeneration'), - type: 'answer-part', - tabID: message.tabID, - }) - this.messenger.sendUpdatePlaceholder( - message.tabID, - i18n('AWS.amazonq.featureDev.pillText.stoppingCodeGeneration') - ) - this.messenger.sendChatInputEnabled(message.tabID, false) - - const session = await this.sessionStorage.getSession(message.tabID) - if (session.state?.tokenSource) { - session.state?.tokenSource?.cancel() - } - } - - private async tabOpened(message: any) { - let session: Session | undefined - try { - session = await this.sessionStorage.getSession(message.tabID) - getLogger().debug(`${featureName}: Session created with id: ${session.tabID}`) - - const authState = await AuthUtil.instance.getChatAuthState() - if (authState.amazonQ !== 'connected') { - void this.messenger.sendAuthNeededExceptionMessage(authState, message.tabID) - session.isAuthenticating = true - return - } - } catch (err: any) { - if (err instanceof WorkspaceFolderNotFoundError) { - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: err.message, - }) - this.messenger.sendChatInputEnabled(message.tabID, false) - } else { - this.messenger.sendErrorMessage( - createUserFacingErrorMessage(err.message), - message.tabID, - this.retriesRemaining(session), - session?.conversationIdUnsafe - ) - } - } - } - - private authClicked(message: any) { - this.authController.handleAuth(message.authType) - - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: i18n('AWS.amazonq.featureDev.pillText.reauthenticate'), - }) - - // Explicitly ensure the user goes through the re-authenticate flow - this.messenger.sendChatInputEnabled(message.tabID, false) - } - - private tabClosed(message: any) { - this.sessionStorage.deleteSession(message.tabID) - } - - private async newTask(message: any, prefilledPrompt?: string) { - // Old session for the tab is ending, delete it so we can create a new one for the message id - const session = await this.sessionStorage.getSession(message.tabID) - await session.disableFileList() - telemetry.amazonq_endChat.emit({ - amazonqConversationId: session.conversationId, - amazonqEndOfTheConversationLatency: performance.now() - session.telemetry.sessionStartTime, - result: 'Succeeded', - }) - this.sessionStorage.deleteSession(message.tabID) - - // Re-run the opening flow, where we check auth + create a session - await this.tabOpened(message) - - if (prefilledPrompt) { - await this.processUserChatMessage({ ...message, message: prefilledPrompt }) - } else { - this.messenger.sendChatInputEnabled(message.tabID, true) - this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.featureDev.placeholder.describe')) - } - } - - private async closeSession(message: any) { - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: i18n('AWS.amazonq.featureDev.answer.sessionClosed'), - }) - this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.featureDev.placeholder.sessionClosed')) - this.messenger.sendChatInputEnabled(message.tabID, false) - - const session = await this.sessionStorage.getSession(message.tabID) - await session.disableFileList() - telemetry.amazonq_endChat.emit({ - amazonqConversationId: session.conversationId, - amazonqEndOfTheConversationLatency: performance.now() - session.telemetry.sessionStartTime, - result: 'Succeeded', - }) - } - - private sendFeedback() { - void submitFeedback(placeholder, 'Amazon Q') - } - - private processLink(message: any) { - void openUrl(vscode.Uri.parse(message.link)) - } - - private insertCodeAtPosition(message: any) { - this.contentController.insertTextAtCursorPosition(message.code, () => {}) - } - - private retriesRemaining(session: Session | undefined) { - return session?.retries ?? defaultRetryLimit - } - - private async clearAcceptCodeMessageId(tabID: string) { - const session = await this.sessionStorage.getSession(tabID) - session.updateAcceptCodeMessageId(undefined) - } - - private sendAcceptCodeTelemetry(session: Session, amazonqNumberOfFilesAccepted: number) { - // accepted code telemetry is only to be sent once per iteration of code generation - if (amazonqNumberOfFilesAccepted > 0 && !session.acceptCodeTelemetrySent) { - session.updateAcceptCodeTelemetrySent(true) - telemetry.amazonq_isAcceptedCodeChanges.emit({ - credentialStartUrl: AuthUtil.instance.startUrl, - amazonqConversationId: session.conversationId, - amazonqNumberOfFilesAccepted, - enabled: true, - result: 'Succeeded', - }) - } - } -} diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/messenger/constants.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/messenger/constants.ts deleted file mode 100644 index 086096b68a2..00000000000 --- a/packages/core/src/amazonqFeatureDev/controllers/chat/messenger/constants.ts +++ /dev/null @@ -1,6 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -export type MessengerTypes = 'answer' | 'answer-part' | 'answer-stream' | 'system-prompt' diff --git a/packages/core/src/amazonqFeatureDev/errors.ts b/packages/core/src/amazonqFeatureDev/errors.ts deleted file mode 100644 index 2eb142f765b..00000000000 --- a/packages/core/src/amazonqFeatureDev/errors.ts +++ /dev/null @@ -1,191 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { featureName, clientErrorMessages, startTaskAssistLimitReachedMessage } from './constants' -import { uploadCodeError } from './userFacingText' -import { i18n } from '../shared/i18n-helper' -import { LlmError } from '../amazonq/errors' -import { MetricDataResult } from '../amazonq/commons/types' -import { - ClientError, - ServiceError, - ContentLengthError as CommonContentLengthError, - ToolkitError, -} from '../shared/errors' - -export class ConversationIdNotFoundError extends ServiceError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.conversationIdNotFoundError'), { - code: 'ConversationIdNotFound', - }) - } -} - -export class TabIdNotFoundError extends ServiceError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.tabIdNotFoundError'), { - code: 'TabIdNotFound', - }) - } -} - -export class WorkspaceFolderNotFoundError extends ServiceError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.workspaceFolderNotFoundError'), { - code: 'WorkspaceFolderNotFound', - }) - } -} - -export class UserMessageNotFoundError extends ServiceError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.userMessageNotFoundError'), { - code: 'MessageNotFound', - }) - } -} - -export class SelectedFolderNotInWorkspaceFolderError extends ClientError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.selectedFolderNotInWorkspaceFolderError'), { - code: 'SelectedFolderNotInWorkspaceFolder', - }) - } -} - -export class PromptRefusalException extends ClientError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.promptRefusalException'), { - code: 'PromptRefusalException', - }) - } -} - -export class NoChangeRequiredException extends ClientError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.noChangeRequiredException'), { - code: 'NoChangeRequiredException', - }) - } -} - -export class FeatureDevServiceError extends ServiceError { - constructor(message: string, code: string) { - super(message, { code }) - } -} - -export class PrepareRepoFailedError extends ServiceError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.prepareRepoFailedError'), { - code: 'PrepareRepoFailed', - }) - } -} - -export class UploadCodeError extends ServiceError { - constructor(statusCode: string) { - super(uploadCodeError, { code: `UploadCode-${statusCode}` }) - } -} - -export class UploadURLExpired extends ClientError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.uploadURLExpired'), { code: 'UploadURLExpired' }) - } -} - -export class IllegalStateTransition extends ServiceError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.illegalStateTransition'), { code: 'IllegalStateTransition' }) - } -} - -export class IllegalStateError extends ServiceError { - constructor(message: string) { - super(message, { code: 'IllegalStateTransition' }) - } -} - -export class ContentLengthError extends CommonContentLengthError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.contentLengthError'), { code: ContentLengthError.name }) - } -} - -export class ZipFileError extends ServiceError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.zipFileError'), { code: ZipFileError.name }) - } -} - -export class CodeIterationLimitError extends ClientError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.codeIterationLimitError'), { code: CodeIterationLimitError.name }) - } -} - -export class MonthlyConversationLimitError extends ClientError { - constructor(message: string) { - super(message, { code: MonthlyConversationLimitError.name }) - } -} - -export class UnknownApiError extends ServiceError { - constructor(message: string, api: string) { - super(message, { code: `${api}-Unknown` }) - } -} - -export class ApiClientError extends ClientError { - constructor(message: string, api: string, errorName: string, errorCode: number) { - super(message, { code: `${api}-${errorName}-${errorCode}` }) - } -} - -export class ApiServiceError extends ServiceError { - constructor(message: string, api: string, errorName: string, errorCode: number) { - super(message, { code: `${api}-${errorName}-${errorCode}` }) - } -} - -export class ApiError { - static of(message: string, api: string, errorName: string, errorCode: number) { - if (errorCode >= 400 && errorCode < 500) { - return new ApiClientError(message, api, errorName, errorCode) - } - return new ApiServiceError(message, api, errorName, errorCode) - } -} - -export const denyListedErrors: string[] = ['Deserialization error', 'Inaccessible host'] - -export function createUserFacingErrorMessage(message: string) { - if (denyListedErrors.some((err) => message.includes(err))) { - return `${featureName} API request failed` - } - return message -} - -function isAPIClientError(error: { code?: string; message: string }): boolean { - return ( - clientErrorMessages.some((msg: string) => error.message.includes(msg)) || - error.message.includes(startTaskAssistLimitReachedMessage) - ) -} - -export function getMetricResult(error: ToolkitError): MetricDataResult { - if (error instanceof ClientError || isAPIClientError(error)) { - return MetricDataResult.Error - } - if (error instanceof ServiceError) { - return MetricDataResult.Fault - } - if (error instanceof LlmError) { - return MetricDataResult.LlmFailure - } - - return MetricDataResult.Fault -} diff --git a/packages/core/src/amazonqFeatureDev/index.ts b/packages/core/src/amazonqFeatureDev/index.ts deleted file mode 100644 index 55114de0a06..00000000000 --- a/packages/core/src/amazonqFeatureDev/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -export * from './userFacingText' -export * from './errors' -export * from './session/sessionState' -export * from './constants' -export { Session } from './session/session' -export { FeatureDevClient } from './client/featureDev' -export { FeatureDevChatSessionStorage } from './storages/chatSession' -export { TelemetryHelper } from '../amazonq/util/telemetryHelper' -export { prepareRepoData, PrepareRepoDataOptions } from '../amazonq/util/files' -export { ChatControllerEventEmitters, FeatureDevController } from './controllers/chat/controller' diff --git a/packages/core/src/amazonqFeatureDev/limits.ts b/packages/core/src/amazonqFeatureDev/limits.ts deleted file mode 100644 index 04b677aaa0f..00000000000 --- a/packages/core/src/amazonqFeatureDev/limits.ts +++ /dev/null @@ -1,14 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -// Max number of times a user can attempt to retry a codegen request if it fails -export const codeGenRetryLimit = 3 - -// The default retry limit used when the session could not be found -export const defaultRetryLimit = 0 - -// The max size a file that is uploaded can be -// 1024 KB -export const maxFileSizeBytes = 1024000 diff --git a/packages/core/src/amazonqFeatureDev/models.ts b/packages/core/src/amazonqFeatureDev/models.ts deleted file mode 100644 index ad37c01e477..00000000000 --- a/packages/core/src/amazonqFeatureDev/models.ts +++ /dev/null @@ -1,12 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -export interface IManifestFile { - pomArtifactId: string - pomFolderName: string - hilCapability: string - pomGroupId: string - sourcePomVersion: string -} diff --git a/packages/core/src/amazonqFeatureDev/session/session.ts b/packages/core/src/amazonqFeatureDev/session/session.ts deleted file mode 100644 index c1fc81a4701..00000000000 --- a/packages/core/src/amazonqFeatureDev/session/session.ts +++ /dev/null @@ -1,412 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as path from 'path' - -import { ConversationNotStartedState, FeatureDevPrepareCodeGenState } from './sessionState' -import { - type DeletedFileInfo, - type Interaction, - type NewFileInfo, - type SessionState, - type SessionStateConfig, - UpdateFilesPathsParams, -} from '../../amazonq/commons/types' -import { ContentLengthError, ConversationIdNotFoundError, IllegalStateError } from '../errors' -import { featureDevChat, featureDevScheme } from '../constants' -import fs from '../../shared/fs/fs' -import { FeatureDevClient } from '../client/featureDev' -import { codeGenRetryLimit } from '../limits' -import { telemetry } from '../../shared/telemetry/telemetry' -import { TelemetryHelper } from '../../amazonq/util/telemetryHelper' -import { ReferenceLogViewProvider } from '../../codewhisperer/service/referenceLogViewProvider' -import { AuthUtil } from '../../codewhisperer/util/authUtil' -import { getLogger } from '../../shared/logger/logger' -import { logWithConversationId } from '../userFacingText' -import { CodeReference } from '../../amazonq/webview/ui/connector' -import { MynahIcons } from '@aws/mynah-ui' -import { i18n } from '../../shared/i18n-helper' -import { computeDiff } from '../../amazonq/commons/diff' -import { UpdateAnswerMessage } from '../../amazonq/commons/connector/connectorMessages' -import { FollowUpTypes } from '../../amazonq/commons/types' -import { SessionConfig } from '../../amazonq/commons/session/sessionConfigFactory' -import { Messenger } from '../../amazonq/commons/connector/baseMessenger' -import { ContentLengthError as CommonContentLengthError } from '../../shared/errors' -import { referenceLogText } from '../../amazonq/commons/model' - -export class Session { - private _state?: SessionState | Omit - private task: string = '' - private proxyClient: FeatureDevClient - private _conversationId?: string - private codeGenRetries: number - private preloaderFinished = false - private _latestMessage: string = '' - private _telemetry: TelemetryHelper - private _codeResultMessageId: string | undefined = undefined - private _acceptCodeMessageId: string | undefined = undefined - private _acceptCodeTelemetrySent = false - private _reportedCodeChanges: Set - - // Used to keep track of whether or not the current session is currently authenticating/needs authenticating - public isAuthenticating: boolean - - constructor( - public readonly config: SessionConfig, - private messenger: Messenger, - public readonly tabID: string, - initialState: Omit = new ConversationNotStartedState(tabID), - proxyClient: FeatureDevClient = new FeatureDevClient() - ) { - this._state = initialState - this.proxyClient = proxyClient - - this.codeGenRetries = codeGenRetryLimit - - this._telemetry = new TelemetryHelper() - this.isAuthenticating = false - this._reportedCodeChanges = new Set() - } - - /** - * Preload any events that have to run before a chat message can be sent - */ - async preloader() { - if (!this.preloaderFinished) { - await this.setupConversation() - this.preloaderFinished = true - this.messenger.sendAsyncEventProgress(this.tabID, true, undefined) - await this.proxyClient.sendFeatureDevTelemetryEvent(this.conversationId) // send the event only once per conversation. - } - } - - /** - * setupConversation - * - * Starts a conversation with the backend and uploads the repo for the LLMs to be able to use it. - */ - private async setupConversation() { - await telemetry.amazonq_startConversationInvoke.run(async (span) => { - this._conversationId = await this.proxyClient.createConversation() - getLogger().info(logWithConversationId(this.conversationId)) - - span.record({ amazonqConversationId: this._conversationId, credentialStartUrl: AuthUtil.instance.startUrl }) - }) - - this._state = new FeatureDevPrepareCodeGenState( - { - ...this.getSessionStateConfig(), - conversationId: this.conversationId, - uploadId: '', - currentCodeGenerationId: undefined, - }, - [], - [], - [], - this.tabID, - 0 - ) - } - - updateWorkspaceRoot(workspaceRootFolder: string) { - this.config.workspaceRoots = [workspaceRootFolder] - this._state && this._state.updateWorkspaceRoot && this._state.updateWorkspaceRoot(workspaceRootFolder) - } - - getWorkspaceRoot(): string { - return this.config.workspaceRoots[0] - } - - private getSessionStateConfig(): Omit { - return { - workspaceRoots: this.config.workspaceRoots, - workspaceFolders: this.config.workspaceFolders, - proxyClient: this.proxyClient, - conversationId: this.conversationId, - } - } - - async send(msg: string): Promise { - // When the task/"thing to do" hasn't been set yet, we want it to be the incoming message - if (this.task === '' && msg) { - this.task = msg - } - - this._latestMessage = msg - - return this.nextInteraction(msg) - } - - private async nextInteraction(msg: string) { - try { - const resp = await this.state.interact({ - task: this.task, - msg, - fs: this.config.fs, - messenger: this.messenger, - telemetry: this.telemetry, - tokenSource: this.state.tokenSource, - uploadHistory: this.state.uploadHistory, - }) - - if (resp.nextState) { - if (!this.state?.tokenSource?.token.isCancellationRequested) { - this.state?.tokenSource?.cancel() - } - // Move to the next state - this._state = resp.nextState - } - - return resp.interaction - } catch (e) { - if (e instanceof CommonContentLengthError) { - getLogger().debug(`Content length validation failed: ${e.message}`) - throw new ContentLengthError() - } - throw e - } - } - - public async updateFilesPaths(params: UpdateFilesPathsParams) { - const { tabID, filePaths, deletedFiles, messageId, disableFileActions = false } = params - this.messenger.updateFileComponent(tabID, filePaths, deletedFiles, messageId, disableFileActions) - await this.updateChatAnswer(tabID, this.getInsertCodePillText([...filePaths, ...deletedFiles])) - } - - public async updateChatAnswer(tabID: string, insertCodePillText: string) { - if (this._acceptCodeMessageId) { - const answer = new UpdateAnswerMessage( - { - messageId: this._acceptCodeMessageId, - messageType: 'system-prompt', - followUps: [ - { - pillText: insertCodePillText, - type: FollowUpTypes.InsertCode, - icon: 'ok' as MynahIcons, - status: 'success', - }, - { - pillText: i18n('AWS.amazonq.featureDev.pillText.provideFeedback'), - type: FollowUpTypes.ProvideFeedbackAndRegenerateCode, - icon: 'refresh' as MynahIcons, - status: 'info', - }, - ], - }, - tabID, - featureDevChat - ) - this.messenger.updateChatAnswer(answer) - } - } - - public async insertChanges() { - const newFilePaths = - this.state.filePaths?.filter((filePath) => !filePath.rejected && !filePath.changeApplied) ?? [] - await this.insertNewFiles(newFilePaths) - - const deletedFiles = - this.state.deletedFiles?.filter((deletedFile) => !deletedFile.rejected && !deletedFile.changeApplied) ?? [] - await this.applyDeleteFiles(deletedFiles) - - await this.insertCodeReferenceLogs(this.state.references ?? []) - - if (this._codeResultMessageId) { - await this.updateFilesPaths({ - tabID: this.state.tabID, - filePaths: this.state.filePaths ?? [], - deletedFiles: this.state.deletedFiles ?? [], - messageId: this._codeResultMessageId, - }) - } - } - - public async insertNewFiles(newFilePaths: NewFileInfo[]) { - await this.sendLinesOfCodeAcceptedTelemetry(newFilePaths) - for (const filePath of newFilePaths) { - const absolutePath = path.join(filePath.workspaceFolder.uri.fsPath, filePath.relativePath) - - const uri = filePath.virtualMemoryUri - const content = await this.config.fs.readFile(uri) - const decodedContent = new TextDecoder().decode(content) - - await fs.mkdir(path.dirname(absolutePath)) - await fs.writeFile(absolutePath, decodedContent) - filePath.changeApplied = true - } - } - - public async applyDeleteFiles(deletedFiles: DeletedFileInfo[]) { - for (const filePath of deletedFiles) { - const absolutePath = path.join(filePath.workspaceFolder.uri.fsPath, filePath.relativePath) - await fs.delete(absolutePath) - filePath.changeApplied = true - } - } - - public async insertCodeReferenceLogs(codeReferences: CodeReference[]) { - for (const ref of codeReferences) { - ReferenceLogViewProvider.instance.addReferenceLog(referenceLogText(ref)) - } - } - - public async disableFileList() { - if (this._codeResultMessageId === undefined) { - return - } - - await this.updateFilesPaths({ - tabID: this.state.tabID, - filePaths: this.state.filePaths ?? [], - deletedFiles: this.state.deletedFiles ?? [], - messageId: this._codeResultMessageId, - disableFileActions: true, - }) - this._codeResultMessageId = undefined - } - - public updateCodeResultMessageId(messageId?: string) { - this._codeResultMessageId = messageId - } - - public updateAcceptCodeMessageId(messageId?: string) { - this._acceptCodeMessageId = messageId - } - - public updateAcceptCodeTelemetrySent(sent: boolean) { - this._acceptCodeTelemetrySent = sent - } - - public getInsertCodePillText(files: Array) { - if (files.every((file) => file.rejected || file.changeApplied)) { - return i18n('AWS.amazonq.featureDev.pillText.continue') - } - if (files.some((file) => file.rejected || file.changeApplied)) { - return i18n('AWS.amazonq.featureDev.pillText.acceptRemainingChanges') - } - return i18n('AWS.amazonq.featureDev.pillText.acceptAllChanges') - } - - public async computeFilePathDiff(filePath: NewFileInfo) { - const leftPath = `${filePath.workspaceFolder.uri.fsPath}/${filePath.relativePath}` - const rightPath = filePath.virtualMemoryUri.path - const diff = await computeDiff(leftPath, rightPath, this.tabID, featureDevScheme) - return { leftPath, rightPath, ...diff } - } - - public async sendMetricDataTelemetry(operationName: string, result: string) { - await this.proxyClient.sendMetricData({ - metricName: 'Operation', - metricValue: 1, - timestamp: new Date(), - product: 'FeatureDev', - dimensions: [ - { - name: 'operationName', - value: operationName, - }, - { - name: 'result', - value: result, - }, - ], - }) - } - - public async sendLinesOfCodeGeneratedTelemetry() { - let charactersOfCodeGenerated = 0 - let linesOfCodeGenerated = 0 - // deleteFiles are currently not counted because the number of lines added is always 0 - const filePaths = this.state.filePaths ?? [] - for (const filePath of filePaths) { - const { leftPath, changes, charsAdded, linesAdded } = await this.computeFilePathDiff(filePath) - const codeChangeKey = `${leftPath}#@${JSON.stringify(changes)}` - if (this._reportedCodeChanges.has(codeChangeKey)) { - continue - } - charactersOfCodeGenerated += charsAdded - linesOfCodeGenerated += linesAdded - this._reportedCodeChanges.add(codeChangeKey) - } - await this.proxyClient.sendFeatureDevCodeGenerationEvent({ - conversationId: this.conversationId, - charactersOfCodeGenerated, - linesOfCodeGenerated, - }) - } - - public async sendLinesOfCodeAcceptedTelemetry(filePaths: NewFileInfo[]) { - let charactersOfCodeAccepted = 0 - let linesOfCodeAccepted = 0 - for (const filePath of filePaths) { - const { charsAdded, linesAdded } = await this.computeFilePathDiff(filePath) - charactersOfCodeAccepted += charsAdded - linesOfCodeAccepted += linesAdded - } - await this.proxyClient.sendFeatureDevCodeAcceptanceEvent({ - conversationId: this.conversationId, - charactersOfCodeAccepted, - linesOfCodeAccepted, - }) - } - - get state() { - if (!this._state) { - throw new IllegalStateError("State should be initialized before it's read") - } - return this._state - } - - get currentCodeGenerationId() { - return this.state.currentCodeGenerationId - } - - get uploadId() { - if (!('uploadId' in this.state)) { - throw new IllegalStateError("UploadId has to be initialized before it's read") - } - return this.state.uploadId - } - - get retries() { - return this.codeGenRetries - } - - decreaseRetries() { - this.codeGenRetries -= 1 - } - get conversationId() { - if (!this._conversationId) { - throw new ConversationIdNotFoundError() - } - return this._conversationId - } - - // Used for cases where it is not needed to have conversationId - get conversationIdUnsafe() { - return this._conversationId - } - - get latestMessage() { - return this._latestMessage - } - - set latestMessage(msg: string) { - this._latestMessage = msg - } - - get telemetry() { - return this._telemetry - } - - get acceptCodeMessageId() { - return this._acceptCodeMessageId - } - - get acceptCodeTelemetrySent() { - return this._acceptCodeTelemetrySent - } -} diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts deleted file mode 100644 index 5879c16493f..00000000000 --- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts +++ /dev/null @@ -1,285 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { MynahIcons } from '@aws/mynah-ui' -import * as path from 'path' -import * as vscode from 'vscode' -import { getLogger } from '../../shared/logger/logger' -import { featureDevScheme } from '../constants' -import { - ApiClientError, - ApiServiceError, - IllegalStateTransition, - NoChangeRequiredException, - PromptRefusalException, -} from '../errors' -import { - DeletedFileInfo, - DevPhase, - Intent, - NewFileInfo, - SessionState, - SessionStateAction, - SessionStateConfig, - SessionStateInteraction, -} from '../../amazonq/commons/types' -import { registerNewFiles } from '../../amazonq/util/files' -import { randomUUID } from '../../shared/crypto' -import { collectFiles } from '../../shared/utilities/workspaceUtils' -import { i18n } from '../../shared/i18n-helper' -import { Messenger } from '../../amazonq/commons/connector/baseMessenger' -import { FollowUpTypes } from '../../amazonq/commons/types' -import { - BaseCodeGenState, - BaseMessenger, - BasePrepareCodeGenState, - CreateNextStateParams, -} from '../../amazonq/session/sessionState' -import { LlmError } from '../../amazonq/errors' - -export class ConversationNotStartedState implements Omit { - public tokenSource: vscode.CancellationTokenSource - public readonly phase = DevPhase.INIT - - constructor(public tabID: string) { - this.tokenSource = new vscode.CancellationTokenSource() - } - - async interact(_action: SessionStateAction): Promise { - throw new IllegalStateTransition() - } -} - -export class MockCodeGenState implements SessionState { - public tokenSource: vscode.CancellationTokenSource - public filePaths: NewFileInfo[] - public deletedFiles: DeletedFileInfo[] - public readonly conversationId: string - public readonly codeGenerationId?: string - public readonly uploadId: string - - constructor( - private config: SessionStateConfig, - public tabID: string - ) { - this.tokenSource = new vscode.CancellationTokenSource() - this.filePaths = [] - this.deletedFiles = [] - this.conversationId = this.config.conversationId - this.uploadId = randomUUID() - } - - async interact(action: SessionStateAction): Promise { - // in a `mockcodegen` state, we should read from the `mock-data` folder and output - // every file retrieved in the same shape the LLM would - try { - const files = await collectFiles( - this.config.workspaceFolders.map((f) => path.join(f.uri.fsPath, './mock-data')), - this.config.workspaceFolders, - { - excludeByGitIgnore: false, - } - ) - const newFileContents = files.map((f) => ({ - zipFilePath: f.zipFilePath, - fileContent: f.fileContent, - })) - this.filePaths = registerNewFiles( - action.fs, - newFileContents, - this.uploadId, - this.config.workspaceFolders, - this.conversationId, - featureDevScheme - ) - this.deletedFiles = [ - { - zipFilePath: 'src/this-file-should-be-deleted.ts', - workspaceFolder: this.config.workspaceFolders[0], - relativePath: 'src/this-file-should-be-deleted.ts', - rejected: false, - changeApplied: false, - }, - ] - action.messenger.sendCodeResult( - this.filePaths, - this.deletedFiles, - [ - { - licenseName: 'MIT', - repository: 'foo', - url: 'foo', - }, - ], - this.tabID, - this.uploadId, - this.codeGenerationId ?? '' - ) - action.messenger.sendAnswer({ - message: undefined, - type: 'system-prompt', - followUps: [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.acceptAllChanges'), - type: FollowUpTypes.InsertCode, - icon: 'ok' as MynahIcons, - status: 'success', - }, - { - pillText: i18n('AWS.amazonq.featureDev.pillText.provideFeedback'), - type: FollowUpTypes.ProvideFeedbackAndRegenerateCode, - icon: 'refresh' as MynahIcons, - status: 'info', - }, - ], - tabID: this.tabID, - }) - } catch (e) { - // TODO: handle this error properly, double check what would be expected behaviour if mock code does not work. - getLogger().error('Unable to use mock code generation: %O', e) - } - - return { - // no point in iterating after a mocked code gen? - nextState: this, - interaction: {}, - } - } -} - -export class FeatureDevCodeGenState extends BaseCodeGenState { - protected handleProgress(messenger: Messenger, action: SessionStateAction, detail?: string): void { - if (detail) { - messenger.sendAnswer({ - message: i18n('AWS.amazonq.featureDev.pillText.generatingCode') + `\n\n${detail}`, - type: 'answer-part', - tabID: this.tabID, - }) - } - } - - protected getScheme(): string { - return featureDevScheme - } - - protected getTimeoutErrorCode(): string { - return 'CodeGenTimeout' - } - - protected handleGenerationComplete( - _messenger: Messenger, - _newFileInfo: NewFileInfo[], - action: SessionStateAction - ): void { - // No special handling needed for feature dev - } - - protected handleError(messenger: BaseMessenger, codegenResult: any): Error { - switch (true) { - case codegenResult.codeGenerationStatusDetail?.includes('Guardrails'): { - return new ApiClientError( - i18n('AWS.amazonq.featureDev.error.codeGen.default'), - 'GetTaskAssistCodeGeneration', - 'GuardrailsException', - 400 - ) - } - case codegenResult.codeGenerationStatusDetail?.includes('PromptRefusal'): { - return new PromptRefusalException() - } - case codegenResult.codeGenerationStatusDetail?.includes('EmptyPatch'): { - if (codegenResult.codeGenerationStatusDetail?.includes('NO_CHANGE_REQUIRED')) { - return new NoChangeRequiredException() - } - return new LlmError(i18n('AWS.amazonq.featureDev.error.codeGen.default'), { - code: 'EmptyPatchException', - }) - } - case codegenResult.codeGenerationStatusDetail?.includes('Throttling'): { - return new ApiClientError( - i18n('AWS.amazonq.featureDev.error.throttling'), - 'GetTaskAssistCodeGeneration', - 'ThrottlingException', - 429 - ) - } - case codegenResult.codeGenerationStatusDetail?.includes('FileCreationFailed'): { - return new ApiServiceError( - i18n('AWS.amazonq.featureDev.error.codeGen.default'), - 'GetTaskAssistCodeGeneration', - 'FileCreationFailedException', - 500 - ) - } - default: { - return new ApiServiceError( - i18n('AWS.amazonq.featureDev.error.codeGen.default'), - 'GetTaskAssistCodeGeneration', - 'UnknownException', - 500 - ) - } - } - } - - protected async startCodeGeneration(action: SessionStateAction, codeGenerationId: string): Promise { - await this.config.proxyClient.startCodeGeneration( - this.config.conversationId, - this.config.uploadId, - action.msg, - Intent.DEV, - codeGenerationId, - this.currentCodeGenerationId - ) - - if (!this.isCancellationRequested) { - action.messenger.sendAnswer({ - message: i18n('AWS.amazonq.featureDev.pillText.generatingCode'), - type: 'answer-part', - tabID: this.tabID, - }) - action.messenger.sendUpdatePlaceholder(this.tabID, i18n('AWS.amazonq.featureDev.pillText.generatingCode')) - } - } - - protected override createNextState(config: SessionStateConfig, params: CreateNextStateParams): SessionState { - return super.createNextState( - { ...config, currentCodeGenerationId: this.currentCodeGenerationId }, - params, - FeatureDevPrepareCodeGenState - ) - } -} - -export class FeatureDevPrepareCodeGenState extends BasePrepareCodeGenState { - protected preUpload(action: SessionStateAction): void { - action.messenger.sendAnswer({ - message: i18n('AWS.amazonq.featureDev.pillText.uploadingCode'), - type: 'answer-part', - tabID: this.tabID, - }) - - action.messenger.sendUpdatePlaceholder(this.tabID, i18n('AWS.amazonq.featureDev.pillText.uploadingCode')) - } - - protected postUpload(action: SessionStateAction): void { - if (!action.tokenSource?.token.isCancellationRequested) { - action.messenger.sendAnswer({ - message: i18n('AWS.amazonq.featureDev.pillText.contextGatheringCompleted'), - type: 'answer-part', - tabID: this.tabID, - }) - - action.messenger.sendUpdatePlaceholder( - this.tabID, - i18n('AWS.amazonq.featureDev.pillText.contextGatheringCompleted') - ) - } - } - - protected override createNextState(config: SessionStateConfig): SessionState { - return super.createNextState(config, FeatureDevCodeGenState) - } -} diff --git a/packages/core/src/amazonqFeatureDev/storages/chatSession.ts b/packages/core/src/amazonqFeatureDev/storages/chatSession.ts deleted file mode 100644 index f45576aa9df..00000000000 --- a/packages/core/src/amazonqFeatureDev/storages/chatSession.ts +++ /dev/null @@ -1,23 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { BaseChatSessionStorage } from '../../amazonq/commons/baseChatStorage' -import { Messenger } from '../../amazonq/commons/connector/baseMessenger' -import { createSessionConfig } from '../../amazonq/commons/session/sessionConfigFactory' -import { featureDevScheme } from '../constants' -import { Session } from '../session/session' - -export class FeatureDevChatSessionStorage extends BaseChatSessionStorage { - constructor(protected readonly messenger: Messenger) { - super() - } - - override async createSession(tabID: string): Promise { - const sessionConfig = await createSessionConfig(featureDevScheme) - const session = new Session(sessionConfig, this.messenger, tabID) - this.sessions.set(tabID, session) - return session - } -} diff --git a/packages/core/src/amazonqFeatureDev/userFacingText.ts b/packages/core/src/amazonqFeatureDev/userFacingText.ts deleted file mode 100644 index 9b8a781ef1a..00000000000 --- a/packages/core/src/amazonqFeatureDev/userFacingText.ts +++ /dev/null @@ -1,25 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { manageAccessGuideURL } from '../amazonq/webview/ui/texts/constants' -import { userGuideURL } from '../amazonq/webview/ui/texts/constants' -import { featureName } from './constants' - -export const examples = ` -You can use /dev to: -- Add a new feature or logic -- Write tests -- Fix a bug in your project -- Generate a README for a file, folder, or project - -To learn more, visit the _[Amazon Q Developer User Guide](${userGuideURL})_. -` - -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.` - -// Utils for logging and showing customer facing conversation id text -export const messageWithConversationId = (conversationId?: string) => - conversationId ? `\n\nConversation ID: **${conversationId}**` : '' -export const logWithConversationId = (conversationId: string) => `${featureName} Conversation ID: ${conversationId}` diff --git a/packages/core/src/amazonqFeatureDev/views/actions/uiMessageListener.ts b/packages/core/src/amazonqFeatureDev/views/actions/uiMessageListener.ts deleted file mode 100644 index 5d92fb7188c..00000000000 --- a/packages/core/src/amazonqFeatureDev/views/actions/uiMessageListener.ts +++ /dev/null @@ -1,169 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ChatControllerEventEmitters } from '../../controllers/chat/controller' -import { MessageListener } from '../../../amazonq/messages/messageListener' -import { ExtensionMessage } from '../../../amazonq/webview/ui/commands' - -export interface UIMessageListenerProps { - readonly chatControllerEventEmitters: ChatControllerEventEmitters - readonly webViewMessageListener: MessageListener -} - -export class UIMessageListener { - private featureDevControllerEventsEmitters: ChatControllerEventEmitters | undefined - private webViewMessageListener: MessageListener - - constructor(props: UIMessageListenerProps) { - this.featureDevControllerEventsEmitters = props.chatControllerEventEmitters - this.webViewMessageListener = props.webViewMessageListener - - // Now we are listening to events that get sent from amazonq/webview/actions/actionListener (e.g. the tab) - this.webViewMessageListener.onMessage((msg) => { - this.handleMessage(msg) - }) - } - - private handleMessage(msg: ExtensionMessage) { - switch (msg.command) { - case 'chat-prompt': - this.processChatMessage(msg) - break - case 'follow-up-was-clicked': - this.followUpClicked(msg) - break - case 'open-diff': - this.openDiff(msg) - break - case 'chat-item-voted': - this.chatItemVoted(msg) - break - case 'chat-item-feedback': - this.chatItemFeedback(msg) - break - case 'stop-response': - this.stopResponse(msg) - break - case 'new-tab-was-created': - this.tabOpened(msg) - break - case 'tab-was-removed': - this.tabClosed(msg) - break - case 'auth-follow-up-was-clicked': - this.authClicked(msg) - break - case 'response-body-link-click': - this.processResponseBodyLinkClick(msg) - break - case 'insert_code_at_cursor_position': - this.insertCodeAtPosition(msg) - break - case 'file-click': - this.fileClicked(msg) - break - case 'store-code-result-message-id': - this.storeCodeResultMessageId(msg) - break - } - } - - private chatItemVoted(msg: any) { - this.featureDevControllerEventsEmitters?.processChatItemVotedMessage.fire({ - tabID: msg.tabID, - command: msg.command, - vote: msg.vote, - messageId: msg.messageId, - }) - } - - private chatItemFeedback(msg: any) { - this.featureDevControllerEventsEmitters?.processChatItemFeedbackMessage.fire(msg) - } - - private processChatMessage(msg: any) { - this.featureDevControllerEventsEmitters?.processHumanChatMessage.fire({ - message: msg.chatMessage, - tabID: msg.tabID, - }) - } - - private followUpClicked(msg: any) { - this.featureDevControllerEventsEmitters?.followUpClicked.fire({ - followUp: msg.followUp, - tabID: msg.tabID, - }) - } - - private fileClicked(msg: any) { - this.featureDevControllerEventsEmitters?.fileClicked.fire({ - tabID: msg.tabID, - filePath: msg.filePath, - actionName: msg.actionName, - messageId: msg.messageId, - }) - } - - private openDiff(msg: any) { - this.featureDevControllerEventsEmitters?.openDiff.fire({ - tabID: msg.tabID, - filePath: msg.filePath, - deleted: msg.deleted, - messageId: msg.messageId, - }) - } - - private stopResponse(msg: any) { - this.featureDevControllerEventsEmitters?.stopResponse.fire({ - tabID: msg.tabID, - }) - } - - private tabOpened(msg: any) { - this.featureDevControllerEventsEmitters?.tabOpened.fire({ - tabID: msg.tabID, - }) - } - - private tabClosed(msg: any) { - this.featureDevControllerEventsEmitters?.tabClosed.fire({ - tabID: msg.tabID, - }) - } - - private authClicked(msg: any) { - this.featureDevControllerEventsEmitters?.authClicked.fire({ - tabID: msg.tabID, - authType: msg.authType, - }) - } - - private processResponseBodyLinkClick(msg: any) { - this.featureDevControllerEventsEmitters?.processResponseBodyLinkClick.fire({ - command: msg.command, - messageId: msg.messageId, - tabID: msg.tabID, - link: msg.link, - }) - } - - private insertCodeAtPosition(msg: any) { - this.featureDevControllerEventsEmitters?.insertCodeAtPositionClicked.fire({ - command: msg.command, - messageId: msg.messageId, - tabID: msg.tabID, - code: msg.code, - insertionTargetType: msg.insertionTargetType, - codeReference: msg.codeReference, - }) - } - - private storeCodeResultMessageId(msg: any) { - this.featureDevControllerEventsEmitters?.storeCodeResultMessageId.fire({ - messageId: msg.messageId, - tabID: msg.tabID, - }) - } -} diff --git a/packages/core/src/amazonqGumby/activation.ts b/packages/core/src/amazonqGumby/activation.ts index 74823f6fbc6..8ab47f5697e 100644 --- a/packages/core/src/amazonqGumby/activation.ts +++ b/packages/core/src/amazonqGumby/activation.ts @@ -21,7 +21,7 @@ import { setContext } from '../shared/vscode/setContext' export async function activate(context: ExtContext) { void setContext('gumby.wasQCodeTransformationUsed', false) - const transformationHubViewProvider = new TransformationHubViewProvider() + const transformationHubViewProvider = TransformationHubViewProvider.instance new ProposedTransformationExplorer(context.extensionContext) // Register an activation event listener to determine when the IDE opens, closes or users // select to open a new workspace @@ -72,6 +72,13 @@ export async function activate(context: ExtContext) { ) }), + Commands.register( + 'aws.amazonq.transformationHub.updateContent', + async (button, startTime, historyFileUpdated) => { + await transformationHubViewProvider.updateContent(button, startTime, historyFileUpdated) + } + ), + workspaceChangeEvent ) } diff --git a/packages/core/src/amazonqGumby/chat/controller/controller.ts b/packages/core/src/amazonqGumby/chat/controller/controller.ts index 3b40ad9882f..d171eae31bf 100644 --- a/packages/core/src/amazonqGumby/chat/controller/controller.ts +++ b/packages/core/src/amazonqGumby/chat/controller/controller.ts @@ -57,6 +57,8 @@ import { } from '../../../codewhisperer/service/transformByQ/transformFileHandler' import { getAuthType } from '../../../auth/utils' import fs from '../../../shared/fs/fs' +import { setContext } from '../../../shared/vscode/setContext' +import { readHistoryFile } from '../../../codewhisperer/service/transformByQ/transformationHistoryHandler' // These events can be interactions within the chat, // or elsewhere in the IDE @@ -188,6 +190,15 @@ export class GumbyController { } private async transformInitiated(message: any) { + // check if any jobs potentially still in progress on backend + const history = await readHistoryFile() + const numInProgress = history.filter((job) => job.status === 'FAILED').length + this.messenger.sendViewHistoryMessage(message.tabID, numInProgress) + if (transformByQState.isRefreshInProgress()) { + this.messenger.sendMessage(CodeWhispererConstants.refreshInProgressChatMessage, message.tabID, 'ai-prompt') + return + } + // silently check for projects eligible for SQL conversion let embeddedSQLProjects: TransformationCandidateProject[] = [] try { @@ -383,6 +394,11 @@ export class GumbyController { case ButtonActions.VIEW_TRANSFORMATION_HUB: await vscode.commands.executeCommand(GumbyCommands.FOCUS_TRANSFORMATION_HUB, CancelActionPositions.Chat) break + case ButtonActions.VIEW_JOB_HISTORY: + await setContext('gumby.wasQCodeTransformationUsed', true) + await vscode.commands.executeCommand(GumbyCommands.FOCUS_TRANSFORMATION_HUB) + await vscode.commands.executeCommand(GumbyCommands.FOCUS_JOB_HISTORY, CancelActionPositions.Chat) + break case ButtonActions.VIEW_SUMMARY: await vscode.commands.executeCommand('aws.amazonq.transformationHub.summary.reveal') break @@ -432,20 +448,30 @@ export class GumbyController { private promptJavaHome(type: 'source' | 'target', tabID: any) { let jdkVersion = undefined + let currJavaHome = undefined if (type === 'source') { this.sessionStorage.getSession().conversationState = ConversationState.PROMPT_SOURCE_JAVA_HOME jdkVersion = transformByQState.getSourceJDKVersion() + currJavaHome = transformByQState.getPathFromJdkVersion(transformByQState.getSourceJDKVersion()) } else if (type === 'target') { this.sessionStorage.getSession().conversationState = ConversationState.PROMPT_TARGET_JAVA_HOME jdkVersion = transformByQState.getTargetJDKVersion() + currJavaHome = transformByQState.getPathFromJdkVersion(transformByQState.getTargetJDKVersion()) + } + let message = MessengerUtils.createJavaHomePrompt(jdkVersion) + if (currJavaHome) { + message += `\n\ncurrent:\n\n\`${currJavaHome}\`` } - const message = MessengerUtils.createJavaHomePrompt(jdkVersion) this.messenger.sendMessage(message, tabID, 'ai-prompt') this.messenger.sendChatInputEnabled(tabID, true) this.messenger.sendUpdatePlaceholder(tabID, CodeWhispererConstants.enterJavaHomePlaceholder) } private async handleUserLanguageUpgradeProjectChoice(message: any) { + if (transformByQState.isRefreshInProgress()) { + this.messenger.sendMessage(CodeWhispererConstants.refreshInProgressChatMessage, message.tabID, 'ai-prompt') + return + } await telemetry.codeTransform_submitSelection.run(async () => { const pathToProject: string = message.formSelectedValues['GumbyTransformLanguageUpgradeProjectForm'] const toJDKVersion: JDKVersion = message.formSelectedValues['GumbyTransformJdkToForm'] @@ -478,6 +504,10 @@ export class GumbyController { } private async handleUserSQLConversionProjectSelection(message: any) { + if (transformByQState.isRefreshInProgress()) { + this.messenger.sendMessage(CodeWhispererConstants.refreshInProgressChatMessage, message.tabID, 'ai-prompt') + return + } await telemetry.codeTransform_submitSelection.run(async () => { const pathToProject: string = message.formSelectedValues['GumbyTransformSQLConversionProjectForm'] const schema: string = message.formSelectedValues['GumbyTransformSQLSchemaForm'] @@ -550,10 +580,14 @@ export class GumbyController { return } const fileContents = await fs.readFileText(fileUri[0].fsPath) - const isValidFile = await validateCustomVersionsFile(fileContents) + const errorMessage = validateCustomVersionsFile(fileContents) - if (!isValidFile) { - this.messenger.sendUnrecoverableErrorResponse('invalid-custom-versions-file', message.tabID) + if (errorMessage) { + this.messenger.sendMessage( + CodeWhispererConstants.invalidCustomVersionsFileMessage(errorMessage), + message.tabID, + 'ai-prompt' + ) return } this.messenger.sendMessage(CodeWhispererConstants.receivedValidConfigFileMessage, message.tabID, 'ai-prompt') @@ -640,6 +674,7 @@ export class GumbyController { const pathToJavaHome = extractPath(data.message) if (pathToJavaHome) { transformByQState.setSourceJavaHome(pathToJavaHome) + transformByQState.setJdkVersionToPath(transformByQState.getSourceJDKVersion(), pathToJavaHome) // if source and target JDK versions are the same, just re-use the source JAVA_HOME and start the build if (transformByQState.getTargetJDKVersion() === transformByQState.getSourceJDKVersion()) { transformByQState.setTargetJavaHome(pathToJavaHome) @@ -657,6 +692,7 @@ export class GumbyController { const pathToJavaHome = extractPath(data.message) if (pathToJavaHome) { transformByQState.setTargetJavaHome(pathToJavaHome) + transformByQState.setJdkVersionToPath(transformByQState.getTargetJDKVersion(), pathToJavaHome) await this.prepareLanguageUpgradeProject(data.tabID) // build right after we get target JDK path } else { this.messenger.sendUnrecoverableErrorResponse('invalid-java-home', data.tabID) diff --git a/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts b/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts index 699e3b77938..409ee89ab04 100644 --- a/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts +++ b/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts @@ -50,7 +50,6 @@ export type UnrecoverableErrorType = | 'job-start-failed' | 'unsupported-source-db' | 'unsupported-target-db' - | 'invalid-custom-versions-file' | 'error-parsing-sct-file' | 'invalid-zip-no-sct-file' | 'invalid-from-to-jdk' @@ -377,6 +376,38 @@ export class Messenger { this.dispatcher.sendChatMessage(jobSubmittedMessage) } + public sendViewHistoryMessage(tabID: string, numInProgress: number) { + const buttons: ChatItemButton[] = [] + + buttons.push({ + keepCardAfterClick: true, + text: CodeWhispererConstants.jobHistoryButtonText, + id: ButtonActions.VIEW_JOB_HISTORY, + disabled: false, + }) + + const messageText = CodeWhispererConstants.viewHistoryMessage(numInProgress) + + const message = new ChatMessage( + { + message: messageText, + messageType: 'ai-prompt', + buttons, + }, + tabID + ) + this.dispatcher.sendChatMessage(message) + } + + public sendJobRefreshInProgressMessage(tabID: string, jobId: string) { + this.dispatcher.sendAsyncEventProgress( + new AsyncEventProgressMessage(tabID, { + inProgress: true, + message: CodeWhispererConstants.refreshingJobChatMessage(jobId), + }) + ) + } + public sendMessage(prompt: string, tabID: string, type: 'prompt' | 'ai-prompt') { this.dispatcher.sendChatMessage( new ChatMessage( @@ -421,9 +452,6 @@ export class Messenger { case 'unsupported-target-db': message = CodeWhispererConstants.invalidMetadataFileUnsupportedTargetDB break - case 'invalid-custom-versions-file': - message = CodeWhispererConstants.invalidCustomVersionsFileMessage - break case 'error-parsing-sct-file': message = CodeWhispererConstants.invalidMetadataFileErrorParsing break diff --git a/packages/core/src/amazonqGumby/chat/controller/messenger/messengerUtils.ts b/packages/core/src/amazonqGumby/chat/controller/messenger/messengerUtils.ts index af9f9f47a7b..2c64a050547 100644 --- a/packages/core/src/amazonqGumby/chat/controller/messenger/messengerUtils.ts +++ b/packages/core/src/amazonqGumby/chat/controller/messenger/messengerUtils.ts @@ -13,6 +13,7 @@ import DependencyVersions from '../../../models/dependencies' export enum ButtonActions { STOP_TRANSFORMATION_JOB = 'gumbyStopTransformationJob', VIEW_TRANSFORMATION_HUB = 'gumbyViewTransformationHub', + VIEW_JOB_HISTORY = 'gumbyViewJobHistory', VIEW_SUMMARY = 'gumbyViewSummary', CONFIRM_LANGUAGE_UPGRADE_TRANSFORMATION_FORM = 'gumbyLanguageUpgradeTransformFormConfirm', CONFIRM_SQL_CONVERSION_TRANSFORMATION_FORM = 'gumbySQLConversionTransformFormConfirm', @@ -33,11 +34,12 @@ export enum GumbyCommands { CLEAR_CHAT = 'aws.awsq.clearchat', START_TRANSFORMATION_FLOW = 'aws.awsq.transform', FOCUS_TRANSFORMATION_HUB = 'aws.amazonq.showTransformationHub', + FOCUS_JOB_HISTORY = 'aws.amazonq.showHistoryInHub', } export default class MessengerUtils { static createJavaHomePrompt = (jdkVersion: JDKVersion | undefined): string => { - let javaHomePrompt = `${CodeWhispererConstants.enterJavaHomeChatMessage} ${jdkVersion}. \n` + let javaHomePrompt = `${CodeWhispererConstants.enterJavaHomeChatMessage} ${jdkVersion}.\n\n` if (os.platform() === 'win32') { javaHomePrompt += CodeWhispererConstants.windowsJavaHomeHelpChatMessage } else if (os.platform() === 'darwin') { diff --git a/packages/core/src/amazonqTest/app.ts b/packages/core/src/amazonqTest/app.ts deleted file mode 100644 index 6c638c13b71..00000000000 --- a/packages/core/src/amazonqTest/app.ts +++ /dev/null @@ -1,76 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import { AmazonQAppInitContext } from '../amazonq/apps/initContext' -import { MessagePublisher } from '../amazonq/messages/messagePublisher' -import { MessageListener } from '../amazonq/messages/messageListener' -import { AuthUtil } from '../codewhisperer/util/authUtil' -import { ChatSessionManager } from './chat/storages/chatSession' -import { TestController, TestChatControllerEventEmitters } from './chat/controller/controller' -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 { testGenState } from '../codewhisperer/models/model' - -export function init(appContext: AmazonQAppInitContext) { - const testChatControllerEventEmitters: TestChatControllerEventEmitters = { - tabOpened: new vscode.EventEmitter(), - tabClosed: new vscode.EventEmitter(), - authClicked: new vscode.EventEmitter(), - startTestGen: new vscode.EventEmitter(), - processHumanChatMessage: new vscode.EventEmitter(), - updateTargetFileInfo: new vscode.EventEmitter(), - showCodeGenerationResults: new vscode.EventEmitter(), - openDiff: new vscode.EventEmitter(), - formActionClicked: new vscode.EventEmitter(), - followUpClicked: new vscode.EventEmitter(), - sendUpdatePromptProgress: new vscode.EventEmitter(), - errorThrown: new vscode.EventEmitter(), - insertCodeAtCursorPosition: new vscode.EventEmitter(), - processResponseBodyLinkClick: new vscode.EventEmitter(), - processChatItemVotedMessage: new vscode.EventEmitter(), - processChatItemFeedbackMessage: new vscode.EventEmitter(), - } - const dispatcher = new AppToWebViewMessageDispatcher(appContext.getAppsToWebViewMessagePublisher()) - const messenger = new Messenger(dispatcher) - - new TestController(testChatControllerEventEmitters, messenger, appContext.onDidChangeAmazonQVisibility.event) - - const testChatUIInputEventEmitter = new vscode.EventEmitter() - - new UIMessageListener({ - chatControllerEventEmitters: testChatControllerEventEmitters, - webViewMessageListener: new MessageListener(testChatUIInputEventEmitter), - }) - - appContext.registerWebViewToAppMessagePublisher(new MessagePublisher(testChatUIInputEventEmitter), 'testgen') - - const debouncedEvent = debounce(async () => { - const authenticated = (await AuthUtil.instance.getChatAuthState()).amazonQ === 'connected' - let authenticatingSessionID = '' - - if (authenticated) { - const session = ChatSessionManager.Instance.getSession() - - if (session.isTabOpen() && session.isAuthenticating) { - authenticatingSessionID = session.tabID! - session.isAuthenticating = false - } - } - - messenger.sendAuthenticationUpdate(authenticated, [authenticatingSessionID]) - }, 500) - - AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(() => { - return debouncedEvent() - }) - AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { - return debouncedEvent() - }) - testGenState.setChatControllers(testChatControllerEventEmitters) - // TODO: Add testGen provider for creating new files after test generation if they does not exist -} diff --git a/packages/core/src/amazonqTest/chat/controller/controller.ts b/packages/core/src/amazonqTest/chat/controller/controller.ts deleted file mode 100644 index 747cca57e8e..00000000000 --- a/packages/core/src/amazonqTest/chat/controller/controller.ts +++ /dev/null @@ -1,1464 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * This class is responsible for responding to UI events by calling - * the Test extension. - */ -import * as vscode from 'vscode' -import path from 'path' -import { FollowUps, Messenger, TestNamedMessages } from './messenger/messenger' -import { AuthController } from '../../../amazonq/auth/controller' -import { ChatSessionManager } from '../storages/chatSession' -import { BuildStatus, ConversationState, Session } from '../session/session' -import { AuthUtil } from '../../../codewhisperer/util/authUtil' -import { - buildProgressField, - cancellingProgressField, - cancelTestGenButton, - errorProgressField, - testGenBuildProgressMessage, - testGenCompletedField, - testGenProgressField, - testGenSummaryMessage, - maxUserPromptLength, -} from '../../models/constants' -import MessengerUtils, { ButtonActions } from './messenger/messengerUtils' -import { getTelemetryReasonDesc, isAwsError } from '../../../shared/errors' -import { ChatItemType } from '../../../amazonq/commons/model' -import { ChatItemButton, MynahIcons, ProgressField } from '@aws/mynah-ui' -import { FollowUpTypes } from '../../../amazonq/commons/types' -import { - cancelBuild, - runBuildCommand, - startTestGenerationProcess, -} from '../../../codewhisperer/commands/startTestGeneration' -import { UserIntent } from '@amzn/codewhisperer-streaming' -import { getSelectedCustomization } from '../../../codewhisperer/util/customizationUtil' -import { createCodeWhispererChatStreamingClient } from '../../../shared/clients/codewhispererChatClient' -import { - ChatItemVotedMessage, - ChatTriggerType, - TriggerPayload, -} from '../../../codewhispererChat/controllers/chat/model' -import { triggerPayloadToChatRequest } from '../../../codewhispererChat/controllers/chat/chatRequest/converter' -import { EditorContentController } from '../../../amazonq/commons/controllers/contentController' -import { amazonQTabSuffix } from '../../../shared/constants' -import { applyChanges } from '../../../shared/utilities/textDocumentUtilities' -import { telemetry } from '../../../shared/telemetry/telemetry' -import { CodeWhispererSettings } from '../../../codewhisperer/util/codewhispererSettings' -import globals from '../../../shared/extensionGlobals' -import { openUrl } from '../../../shared/utilities/vsCodeUtils' -import { getLogger } from '../../../shared/logger/logger' -import { i18n } from '../../../shared/i18n-helper' -import { sleep } from '../../../shared/utilities/timeoutUtils' -import { fs } from '../../../shared/fs/fs' -import { randomUUID } from '../../../shared/crypto' -import { tempDirPath, testGenerationLogsDir } from '../../../shared/filesystemUtilities' -import { CodeReference } from '../../../codewhispererChat/view/connector/connector' -import { TelemetryHelper } from '../../../codewhisperer/util/telemetryHelper' -import { Reference, testGenState } from '../../../codewhisperer/models/model' -import { - referenceLogText, - TestGenerationBuildStep, - tooManyRequestErrorMessage, - unitTestGenerationCancelMessage, - utgLimitReached, -} from '../../../codewhisperer/models/constants' -import { UserWrittenCodeTracker } from '../../../codewhisperer/tracker/userWrittenCodeTracker' -import { ReferenceLogViewProvider } from '../../../codewhisperer/service/referenceLogViewProvider' -import { TargetFileInfo } from '../../../codewhisperer/client/codewhispereruserclient' -import { submitFeedback } from '../../../feedback/vue/submitFeedback' -import { placeholder } from '../../../shared/vscode/commands2' -import { Auth } from '../../../auth/auth' -import { defaultContextLengths } from '../../../codewhispererChat/constants' - -export interface TestChatControllerEventEmitters { - readonly tabOpened: vscode.EventEmitter - readonly tabClosed: vscode.EventEmitter - readonly authClicked: vscode.EventEmitter - readonly startTestGen: vscode.EventEmitter - readonly processHumanChatMessage: vscode.EventEmitter - readonly updateTargetFileInfo: vscode.EventEmitter - readonly showCodeGenerationResults: vscode.EventEmitter - readonly openDiff: vscode.EventEmitter - readonly formActionClicked: vscode.EventEmitter - readonly followUpClicked: vscode.EventEmitter - readonly sendUpdatePromptProgress: vscode.EventEmitter - readonly errorThrown: vscode.EventEmitter - readonly insertCodeAtCursorPosition: vscode.EventEmitter - readonly processResponseBodyLinkClick: vscode.EventEmitter - readonly processChatItemVotedMessage: vscode.EventEmitter - readonly processChatItemFeedbackMessage: vscode.EventEmitter -} - -type OpenDiffMessage = { - tabID: string - messageId: string - filePath: string - codeGenerationId: string -} - -export class TestController { - private readonly messenger: Messenger - private readonly sessionStorage: ChatSessionManager - private authController: AuthController - private readonly editorContentController: EditorContentController - tempResultDirPath = path.join(tempDirPath, 'q-testgen') - - public constructor( - private readonly chatControllerMessageListeners: TestChatControllerEventEmitters, - messenger: Messenger, - onDidChangeAmazonQVisibility: vscode.Event - ) { - this.messenger = messenger - this.sessionStorage = ChatSessionManager.Instance - this.authController = new AuthController() - this.editorContentController = new EditorContentController() - - this.chatControllerMessageListeners.tabOpened.event((data) => { - return this.tabOpened(data) - }) - - this.chatControllerMessageListeners.tabClosed.event((data) => { - return this.tabClosed(data) - }) - - this.chatControllerMessageListeners.authClicked.event((data) => { - this.authClicked(data) - }) - - this.chatControllerMessageListeners.startTestGen.event(async (data) => { - await this.startTestGen(data, false) - }) - - this.chatControllerMessageListeners.processHumanChatMessage.event((data) => { - return this.processHumanChatMessage(data) - }) - - this.chatControllerMessageListeners.formActionClicked.event((data) => { - return this.handleFormActionClicked(data) - }) - - this.chatControllerMessageListeners.updateTargetFileInfo.event((data) => { - return this.updateTargetFileInfo(data) - }) - - this.chatControllerMessageListeners.showCodeGenerationResults.event((data) => { - return this.showCodeGenerationResults(data) - }) - - this.chatControllerMessageListeners.openDiff.event((data) => { - return this.openDiff(data) - }) - - this.chatControllerMessageListeners.sendUpdatePromptProgress.event((data) => { - return this.handleUpdatePromptProgress(data) - }) - - this.chatControllerMessageListeners.errorThrown.event((data) => { - return this.handleErrorMessage(data) - }) - - this.chatControllerMessageListeners.insertCodeAtCursorPosition.event((data) => { - return this.handleInsertCodeAtCursorPosition(data) - }) - - this.chatControllerMessageListeners.processResponseBodyLinkClick.event((data) => { - return this.processLink(data) - }) - - this.chatControllerMessageListeners.processChatItemVotedMessage.event((data) => { - this.processChatItemVotedMessage(data).catch((e) => { - getLogger().error('processChatItemVotedMessage failed: %s', (e as Error).message) - }) - }) - - this.chatControllerMessageListeners.processChatItemFeedbackMessage.event((data) => { - this.processChatItemFeedbackMessage(data).catch((e) => { - getLogger().error('processChatItemFeedbackMessage failed: %s', (e as Error).message) - }) - }) - - this.chatControllerMessageListeners.followUpClicked.event((data) => { - switch (data.followUp.type) { - case FollowUpTypes.ViewDiff: - return this.openDiff(data) - case FollowUpTypes.AcceptCode: - return this.acceptCode(data) - case FollowUpTypes.RejectCode: - return this.endSession(data, FollowUpTypes.RejectCode) - case FollowUpTypes.ContinueBuildAndExecute: - return this.handleBuildIteration(data) - case FollowUpTypes.BuildAndExecute: - return this.checkForInstallationDependencies(data) - case FollowUpTypes.ModifyCommands: - return this.modifyBuildCommand(data) - case FollowUpTypes.SkipBuildAndFinish: - return this.endSession(data, FollowUpTypes.SkipBuildAndFinish) - case FollowUpTypes.InstallDependenciesAndContinue: - return this.handleInstallDependencies(data) - case FollowUpTypes.ViewCodeDiffAfterIteration: - return this.openDiff(data) - } - }) - - AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { - this.sessionStorage.removeActiveTab() - }) - } - - /** - * Basic Functions - */ - private async tabOpened(message: any) { - const session: Session = this.sessionStorage.getSession() - const tabID = this.sessionStorage.setActiveTab(message.tabID) - const logger = getLogger() - logger.debug('Tab opened Processing message tabId: %s', message.tabID) - - // check if authentication has expired - try { - logger.debug(`Q - Test: Session created with id: ${session.tabID}`) - - const authState = await AuthUtil.instance.getChatAuthState() - if (authState.amazonQ !== 'connected') { - void this.messenger.sendAuthNeededExceptionMessage(authState, tabID) - session.isAuthenticating = true - return - } - } catch (err: any) { - logger.error('tabOpened failed: %O', err) - this.messenger.sendErrorMessage(err.message, message.tabID) - } - } - - private async processChatItemVotedMessage(message: ChatItemVotedMessage) { - const session = this.sessionStorage.getSession() - - telemetry.amazonq_feedback.emit({ - featureId: 'amazonQTest', - amazonqConversationId: session.startTestGenerationRequestId, - credentialStartUrl: AuthUtil.instance.startUrl, - interactionType: message.vote, - }) - } - - private async processChatItemFeedbackMessage(message: any) { - const session = this.sessionStorage.getSession() - - await globals.telemetry.postFeedback({ - comment: `${JSON.stringify({ - type: 'testgen-chat-answer-feedback', - amazonqConversationId: session.startTestGenerationRequestId, - reason: message?.selectedOption, - userComment: message?.comment, - })}`, - sentiment: 'Negative', - }) - } - - private async tabClosed(data: any) { - getLogger().debug('Tab closed with data tab id: %s', data.tabID) - await this.sessionCleanUp() - getLogger().debug('Removing active tab') - this.sessionStorage.removeActiveTab() - } - - private authClicked(message: any) { - this.authController.handleAuth(message.authType) - - this.messenger.sendMessage('Follow instructions to re-authenticate ...', message.tabID, 'answer') - - // Explicitly ensure the user goes through the re-authenticate flow - this.messenger.sendChatInputEnabled(message.tabID, false) - } - - private processLink(message: any) { - void openUrl(vscode.Uri.parse(message.link)) - } - - private handleInsertCodeAtCursorPosition(message: any) { - this.editorContentController.insertTextAtCursorPosition(message.code, () => {}) - } - - private checkCodeDiffLengthAndBuildStatus(state: { codeDiffLength: number; buildStatus: BuildStatus }): boolean { - return state.codeDiffLength !== 0 && state.buildStatus !== BuildStatus.SUCCESS - } - - // Displaying error message to the user in the chat tab - private async handleErrorMessage(data: any) { - testGenState.setToNotStarted() - // eslint-disable-next-line unicorn/no-null - this.messenger.sendUpdatePromptProgress(data.tabID, null) - const session = this.sessionStorage.getSession() - const isCancel = data.error.uiMessage === unitTestGenerationCancelMessage - let telemetryErrorMessage = getTelemetryReasonDesc(data.error) - if (session.stopIteration) { - telemetryErrorMessage = getTelemetryReasonDesc(data.error.uiMessage.replaceAll('```', '')) - } - TelemetryHelper.instance.sendTestGenerationToolkitEvent( - session, - session.isSupportedLanguage, - true, - isCancel ? 'Cancelled' : 'Failed', - session.startTestGenerationRequestId, - performance.now() - session.testGenerationStartTime, - telemetryErrorMessage, - session.isCodeBlockSelected, - session.artifactsUploadDuration, - session.srcPayloadSize, - session.srcZipFileSize, - session.charsOfCodeAccepted, - session.numberOfTestsGenerated, - session.linesOfCodeGenerated, - session.charsOfCodeGenerated, - session.numberOfTestsGenerated, - session.linesOfCodeGenerated, - undefined, - isCancel ? 'CANCELLED' : 'FAILED' - ) - if (session.stopIteration) { - // Error from Science - this.messenger.sendMessage( - data.error.uiMessage.replaceAll('```', ''), - data.tabID, - 'answer', - 'testGenErrorMessage', - this.getFeedbackButtons() - ) - } else { - isCancel - ? this.messenger.sendMessage( - data.error.uiMessage, - data.tabID, - 'answer', - 'testGenErrorMessage', - this.getFeedbackButtons() - ) - : this.sendErrorMessage(data) - } - await this.sessionCleanUp() - return - } - // Client side error messages - private sendErrorMessage(data: { - tabID: string - error: { uiMessage: string; message: string; code: string; statusCode: string } - }) { - const { error, tabID } = data - - // If user reached monthly limit for builderId - if (error.code === 'CreateTestJobError') { - if (error.message.includes(utgLimitReached)) { - getLogger().error('Monthly quota reached for QSDA actions.') - return this.messenger.sendMessage( - i18n('AWS.amazonq.featureDev.error.monthlyLimitReached'), - tabID, - 'answer', - 'testGenErrorMessage', - this.getFeedbackButtons() - ) - } - if (error.message.includes('Too many requests')) { - getLogger().error(error.message) - return this.messenger.sendErrorMessage(tooManyRequestErrorMessage, tabID) - } - } - if (isAwsError(error)) { - if (error.code === 'ThrottlingException') { - // TODO: use the explicitly modeled exception reason for quota vs throttle{ - getLogger().error(error.message) - this.messenger.sendErrorMessage(tooManyRequestErrorMessage, tabID) - return - } - // other service errors: - // AccessDeniedException - should not happen because access is validated before this point in the client - // ValidationException - shouldn't happen because client should not send malformed requests - // ConflictException - should not happen because the client will maintain proper state - // InternalServerException - shouldn't happen but needs to be caught - getLogger().error('Other error message: %s', error.message) - this.messenger.sendErrorMessage('', tabID) - return - } - // other unexpected errors (TODO enumerate all other failure cases) - getLogger().error('Other error message: %s', error.uiMessage) - this.messenger.sendErrorMessage('', tabID) - } - - // This function handles actions if user clicked on any Button one of these cases will be executed - private async handleFormActionClicked(data: any) { - const typedAction = MessengerUtils.stringToEnumValue(ButtonActions, data.action as any) - let getFeedbackCommentData = '' - switch (typedAction) { - case ButtonActions.STOP_TEST_GEN: - testGenState.setToCancelling() - telemetry.ui_click.emit({ elementId: 'unitTestGeneration_cancelTestGenerationProgress' }) - break - case ButtonActions.STOP_BUILD: - cancelBuild() - void this.handleUpdatePromptProgress({ status: 'cancel', tabID: data.tabID }) - telemetry.ui_click.emit({ elementId: 'unitTestGeneration_cancelBuildProgress' }) - this.messenger.sendChatInputEnabled(data.tabID, true) - await this.sessionCleanUp() - break - case ButtonActions.PROVIDE_FEEDBACK: - getFeedbackCommentData = `Q Test Generation: RequestId: ${this.sessionStorage.getSession().startTestGenerationRequestId}, TestGenerationJobId: ${this.sessionStorage.getSession().testGenerationJob?.testGenerationJobId}` - void submitFeedback(placeholder, 'Amazon Q', getFeedbackCommentData) - telemetry.ui_click.emit({ elementId: 'unitTestGeneration_provideFeedback' }) - break - } - } - // This function handles actions if user gives any input from the chatInput box - private async processHumanChatMessage(data: { prompt: string; tabID: string }) { - const session = this.sessionStorage.getSession() - const conversationState = session.conversationState - - if (conversationState === ConversationState.WAITING_FOR_BUILD_COMMMAND_INPUT) { - this.messenger.sendChatInputEnabled(data.tabID, false) - this.sessionStorage.getSession().conversationState = ConversationState.IDLE - session.updatedBuildCommands = [data.prompt] - const updatedCommands = session.updatedBuildCommands.join('\n') - this.messenger.sendMessage(`Updated command to \`${updatedCommands}\``, data.tabID, 'prompt') - await this.checkForInstallationDependencies(data) - return - } else { - await this.startTestGen(data, false) - } - } - // This function takes filePath as input parameter and returns file language - private async getLanguageForFilePath(filePath: string): Promise { - try { - const document = await vscode.workspace.openTextDocument(filePath) - return document.languageId - } catch (error) { - return 'plaintext' - } - } - - private getFeedbackButtons(): ChatItemButton[] { - const buttons: ChatItemButton[] = [] - if (Auth.instance.isInternalAmazonUser()) { - buttons.push({ - keepCardAfterClick: true, - text: 'How can we make /test better?', - id: ButtonActions.PROVIDE_FEEDBACK, - disabled: false, // allow button to be re-clicked - position: 'outside', - icon: 'comment' as MynahIcons, - }) - } - return buttons - } - - /** - * Start Test Generation and show the code results - */ - - private async startTestGen(message: any, regenerateTests: boolean) { - const session: Session = this.sessionStorage.getSession() - // Perform session cleanup before start of unit test generation workflow unless there is an existing job in progress. - if (!ChatSessionManager.Instance.getIsInProgress()) { - await this.sessionCleanUp() - } - const tabID = this.sessionStorage.setActiveTab(message.tabID) - getLogger().debug('startTestGen message: %O', message) - getLogger().debug('startTestGen tabId: %O', message.tabID) - let fileName = '' - let filePath = '' - let userFacingMessage = '' - let userPrompt = '' - session.testGenerationStartTime = performance.now() - - try { - if (ChatSessionManager.Instance.getIsInProgress()) { - void vscode.window.showInformationMessage( - "There is already a test generation job in progress. Cancel current job or wait until it's finished to try again." - ) - return - } - if (testGenState.isCancelling()) { - void vscode.window.showInformationMessage( - 'There is a test generation job being cancelled. Please wait for cancellation to finish.' - ) - return - } - // Truncating the user prompt if the prompt is more than 4096. - userPrompt = message.prompt.slice(0, maxUserPromptLength) - - // check that the session is authenticated - const authState = await AuthUtil.instance.getChatAuthState() - if (authState.amazonQ !== 'connected') { - void this.messenger.sendAuthNeededExceptionMessage(authState, tabID) - session.isAuthenticating = true - return - } - - // check that a project/workspace is open - const workspaceFolders = vscode.workspace.workspaceFolders - if (workspaceFolders === undefined || workspaceFolders.length === 0) { - this.messenger.sendUnrecoverableErrorResponse('no-project-found', tabID) - return - } - - // check if IDE has active file open. - const activeEditor = vscode.window.activeTextEditor - // also check all open editors and allow this to proceed if only one is open (even if not main focus) - const allVisibleEditors = vscode.window.visibleTextEditors - const openFileEditors = allVisibleEditors.filter((editor) => editor.document.uri.scheme === 'file') - const hasOnlyOneOpenFileSplitView = openFileEditors.length === 1 - getLogger().debug(`hasOnlyOneOpenSplitView: ${hasOnlyOneOpenFileSplitView}`) - // is not a file if the currently highlighted window is not a file, and there is either more than one or no file windows open - const isNotFile = activeEditor?.document.uri.scheme !== 'file' && !hasOnlyOneOpenFileSplitView - getLogger().debug(`activeEditor: ${activeEditor}, isNotFile: ${isNotFile}`) - if (!activeEditor || isNotFile) { - this.messenger.sendUnrecoverableErrorResponse( - isNotFile ? 'invalid-file-type' : 'no-open-file-found', - tabID - ) - this.messenger.sendUpdatePlaceholder( - tabID, - 'Please open and highlight a source code file in order to generate tests.' - ) - this.messenger.sendChatInputEnabled(tabID, true) - this.sessionStorage.getSession().conversationState = ConversationState.WAITING_FOR_INPUT - return - } - - const fileEditorToTest = hasOnlyOneOpenFileSplitView ? openFileEditors[0] : activeEditor - getLogger().debug(`File path: ${fileEditorToTest.document.uri.fsPath}`) - filePath = fileEditorToTest.document.uri.fsPath - fileName = path.basename(filePath) - userFacingMessage = userPrompt - ? regenerateTests - ? `${userPrompt}` - : `/test ${userPrompt}` - : `/test Generate unit tests for \`${fileName}\`` - - session.hasUserPromptSupplied = userPrompt.length > 0 - - // displaying user message prompt in Test tab - this.messenger.sendMessage(userFacingMessage, tabID, 'prompt') - this.messenger.sendChatInputEnabled(tabID, false) - this.sessionStorage.getSession().conversationState = ConversationState.IN_PROGRESS - this.messenger.sendUpdatePromptProgress(message.tabID, testGenProgressField) - - const language = await this.getLanguageForFilePath(filePath) - session.fileLanguage = language - const workspaceFolder = vscode.workspace.getWorkspaceFolder(fileEditorToTest.document.uri) - - /* - For Re:Invent 2024 we are supporting only java and python for unit test generation, rest of the languages shows the similar experience as CWC - */ - if (!['java', 'python'].includes(language) || workspaceFolder === undefined) { - if (!workspaceFolder) { - // File is outside of workspace - const unsupportedMessage = `I can't generate tests for ${fileName} because the file is outside of workspace scope.
I can still provide examples, instructions and code suggestions.` - this.messenger.sendMessage(unsupportedMessage, tabID, 'answer') - } - // Keeping this metric as is. TODO - Change to true once we support through other feature - session.isSupportedLanguage = false - await this.onCodeGeneration( - session, - userPrompt, - tabID, - fileName, - filePath, - workspaceFolder !== undefined - ) - } else { - this.messenger.sendCapabilityCard({ tabID }) - this.messenger.sendMessage(testGenSummaryMessage(fileName), message.tabID, 'answer-part') - - // Grab the selection from the fileEditorToTest and get the vscode Range - const selection = fileEditorToTest.selection - let selectionRange = undefined - if ( - selection.start.line !== selection.end.line || - selection.start.character !== selection.end.character - ) { - selectionRange = new vscode.Range( - selection.start.line, - selection.start.character, - selection.end.line, - selection.end.character - ) - } - session.isCodeBlockSelected = selectionRange !== undefined - session.isSupportedLanguage = true - - /** - * Zip the project - * Create pre-signed URL and upload artifact to S3 - * send API request to startTestGeneration API - * Poll from getTestGeneration API - * Get Diff from exportResultArchive API - */ - ChatSessionManager.Instance.setIsInProgress(true) - await startTestGenerationProcess(filePath, message.prompt, tabID, true, selectionRange) - } - } catch (err: any) { - // TODO: refactor error handling to be more robust - ChatSessionManager.Instance.setIsInProgress(false) - getLogger().error('startTestGen failed: %O', err) - this.messenger.sendUpdatePromptProgress(message.tabID, cancellingProgressField) - this.sendErrorMessage({ tabID, error: err }) - this.messenger.sendChatInputEnabled(tabID, true) - this.sessionStorage.getSession().conversationState = ConversationState.WAITING_FOR_INPUT - await sleep(2000) - // eslint-disable-next-line unicorn/no-null - this.messenger.sendUpdatePromptProgress(message.tabID, null) - } - } - - // Updating Progress bar - private async handleUpdatePromptProgress(data: any) { - const getProgressField = (status: string): ProgressField | null => { - switch (status) { - case 'Completed': - return testGenCompletedField - case 'Error': - return errorProgressField - case 'cancel': - return cancellingProgressField - case 'InProgress': - default: - return { - status: 'info', - text: 'Generating unit tests...', - value: data.progressRate, - valueText: data.progressRate.toString() + '%', - actions: [cancelTestGenButton], - } - } - } - this.messenger.sendUpdatePromptProgress(data.tabID, getProgressField(data.status)) - - await sleep(2000) - - // don't flash the bar when generation in progress - if (data.status !== 'InProgress') { - // eslint-disable-next-line unicorn/no-null - this.messenger.sendUpdatePromptProgress(data.tabID, null) - } - } - - private async updateTargetFileInfo(message: { - tabID: string - targetFileInfo?: TargetFileInfo - testGenerationJobGroupName: string - testGenerationJobId: string - type: ChatItemType - filePath: string - }) { - this.messenger.sendShortSummary({ - type: 'answer', - tabID: message.tabID, - message: testGenSummaryMessage( - path.basename(message.targetFileInfo?.filePath ?? message.filePath), - message.targetFileInfo?.filePlan?.replaceAll('```', '') - ), - canBeVoted: true, - filePath: message.targetFileInfo?.testFilePath, - }) - } - - private async showCodeGenerationResults(data: { tabID: string; filePath: string; projectName: string }) { - const session = this.sessionStorage.getSession() - // return early if references are disabled and there are references - if (!CodeWhispererSettings.instance.isSuggestionsWithCodeReferencesEnabled() && session.references.length > 0) { - void vscode.window.showInformationMessage('Your settings do not allow code generation with references.') - await this.endSession(data, FollowUpTypes.SkipBuildAndFinish) - await this.sessionCleanUp() - return - } - const followUps: FollowUps = { - text: '', - options: [ - { - pillText: `View diff`, - type: FollowUpTypes.ViewDiff, - status: 'primary', - }, - ], - } - session.generatedFilePath = data.filePath - try { - const tempFilePath = path.join(this.tempResultDirPath, 'resultArtifacts', data.filePath) - const newContent = await fs.readFileText(tempFilePath) - const workspaceFolder = vscode.workspace.workspaceFolders?.[0] - let linesGenerated = newContent.split('\n').length - let charsGenerated = newContent.length - if (workspaceFolder) { - const projectPath = workspaceFolder.uri.fsPath - const absolutePath = path.join(projectPath, data.filePath) - const fileExists = await fs.existsFile(absolutePath) - if (fileExists) { - const originalContent = await fs.readFileText(absolutePath) - linesGenerated -= originalContent.split('\n').length - charsGenerated -= originalContent.length - } - } - session.linesOfCodeGenerated = linesGenerated > 0 ? linesGenerated : 0 - session.charsOfCodeGenerated = charsGenerated > 0 ? charsGenerated : 0 - } catch (e: any) { - getLogger().debug('failed to get chars and lines of code generated from test generation result: %O', e) - } - - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer', - codeGenerationId: '', - message: `${session.jobSummary}\n\n Please see the unit tests generated below. Click “View diff” to review the changes in the code editor.`, - canBeVoted: true, - messageId: '', - followUps, - fileList: { - fileTreeTitle: 'READY FOR REVIEW', - rootFolderTitle: data.projectName, - filePaths: [data.filePath], - }, - codeReference: session.references.map( - (ref: Reference) => - ({ - ...ref, - information: `${ref.licenseName} - ${ref.repository}`, - }) as CodeReference - ), - }) - this.messenger.sendChatInputEnabled(data.tabID, false) - this.messenger.sendUpdatePlaceholder(data.tabID, `Select View diff to see the generated unit tests.`) - this.sessionStorage.getSession().conversationState = ConversationState.IDLE - } - - private async openDiff(message: OpenDiffMessage) { - const session = this.sessionStorage.getSession() - const filePath = session.generatedFilePath - const absolutePath = path.join(session.projectRootPath, filePath) - const fileExists = await fs.existsFile(absolutePath) - const leftUri = fileExists ? vscode.Uri.file(absolutePath) : vscode.Uri.from({ scheme: 'untitled' }) - const rightUri = vscode.Uri.file(path.join(this.tempResultDirPath, 'resultArtifacts', filePath)) - const fileName = path.basename(absolutePath) - await vscode.commands.executeCommand('vscode.diff', leftUri, rightUri, `${fileName} ${amazonQTabSuffix}`) - telemetry.ui_click.emit({ elementId: 'unitTestGeneration_viewDiff' }) - session.latencyOfTestGeneration = performance.now() - session.testGenerationStartTime - this.messenger.sendUpdatePlaceholder(message.tabID, `Please select an action to proceed (Accept or Reject)`) - } - - private async acceptCode(message: any) { - const session = this.sessionStorage.getSession() - session.acceptedJobId = session.listOfTestGenerationJobId[session.listOfTestGenerationJobId.length - 1] - const filePath = session.generatedFilePath - const absolutePath = path.join(session.projectRootPath, filePath) - const fileExists = await fs.existsFile(absolutePath) - const buildCommand = session.updatedBuildCommands?.join(' ') - - const tempFilePath = path.join(this.tempResultDirPath, 'resultArtifacts', filePath) - const updatedContent = await fs.readFileText(tempFilePath) - let acceptedLines = updatedContent.split('\n').length - let acceptedChars = updatedContent.length - if (fileExists) { - const originalContent = await fs.readFileText(absolutePath) - acceptedLines -= originalContent.split('\n').length - acceptedLines = acceptedLines < 0 ? 0 : acceptedLines - acceptedChars -= originalContent.length - acceptedChars = acceptedChars < 0 ? 0 : acceptedChars - UserWrittenCodeTracker.instance.onQStartsMakingEdits() - const document = await vscode.workspace.openTextDocument(absolutePath) - await applyChanges( - document, - new vscode.Range(document.lineAt(0).range.start, document.lineAt(document.lineCount - 1).range.end), - updatedContent - ) - UserWrittenCodeTracker.instance.onQFinishesEdits() - } else { - await fs.writeFile(absolutePath, updatedContent) - } - session.charsOfCodeAccepted = acceptedChars - session.linesOfCodeAccepted = acceptedLines - - // add accepted references to reference log, if any - const fileName = path.basename(session.generatedFilePath) - const time = new Date().toLocaleString() - // TODO: this is duplicated in basicCommands.ts for scan (codewhisperer). Fix this later. - for (const reference of session.references) { - getLogger().debug('Processing reference: %O', reference) - // Log values for debugging - getLogger().debug('updatedContent: %s', updatedContent) - getLogger().debug( - 'start: %d, end: %d', - reference.recommendationContentSpan?.start, - reference.recommendationContentSpan?.end - ) - // given a start and end index, figure out which line number they belong to when splitting a string on /n characters - const getLineNumber = (content: string, index: number): number => { - const lines = content.slice(0, index).split('\n') - return lines.length - } - const startLine = getLineNumber(updatedContent, reference.recommendationContentSpan!.start) - const endLine = getLineNumber(updatedContent, reference.recommendationContentSpan!.end) - getLogger().debug('startLine: %d, endLine: %d', startLine, endLine) - - const code = updatedContent.slice( - reference.recommendationContentSpan?.start, - reference.recommendationContentSpan?.end - ) - getLogger().debug('Extracted code slice: %s', code) - const referenceLog = - `[${time}] Accepted recommendation ` + - referenceLogText( - `
${code}
`, - reference.licenseName!, - reference.repository!, - fileName, - startLine === endLine ? `(line at ${startLine})` : `(lines from ${startLine} to ${endLine})` - ) + - '
' - getLogger().debug('Adding reference log: %s', referenceLog) - ReferenceLogViewProvider.instance.addReferenceLog(referenceLog) - } - - // TODO: see if there's a better way to check if active file is a diff - if (vscode.window.tabGroups.activeTabGroup.activeTab?.label.includes(amazonQTabSuffix)) { - await vscode.commands.executeCommand('workbench.action.closeActiveEditor') - } - const document = await vscode.workspace.openTextDocument(absolutePath) - await vscode.window.showTextDocument(document) - // TODO: send the message once again once build is enabled - // this.messenger.sendMessage('Accepted', message.tabID, 'prompt') - telemetry.ui_click.emit({ elementId: 'unitTestGeneration_acceptDiff' }) - - getLogger().info( - `Generated unit tests are accepted for ${session.fileLanguage ?? 'plaintext'} language with jobId: ${session.listOfTestGenerationJobId[0]}, jobGroupName: ${session.testGenerationJobGroupName}, result: Succeeded` - ) - TelemetryHelper.instance.sendTestGenerationToolkitEvent( - session, - true, - true, - 'Succeeded', - session.startTestGenerationRequestId, - session.latencyOfTestGeneration, - undefined, - session.isCodeBlockSelected, - session.artifactsUploadDuration, - session.srcPayloadSize, - session.srcZipFileSize, - session.charsOfCodeAccepted, - session.numberOfTestsGenerated, - session.linesOfCodeAccepted, - session.charsOfCodeGenerated, - session.numberOfTestsGenerated, - session.linesOfCodeGenerated, - undefined, - 'ACCEPTED' - ) - - await this.endSession(message, FollowUpTypes.SkipBuildAndFinish) - return - - if (session.listOfTestGenerationJobId.length === 1) { - this.startInitialBuild(message) - this.messenger.sendChatInputEnabled(message.tabID, false) - } else if (session.listOfTestGenerationJobId.length < 4) { - const remainingIterations = 4 - session.listOfTestGenerationJobId.length - - let userMessage = 'Would you like Amazon Q to build and execute again, and fix errors?' - if (buildCommand) { - userMessage += ` I will be running this build command: \`${buildCommand}\`` - } - userMessage += `\nYou have ${remainingIterations} iteration${remainingIterations > 1 ? 's' : ''} left.` - - const followUps: FollowUps = { - text: '', - options: [ - { - pillText: `Rebuild`, - type: FollowUpTypes.ContinueBuildAndExecute, - status: 'primary', - }, - { - pillText: `Skip and finish`, - type: FollowUpTypes.SkipBuildAndFinish, - status: 'primary', - }, - ], - } - this.messenger.sendBuildProgressMessage({ - tabID: message.tabID, - messageType: 'answer', - codeGenerationId: '', - message: userMessage, - canBeVoted: false, - messageId: '', - followUps: followUps, - }) - this.messenger.sendChatInputEnabled(message.tabID, false) - } else { - this.sessionStorage.getSession().listOfTestGenerationJobId = [] - this.messenger.sendMessage( - 'You have gone through both iterations and this unit test generation workflow is complete.', - message.tabID, - 'answer' - ) - await this.sessionCleanUp() - } - await fs.delete(this.tempResultDirPath, { recursive: true }) - } - - /** - * Handle a regular incoming message when a user is in the code generation phase - */ - private async onCodeGeneration( - session: Session, - message: string, - tabID: string, - fileName: string, - filePath: string, - fileInWorkspace: boolean - ) { - try { - // TODO: Write this entire gen response to basiccommands and call here. - const editorText = await fs.readFileText(filePath) - - const triggerPayload: TriggerPayload = { - query: `Generate unit tests for the following part of my code: ${message?.trim() || fileName}`, - codeSelection: undefined, - trigger: ChatTriggerType.ChatMessage, - fileText: editorText, - fileLanguage: session.fileLanguage, - filePath: filePath, - message: `Generate unit tests for the following part of my code: ${message?.trim() || fileName}`, - matchPolicy: undefined, - codeQuery: undefined, - userIntent: UserIntent.GENERATE_UNIT_TESTS, - customization: getSelectedCustomization(), - profile: AuthUtil.instance.regionProfileManager.activeRegionProfile, - context: [], - relevantTextDocuments: [], - additionalContents: [], - documentReferences: [], - useRelevantDocuments: false, - contextLengths: { - ...defaultContextLengths, - }, - } - const chatRequest = triggerPayloadToChatRequest(triggerPayload) - const client = await createCodeWhispererChatStreamingClient() - const response = await client.generateAssistantResponse(chatRequest) - UserWrittenCodeTracker.instance.onQFeatureInvoked() - await this.messenger.sendAIResponse( - response, - session, - tabID, - randomUUID.toString(), - triggerPayload, - fileName, - fileInWorkspace - ) - } finally { - this.messenger.sendChatInputEnabled(tabID, true) - this.messenger.sendUpdatePlaceholder(tabID, `/test Generate unit tests...`) - this.sessionStorage.getSession().conversationState = ConversationState.WAITING_FOR_INPUT - } - } - - // TODO: Check if there are more cases to endSession if yes create a enum or type for step - private async endSession(data: any, step: FollowUpTypes) { - this.messenger.sendMessage( - 'Unit test generation completed.', - data.tabID, - 'answer', - 'testGenEndSessionMessage', - this.getFeedbackButtons() - ) - - const session = this.sessionStorage.getSession() - if (step === FollowUpTypes.RejectCode) { - TelemetryHelper.instance.sendTestGenerationToolkitEvent( - session, - true, - true, - 'Succeeded', - session.startTestGenerationRequestId, - session.latencyOfTestGeneration, - undefined, - session.isCodeBlockSelected, - session.artifactsUploadDuration, - session.srcPayloadSize, - session.srcZipFileSize, - 0, - 0, - 0, - session.charsOfCodeGenerated, - session.numberOfTestsGenerated, - session.linesOfCodeGenerated, - undefined, - 'REJECTED' - ) - telemetry.ui_click.emit({ elementId: 'unitTestGeneration_rejectDiff' }) - } - - await this.sessionCleanUp() - - // this.messenger.sendMessage(`Unit test generation workflow is completed.`, data.tabID, 'answer') - this.messenger.sendChatInputEnabled(data.tabID, true) - return - } - - /** - * BUILD LOOP IMPLEMENTATION - */ - - private startInitialBuild(data: any) { - // TODO: Remove the fallback build command after stable version of backend build command. - const userMessage = `Would you like me to help build and execute the test? I will need you to let me know what build command to run if you do.` - const followUps: FollowUps = { - text: '', - options: [ - { - pillText: `Specify command then build and execute`, - type: FollowUpTypes.ModifyCommands, - status: 'primary', - }, - { - pillText: `Skip and finish`, - type: FollowUpTypes.SkipBuildAndFinish, - status: 'primary', - }, - ], - } - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer', - codeGenerationId: '', - message: userMessage, - canBeVoted: false, - messageId: '', - followUps: followUps, - }) - this.messenger.sendChatInputEnabled(data.tabID, false) - } - - private async checkForInstallationDependencies(data: any) { - // const session: Session = this.sessionStorage.getSession() - // const listOfInstallationDependencies = session.testGenerationJob?.shortAnswer?.installationDependencies || [] - // MOCK: As there is no installation dependencies in shortAnswer - const listOfInstallationDependencies = [''] - const installationDependencies = listOfInstallationDependencies.join('\n') - - this.messenger.sendMessage('Build and execute', data.tabID, 'prompt') - telemetry.ui_click.emit({ elementId: 'unitTestGeneration_buildAndExecute' }) - - if (installationDependencies.length > 0) { - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer', - codeGenerationId: '', - message: `Looks like you don’t have ${listOfInstallationDependencies.length > 1 ? `these` : `this`} ${listOfInstallationDependencies.length} required package${listOfInstallationDependencies.length > 1 ? `s` : ``} installed.\n\`\`\`sh\n${installationDependencies}\n`, - canBeVoted: false, - messageId: '', - followUps: { - text: '', - options: [ - { - pillText: `Install and continue`, - type: FollowUpTypes.InstallDependenciesAndContinue, - status: 'primary', - }, - { - pillText: `Skip and finish`, - type: FollowUpTypes.SkipBuildAndFinish, - status: 'primary', - }, - ], - }, - }) - } else { - await this.startLocalBuildExecution(data) - } - } - - private async handleInstallDependencies(data: any) { - this.messenger.sendMessage('Installation dependencies and continue', data.tabID, 'prompt') - telemetry.ui_click.emit({ elementId: 'unitTestGeneration_installDependenciesAndContinue' }) - void this.startLocalBuildExecution(data) - } - - private async handleBuildIteration(data: any) { - this.messenger.sendMessage('Proceed with Iteration', data.tabID, 'prompt') - telemetry.ui_click.emit({ elementId: 'unitTestGeneration_proceedWithIteration' }) - await this.startLocalBuildExecution(data) - } - - private async startLocalBuildExecution(data: any) { - const session: Session = this.sessionStorage.getSession() - // const installationDependencies = session.shortAnswer?.installationDependencies ?? [] - // MOCK: ignoring the installation case until backend send response - const installationDependencies: string[] = [] - const buildCommands = session.updatedBuildCommands - if (!buildCommands) { - throw new Error('Build command not found') - return - } - - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.START_STEP), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - - this.messenger.sendUpdatePromptProgress(data.tabID, buildProgressField) - - if (installationDependencies.length > 0 && session.listOfTestGenerationJobId.length < 2) { - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.INSTALL_DEPENDENCIES, 'current'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - - const status = await runBuildCommand(installationDependencies) - // TODO: Add separate status for installation dependencies - session.buildStatus = status - if (status === BuildStatus.FAILURE) { - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.INSTALL_DEPENDENCIES, 'error'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - } - if (status === BuildStatus.CANCELLED) { - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.INSTALL_DEPENDENCIES, 'error'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - this.messenger.sendMessage('Installation dependencies Cancelled', data.tabID, 'prompt') - this.messenger.sendMessage( - 'Unit test generation workflow is complete. You have 25 out of 30 Amazon Q Developer Agent invocations left this month.', - data.tabID, - 'answer' - ) - return - } - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.INSTALL_DEPENDENCIES, 'done'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - } - - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.RUN_BUILD, 'current'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - - const buildStatus = await runBuildCommand(buildCommands) - session.buildStatus = buildStatus - - if (buildStatus === BuildStatus.FAILURE) { - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.RUN_BUILD, 'error'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - } else if (buildStatus === BuildStatus.CANCELLED) { - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.RUN_BUILD, 'error'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - this.messenger.sendMessage('Build Cancelled', data.tabID, 'prompt') - this.messenger.sendMessage('Unit test generation workflow is complete.', data.tabID, 'answer') - return - } else { - // Build successful - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.RUN_BUILD, 'done'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - } - - // Running execution tests - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.RUN_EXECUTION_TESTS, 'current'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - // After running tests - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.RUN_EXECUTION_TESTS, 'done'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - if (session.buildStatus !== BuildStatus.SUCCESS) { - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.FIXING_TEST_CASES, 'current'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - await startTestGenerationProcess(session.sourceFilePath, '', data.tabID, false) - } - // TODO: Skip this if startTestGenerationProcess timeouts - if (session.generatedFilePath) { - await this.showTestCaseSummary(data) - } - } - - private async showTestCaseSummary(data: { tabID: string }) { - const session: Session = this.sessionStorage.getSession() - let codeDiffLength = 0 - if (session.buildStatus !== BuildStatus.SUCCESS) { - // Check the generated test file content, if fileContent length is 0, exit the unit test generation workflow. - const tempFilePath = path.join(this.tempResultDirPath, 'resultArtifacts', session.generatedFilePath) - const codeDiffFileContent = await fs.readFileText(tempFilePath) - codeDiffLength = codeDiffFileContent.length - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.FIXING_TEST_CASES + 1, 'done'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - } - - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.PROCESS_TEST_RESULTS, 'current'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.PROCESS_TEST_RESULTS, 'done'), - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - }) - - const followUps: FollowUps = { - text: '', - options: [ - { - pillText: `View diff`, - type: FollowUpTypes.ViewCodeDiffAfterIteration, - status: 'primary', - }, - ], - } - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer-part', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: testGenBuildProgressMessage(TestGenerationBuildStep.PROCESS_TEST_RESULTS + 1), - canBeVoted: true, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - followUps: undefined, - fileList: this.checkCodeDiffLengthAndBuildStatus({ codeDiffLength, buildStatus: session.buildStatus }) - ? { - fileTreeTitle: 'READY FOR REVIEW', - rootFolderTitle: 'tests', - filePaths: [session.generatedFilePath], - } - : undefined, - }) - this.messenger.sendBuildProgressMessage({ - tabID: data.tabID, - messageType: 'answer', - codeGenerationId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - message: undefined, - canBeVoted: false, - messageId: TestNamedMessages.TEST_GENERATION_BUILD_STATUS_MESSAGE, - followUps: this.checkCodeDiffLengthAndBuildStatus({ codeDiffLength, buildStatus: session.buildStatus }) - ? followUps - : undefined, - fileList: undefined, - }) - - this.messenger.sendUpdatePromptProgress(data.tabID, testGenCompletedField) - await sleep(2000) - // eslint-disable-next-line unicorn/no-null - this.messenger.sendUpdatePromptProgress(data.tabID, null) - this.messenger.sendChatInputEnabled(data.tabID, false) - - if (codeDiffLength === 0 || session.buildStatus === BuildStatus.SUCCESS) { - this.messenger.sendMessage('Unit test generation workflow is complete.', data.tabID, 'answer') - await this.sessionCleanUp() - } - } - - private modifyBuildCommand(data: any) { - this.sessionStorage.getSession().conversationState = ConversationState.WAITING_FOR_BUILD_COMMMAND_INPUT - this.messenger.sendMessage('Specify commands then build', data.tabID, 'prompt') - telemetry.ui_click.emit({ elementId: 'unitTestGeneration_modifyCommand' }) - this.messenger.sendMessage( - 'Sure, provide all command lines you’d like me to run to build.', - data.tabID, - 'answer' - ) - this.messenger.sendUpdatePlaceholder(data.tabID, 'Waiting on your Inputs') - this.messenger.sendChatInputEnabled(data.tabID, true) - } - - /** Perform Session CleanUp in below cases - * UTG success - * End Session with Reject or SkipAndFinish - * After finishing 3 build loop iterations - * Error while generating unit tests - * Closing a Q-Test tab - * Progress bar cancel - */ - private async sessionCleanUp() { - const session = this.sessionStorage.getSession() - const groupName = session.testGenerationJobGroupName - const filePath = session.generatedFilePath - getLogger().debug('Entering sessionCleanUp function with filePath: %s and groupName: %s', filePath, groupName) - - vscode.window.tabGroups.all.flatMap(({ tabs }) => - tabs.map((tab) => { - if (tab.label === `${path.basename(filePath)} ${amazonQTabSuffix}`) { - const tabClosed = vscode.window.tabGroups.close(tab) - if (!tabClosed) { - getLogger().error('ChatDiff: Unable to close the diff view tab for %s', tab.label) - } - } - }) - ) - - getLogger().debug( - 'listOfTestGenerationJobId length: %d, groupName: %s', - session.listOfTestGenerationJobId.length, - groupName - ) - if (session.listOfTestGenerationJobId.length && groupName) { - for (const id of session.listOfTestGenerationJobId) { - if (id === session.acceptedJobId) { - TelemetryHelper.instance.sendTestGenerationEvent( - groupName, - id, - session.fileLanguage, - session.numberOfTestsGenerated, - session.numberOfTestsGenerated, // this is number of accepted test cases, now they can only accept all - session.linesOfCodeGenerated, - session.linesOfCodeAccepted, - session.charsOfCodeGenerated, - session.charsOfCodeAccepted - ) - } else { - TelemetryHelper.instance.sendTestGenerationEvent( - groupName, - id, - session.fileLanguage, - session.numberOfTestsGenerated, - 0, - session.linesOfCodeGenerated, - 0, - session.charsOfCodeGenerated, - 0 - ) - } - } - } - session.listOfTestGenerationJobId = [] - session.testGenerationJobGroupName = undefined - // session.testGenerationJob = undefined - session.updatedBuildCommands = undefined - session.shortAnswer = undefined - session.testCoveragePercentage = 0 - session.conversationState = ConversationState.IDLE - session.sourceFilePath = '' - session.generatedFilePath = '' - session.projectRootPath = '' - session.stopIteration = false - session.fileLanguage = undefined - ChatSessionManager.Instance.setIsInProgress(false) - session.linesOfCodeGenerated = 0 - session.linesOfCodeAccepted = 0 - session.charsOfCodeGenerated = 0 - session.charsOfCodeAccepted = 0 - session.acceptedJobId = '' - session.numberOfTestsGenerated = 0 - if (session.tabID) { - getLogger().debug('Setting input state with tabID: %s', session.tabID) - this.messenger.sendChatInputEnabled(session.tabID, true) - this.messenger.sendUpdatePlaceholder(session.tabID, 'Enter "/" for quick actions') - } - getLogger().debug( - 'Deleting output.log and temp result directory. testGenerationLogsDir: %s', - testGenerationLogsDir - ) - const outputLogPath = path.join(testGenerationLogsDir, 'output.log') - if (await fs.existsFile(outputLogPath)) { - await fs.delete(outputLogPath) - } - if ( - await fs - .stat(this.tempResultDirPath) - .then(() => true) - .catch(() => false) - ) { - await fs.delete(this.tempResultDirPath, { recursive: true }) - } - } - - // TODO: return build command when product approves - // private getBuildCommands = (): string[] => { - // const session = this.sessionStorage.getSession() - // if (session.updatedBuildCommands?.length) { - // return [...session.updatedBuildCommands] - // } - - // // For Internal amazon users only - // if (Auth.instance.isInternalAmazonUser()) { - // return ['brazil-build release'] - // } - - // if (session.shortAnswer && Array.isArray(session.shortAnswer?.buildCommands)) { - // return [...session.shortAnswer.buildCommands] - // } - - // return ['source qdev-wbr/.venv/bin/activate && pytest --continue-on-collection-errors'] - // } -} diff --git a/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts b/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts deleted file mode 100644 index 5541ef389c5..00000000000 --- a/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts +++ /dev/null @@ -1,365 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * This class controls the presentation of the various chat bubbles presented by the - * Q Test. - * - * As much as possible, all strings used in the experience should originate here. - */ - -import { AuthFollowUpType, AuthMessageDataMap } from '../../../../amazonq/auth/model' -import { FeatureAuthState } from '../../../../codewhisperer/util/authUtil' -import { - AppToWebViewMessageDispatcher, - AuthNeededException, - AuthenticationUpdateMessage, - BuildProgressMessage, - CapabilityCardMessage, - ChatInputEnabledMessage, - ChatMessage, - ChatSummaryMessage, - ErrorMessage, - UpdatePlaceholderMessage, - UpdatePromptProgressMessage, -} from '../../views/connector/connector' -import { ChatItemType } from '../../../../amazonq/commons/model' -import { ChatItemAction, ChatItemButton, ProgressField } from '@aws/mynah-ui' -import * as CodeWhispererConstants from '../../../../codewhisperer/models/constants' -import { TriggerPayload } from '../../../../codewhispererChat/controllers/chat/model' -import { - CodeWhispererStreamingServiceException, - GenerateAssistantResponseCommandOutput, -} from '@amzn/codewhisperer-streaming' -import { Session } from '../../session/session' -import { CodeReference } from '../../../../amazonq/webview/ui/apps/amazonqCommonsConnector' -import { getHttpStatusCode, getRequestId, getTelemetryReasonDesc, ToolkitError } from '../../../../shared/errors' -import { sleep, waitUntil } from '../../../../shared/utilities/timeoutUtils' -import { keys } from '../../../../shared/utilities/tsUtils' -import { cancellingProgressField, testGenCompletedField } from '../../../models/constants' -import { testGenState } from '../../../../codewhisperer/models/model' -import { TelemetryHelper } from '../../../../codewhisperer/util/telemetryHelper' - -export type UnrecoverableErrorType = 'no-project-found' | 'no-open-file-found' | 'invalid-file-type' - -export enum TestNamedMessages { - TEST_GENERATION_BUILD_STATUS_MESSAGE = 'testGenerationBuildStatusMessage', -} - -export interface FollowUps { - text?: string - options?: ChatItemAction[] -} - -export interface FileList { - fileTreeTitle?: string - rootFolderTitle?: string - filePaths?: string[] -} - -export interface SendBuildProgressMessageParams { - tabID: string - messageType: ChatItemType - codeGenerationId: string - message?: string - canBeVoted: boolean - messageId?: string - followUps?: FollowUps - fileList?: FileList - codeReference?: CodeReference[] -} - -export class Messenger { - public constructor(private readonly dispatcher: AppToWebViewMessageDispatcher) {} - - public sendCapabilityCard(params: { tabID: string }) { - this.dispatcher.sendChatMessage(new CapabilityCardMessage(params.tabID)) - } - - public sendMessage( - message: string, - tabID: string, - messageType: ChatItemType, - messageId?: string, - buttons?: ChatItemButton[] - ) { - this.dispatcher.sendChatMessage( - new ChatMessage({ message, messageType, messageId: messageId, buttons: buttons }, tabID) - ) - } - - public sendShortSummary(params: { - message?: string - type: ChatItemType - tabID: string - messageID?: string - canBeVoted?: boolean - filePath?: string - }) { - this.dispatcher.sendChatSummaryMessage( - new ChatSummaryMessage( - { - message: params.message, - messageType: params.type, - messageId: params.messageID, - canBeVoted: params.canBeVoted, - filePath: params.filePath, - }, - params.tabID - ) - ) - } - - public sendChatInputEnabled(tabID: string, enabled: boolean) { - this.dispatcher.sendChatInputEnabled(new ChatInputEnabledMessage(tabID, enabled)) - } - - public sendUpdatePlaceholder(tabID: string, newPlaceholder: string) { - this.dispatcher.sendUpdatePlaceholder(new UpdatePlaceholderMessage(tabID, newPlaceholder)) - } - - public sendUpdatePromptProgress(tabID: string, progressField: ProgressField | null) { - this.dispatcher.sendUpdatePromptProgress(new UpdatePromptProgressMessage(tabID, progressField)) - } - - public async sendAuthNeededExceptionMessage(credentialState: FeatureAuthState, tabID: string) { - let authType: AuthFollowUpType = 'full-auth' - let message = AuthMessageDataMap[authType].message - - switch (credentialState.amazonQ) { - case 'disconnected': - authType = 'full-auth' - message = AuthMessageDataMap[authType].message - break - case 'unsupported': - authType = 'use-supported-auth' - message = AuthMessageDataMap[authType].message - break - case 'expired': - authType = 're-auth' - message = AuthMessageDataMap[authType].message - break - } - - this.dispatcher.sendAuthNeededExceptionMessage(new AuthNeededException(message, authType, tabID)) - } - - public sendAuthenticationUpdate(testEnabled: boolean, authenticatingTabIDs: string[]) { - this.dispatcher.sendAuthenticationUpdate(new AuthenticationUpdateMessage(testEnabled, authenticatingTabIDs)) - } - - /** - * This method renders an error message with a button at the end that will try the - * transformation again from the beginning. This message is meant for errors that are - * completely unrecoverable: the job cannot be completed in its current state, - * and the flow must be tried again. - */ - public sendUnrecoverableErrorResponse(type: UnrecoverableErrorType, tabID: string) { - let message = '...' - switch (type) { - case 'no-project-found': - message = CodeWhispererConstants.noOpenProjectsFoundChatTestGenMessage - break - case 'no-open-file-found': - message = CodeWhispererConstants.noOpenFileFoundChatMessage - break - case 'invalid-file-type': - message = CodeWhispererConstants.invalidFileTypeChatMessage - break - } - this.sendMessage(message, tabID, 'answer-stream') - } - - public sendErrorMessage(errorMessage: string, tabID: string) { - this.dispatcher.sendErrorMessage( - new ErrorMessage(CodeWhispererConstants.genericErrorMessage, errorMessage, tabID) - ) - } - - // To show the response of unsupported languages to the user in the Q-Test tab - public async sendAIResponse( - response: GenerateAssistantResponseCommandOutput, - session: Session, - tabID: string, - triggerID: string, - triggerPayload: TriggerPayload, - fileName: string, - fileInWorkspace: boolean - ) { - let message = '' - let messageId = response.$metadata.requestId ?? '' - let codeReference: CodeReference[] = [] - - if (response.generateAssistantResponseResponse === undefined) { - throw new ToolkitError( - `Empty response from Q Developer service. Request ID: ${response.$metadata.requestId}` - ) - } - - const eventCounts = new Map() - waitUntil( - async () => { - for await (const chatEvent of response.generateAssistantResponseResponse!) { - for (const key of keys(chatEvent)) { - if ((chatEvent[key] as any) !== undefined) { - eventCounts.set(key, (eventCounts.get(key) ?? 0) + 1) - } - } - - if ( - chatEvent.codeReferenceEvent?.references !== undefined && - chatEvent.codeReferenceEvent.references.length > 0 - ) { - codeReference = [ - ...codeReference, - ...chatEvent.codeReferenceEvent.references.map((reference) => ({ - ...reference, - recommendationContentSpan: { - start: reference.recommendationContentSpan?.start ?? 0, - end: reference.recommendationContentSpan?.end ?? 0, - }, - information: `Reference code under **${reference.licenseName}** license from repository \`${reference.repository}\``, - })), - ] - } - if (testGenState.isCancelling()) { - return true - } - if ( - chatEvent.assistantResponseEvent?.content !== undefined && - chatEvent.assistantResponseEvent.content.length > 0 - ) { - message += chatEvent.assistantResponseEvent.content - this.dispatcher.sendBuildProgressMessage( - new BuildProgressMessage({ - tabID, - messageType: 'answer-part', - codeGenerationId: '', - message, - canBeVoted: false, - messageId, - followUps: undefined, - fileList: undefined, - }) - ) - } - } - return true - }, - { timeout: 60000, truthy: true } - ) - .catch((error: any) => { - let errorMessage = 'Error reading chat stream.' - let statusCode = undefined - let requestID = undefined - if (error instanceof CodeWhispererStreamingServiceException) { - errorMessage = error.message - statusCode = getHttpStatusCode(error) ?? 0 - requestID = getRequestId(error) - } - let message = 'This error is reported to the team automatically. Please try sending your message again.' - if (errorMessage !== undefined) { - message += `\n\nDetails: ${errorMessage}` - } - - if (statusCode !== undefined) { - message += `\n\nStatus Code: ${statusCode}` - } - - if (requestID !== undefined) { - messageId = requestID - message += `\n\nRequest ID: ${requestID}` - } - this.sendMessage(message.trim(), tabID, 'answer') - }) - .finally(async () => { - if (testGenState.isCancelling()) { - this.sendMessage(CodeWhispererConstants.unitTestGenerationCancelMessage, tabID, 'answer') - TelemetryHelper.instance.sendTestGenerationToolkitEvent( - session, - false, - fileInWorkspace, - 'Cancelled', - messageId, - performance.now() - session.testGenerationStartTime, - getTelemetryReasonDesc( - `TestGenCancelled: ${CodeWhispererConstants.unitTestGenerationCancelMessage}` - ), - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - 'TestGenCancelled', - 'CANCELLED' - ) - this.dispatcher.sendUpdatePromptProgress( - new UpdatePromptProgressMessage(tabID, cancellingProgressField) - ) - await sleep(500) - } else { - TelemetryHelper.instance.sendTestGenerationToolkitEvent( - session, - false, - fileInWorkspace, - 'Succeeded', - messageId, - performance.now() - session.testGenerationStartTime, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - 'ACCEPTED' - ) - this.dispatcher.sendUpdatePromptProgress( - new UpdatePromptProgressMessage(tabID, testGenCompletedField) - ) - await sleep(500) - } - testGenState.setToNotStarted() - // eslint-disable-next-line unicorn/no-null - this.dispatcher.sendUpdatePromptProgress(new UpdatePromptProgressMessage(tabID, null)) - }) - } - - // To show the Build progress in the chat - public sendBuildProgressMessage(params: SendBuildProgressMessageParams) { - const { - tabID, - messageType, - codeGenerationId, - message, - canBeVoted, - messageId, - followUps, - fileList, - codeReference, - } = params - this.dispatcher.sendBuildProgressMessage( - new BuildProgressMessage({ - tabID, - messageType, - codeGenerationId, - message, - canBeVoted, - messageId, - followUps, - fileList, - codeReference, - }) - ) - } -} diff --git a/packages/core/src/amazonqTest/chat/controller/messenger/messengerUtils.ts b/packages/core/src/amazonqTest/chat/controller/messenger/messengerUtils.ts deleted file mode 100644 index 1eecc0aa4cd..00000000000 --- a/packages/core/src/amazonqTest/chat/controller/messenger/messengerUtils.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - * - */ - -// These enums map to string IDs -export enum ButtonActions { - ACCEPT = 'Accept', - MODIFY = 'Modify', - REJECT = 'Reject', - VIEW_DIFF = 'View-Diff', - STOP_TEST_GEN = 'Stop-Test-Generation', - STOP_BUILD = 'Stop-Build-Process', - PROVIDE_FEEDBACK = 'Provide-Feedback', -} - -// TODO: Refactor the common functionality between Transform, FeatureDev, CWSPRChat, Scan and UTG to a new Folder. - -export default class MessengerUtils { - static stringToEnumValue = ( - enumObject: T, - value: `${T[K]}` - ): T[K] => { - if (Object.values(enumObject).includes(value)) { - return value as unknown as T[K] - } else { - throw new Error('Value provided was not found in Enum') - } - } -} diff --git a/packages/core/src/amazonqTest/chat/session/session.ts b/packages/core/src/amazonqTest/chat/session/session.ts deleted file mode 100644 index 4e3780e6f99..00000000000 --- a/packages/core/src/amazonqTest/chat/session/session.ts +++ /dev/null @@ -1,77 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ShortAnswer, Reference } from '../../../codewhisperer/models/model' -import { TargetFileInfo, TestGenerationJob } from '../../../codewhisperer/client/codewhispereruserclient' - -export enum ConversationState { - IDLE, - JOB_SUBMITTED, - WAITING_FOR_INPUT, - WAITING_FOR_BUILD_COMMMAND_INPUT, - WAITING_FOR_REGENERATE_INPUT, - IN_PROGRESS, -} - -export enum BuildStatus { - SUCCESS, - FAILURE, - CANCELLED, -} - -export class Session { - // Used to keep track of whether or not the current session is currently authenticating/needs authenticating - public isAuthenticating: boolean = false - - // A tab may or may not be currently open - public tabID: string | undefined - - // This is unique per each test generation cycle - public testGenerationJobGroupName: string | undefined = undefined - public listOfTestGenerationJobId: string[] = [] - public startTestGenerationRequestId: string | undefined = undefined - public testGenerationJob: TestGenerationJob | undefined - - // Start Test generation - public isSupportedLanguage: boolean = false - public conversationState: ConversationState = ConversationState.IDLE - public shortAnswer: ShortAnswer | undefined - public sourceFilePath: string = '' - public generatedFilePath: string = '' - public projectRootPath: string = '' - public fileLanguage: string | undefined = 'plaintext' - public stopIteration: boolean = false - public targetFileInfo: TargetFileInfo | undefined - public jobSummary: string = '' - - // Telemetry - public testGenerationStartTime: number = 0 - public hasUserPromptSupplied: boolean = false - public isCodeBlockSelected: boolean = false - public srcPayloadSize: number = 0 - public srcZipFileSize: number = 0 - public artifactsUploadDuration: number = 0 - public numberOfTestsGenerated: number = 0 - public linesOfCodeGenerated: number = 0 - public linesOfCodeAccepted: number = 0 - public charsOfCodeGenerated: number = 0 - public charsOfCodeAccepted: number = 0 - public latencyOfTestGeneration: number = 0 - - // TODO: Take values from ShortAnswer or TestGenerationJob - // Build loop - public buildStatus: BuildStatus = BuildStatus.SUCCESS - public updatedBuildCommands: string[] | undefined = undefined - public testCoveragePercentage: number = 90 - public isInProgress: boolean = false - public acceptedJobId = '' - public references: Reference[] = [] - - constructor() {} - - public isTabOpen(): boolean { - return this.tabID !== undefined - } -} diff --git a/packages/core/src/amazonqTest/chat/storages/chatSession.ts b/packages/core/src/amazonqTest/chat/storages/chatSession.ts deleted file mode 100644 index a8a3ccf429d..00000000000 --- a/packages/core/src/amazonqTest/chat/storages/chatSession.ts +++ /dev/null @@ -1,61 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - * - */ - -import { Session } from '../session/session' -import { getLogger } from '../../../shared/logger/logger' - -export class SessionNotFoundError extends Error {} - -export class ChatSessionManager { - private static _instance: ChatSessionManager - private activeSession: Session | undefined - private isInProgress: boolean = false - - constructor() {} - - public static get Instance() { - return this._instance || (this._instance = new this()) - } - - private createSession(): Session { - this.activeSession = new Session() - return this.activeSession - } - - public getSession(): Session { - if (this.activeSession === undefined) { - return this.createSession() - } - - return this.activeSession - } - - public getIsInProgress(): boolean { - return this.isInProgress - } - - public setIsInProgress(value: boolean): void { - this.isInProgress = value - } - - public setActiveTab(tabID: string): string { - getLogger().debug(`Setting active tab: ${tabID}, activeSession: ${this.activeSession}`) - if (this.activeSession !== undefined) { - this.activeSession.tabID = tabID - return tabID - } - - throw new SessionNotFoundError() - } - - public removeActiveTab(): void { - getLogger().debug(`Removing active tab and deleting activeSession: ${this.activeSession}`) - if (this.activeSession !== undefined) { - this.activeSession.tabID = undefined - this.activeSession = undefined - } - } -} diff --git a/packages/core/src/amazonqTest/chat/views/actions/uiMessageListener.ts b/packages/core/src/amazonqTest/chat/views/actions/uiMessageListener.ts deleted file mode 100644 index e44c002cdf9..00000000000 --- a/packages/core/src/amazonqTest/chat/views/actions/uiMessageListener.ts +++ /dev/null @@ -1,161 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { MessageListener } from '../../../../amazonq/messages/messageListener' -import { ExtensionMessage } from '../../../../amazonq/webview/ui/commands' -import { TestChatControllerEventEmitters } from '../../controller/controller' - -type UIMessage = ExtensionMessage & { - tabID?: string -} - -export interface UIMessageListenerProps { - readonly chatControllerEventEmitters: TestChatControllerEventEmitters - readonly webViewMessageListener: MessageListener -} - -export class UIMessageListener { - private testControllerEventsEmitters: TestChatControllerEventEmitters | undefined - private webViewMessageListener: MessageListener - - constructor(props: UIMessageListenerProps) { - this.testControllerEventsEmitters = props.chatControllerEventEmitters - this.webViewMessageListener = props.webViewMessageListener - - // Now we are listening to events that get sent from amazonq/webview/actions/actionListener (e.g. the tab) - this.webViewMessageListener.onMessage((msg) => { - this.handleMessage(msg) - }) - } - - private handleMessage(msg: ExtensionMessage) { - switch (msg.command) { - case 'new-tab-was-created': - this.tabOpened(msg) - break - case 'tab-was-removed': - this.tabClosed(msg) - break - case 'auth-follow-up-was-clicked': - this.authClicked(msg) - break - case 'start-test-gen': - this.startTestGen(msg) - break - case 'chat-prompt': - this.processChatPrompt(msg) - break - case 'form-action-click': - this.formActionClicked(msg) - break - case 'follow-up-was-clicked': - this.followUpClicked(msg) - break - case 'open-diff': - this.openDiff(msg) - break - case 'insert_code_at_cursor_position': - this.insertCodeAtCursorPosition(msg) - break - case 'response-body-link-click': - this.processResponseBodyLinkClick(msg) - break - case 'chat-item-voted': - this.chatItemVoted(msg) - break - case 'chat-item-feedback': - this.chatItemFeedback(msg) - break - } - } - - private tabOpened(msg: UIMessage) { - this.testControllerEventsEmitters?.tabOpened.fire({ - tabID: msg.tabID, - }) - } - - private tabClosed(msg: UIMessage) { - this.testControllerEventsEmitters?.tabClosed.fire({ - tabID: msg.tabID, - }) - } - - private authClicked(msg: UIMessage) { - this.testControllerEventsEmitters?.authClicked.fire({ - tabID: msg.tabID, - authType: msg.authType, - }) - } - - private startTestGen(msg: UIMessage) { - this.testControllerEventsEmitters?.startTestGen.fire({ - tabID: msg.tabID, - prompt: msg.prompt, - }) - } - - // Takes user input from chat input box. - private processChatPrompt(msg: UIMessage) { - this.testControllerEventsEmitters?.processHumanChatMessage.fire({ - prompt: msg.chatMessage, - tabID: msg.tabID, - }) - } - - private formActionClicked(msg: UIMessage) { - this.testControllerEventsEmitters?.formActionClicked.fire({ - ...msg, - }) - } - - private followUpClicked(msg: any) { - this.testControllerEventsEmitters?.followUpClicked.fire({ - followUp: msg.followUp, - tabID: msg.tabID, - }) - } - - private openDiff(msg: any) { - this.testControllerEventsEmitters?.openDiff.fire({ - tabID: msg.tabID, - filePath: msg.filePath, - deleted: msg.deleted, - messageId: msg.messageId, - }) - } - - private insertCodeAtCursorPosition(msg: any) { - this.testControllerEventsEmitters?.insertCodeAtCursorPosition.fire({ - command: msg.command, - messageId: msg.messageId, - tabID: msg.tabID, - code: msg.code, - insertionTargetType: msg.insertionTargetType, - codeReference: msg.codeReference, - }) - } - - private processResponseBodyLinkClick(msg: UIMessage) { - this.testControllerEventsEmitters?.processResponseBodyLinkClick.fire({ - command: msg.command, - messageId: msg.messageId, - tabID: msg.tabID, - link: msg.link, - }) - } - - private chatItemVoted(msg: any) { - this.testControllerEventsEmitters?.processChatItemVotedMessage.fire({ - tabID: msg.tabID, - command: msg.command, - vote: msg.vote, - }) - } - - private chatItemFeedback(msg: any) { - this.testControllerEventsEmitters?.processChatItemFeedbackMessage.fire(msg) - } -} diff --git a/packages/core/src/amazonqTest/chat/views/connector/connector.ts b/packages/core/src/amazonqTest/chat/views/connector/connector.ts deleted file mode 100644 index 86c7b446b97..00000000000 --- a/packages/core/src/amazonqTest/chat/views/connector/connector.ts +++ /dev/null @@ -1,256 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { AuthFollowUpType } from '../../../../amazonq/auth/model' -import { MessagePublisher } from '../../../../amazonq/messages/messagePublisher' -import { ChatItemAction, ChatItemButton, ProgressField, ChatItemContent } from '@aws/mynah-ui/dist/static' -import { ChatItemType } from '../../../../amazonq/commons/model' -import { testChat } from '../../../models/constants' -import { MynahIcons } from '@aws/mynah-ui' -import { SendBuildProgressMessageParams } from '../../controller/messenger/messenger' -import { CodeReference } from '../../../../codewhispererChat/view/connector/connector' - -class UiMessage { - readonly time: number = Date.now() - readonly sender: string = testChat - readonly type: TestMessageType = 'chatMessage' - readonly status: string = 'info' - - public constructor(protected tabID: string) {} -} - -export type TestMessageType = - | 'authenticationUpdateMessage' - | 'authNeededException' - | 'chatMessage' - | 'chatInputEnabledMessage' - | 'updatePlaceholderMessage' - | 'errorMessage' - | 'updatePromptProgress' - | 'chatSummaryMessage' - | 'buildProgressMessage' - -export class AuthenticationUpdateMessage { - readonly time: number = Date.now() - readonly sender: string = testChat - readonly type: TestMessageType = 'authenticationUpdateMessage' - - constructor( - readonly testEnabled: boolean, - readonly authenticatingTabIDs: string[] - ) {} -} - -export class UpdatePromptProgressMessage extends UiMessage { - readonly progressField: ProgressField | null - override type: TestMessageType = 'updatePromptProgress' - constructor(tabID: string, progressField: ProgressField | null) { - super(tabID) - this.progressField = progressField - } -} - -export class AuthNeededException extends UiMessage { - override type: TestMessageType = 'authNeededException' - - constructor( - readonly message: string, - readonly authType: AuthFollowUpType, - tabID: string - ) { - super(tabID) - } -} - -export interface ChatMessageProps { - readonly message: string | undefined - readonly messageId?: string | undefined - readonly messageType: ChatItemType - readonly buttons?: ChatItemButton[] - readonly followUps?: ChatItemAction[] - readonly canBeVoted?: boolean - readonly filePath?: string - readonly informationCard?: ChatItemContent['informationCard'] -} - -export class ChatMessage extends UiMessage { - readonly message: string | undefined - readonly messageId?: string | undefined - readonly messageType: ChatItemType - readonly canBeVoted?: boolean - readonly buttons?: ChatItemButton[] - readonly informationCard: ChatItemContent['informationCard'] - override type: TestMessageType = 'chatMessage' - - constructor(props: ChatMessageProps, tabID: string) { - super(tabID) - this.message = props.message - this.messageType = props.messageType - this.messageId = props.messageId || undefined - this.canBeVoted = props.canBeVoted || undefined - this.informationCard = props.informationCard || undefined - this.buttons = props.buttons || undefined - } -} - -export class ChatSummaryMessage extends UiMessage { - readonly message: string | undefined - readonly messageId?: string | undefined - readonly messageType: ChatItemType - readonly buttons: ChatItemButton[] - readonly canBeVoted?: boolean - readonly filePath?: string - override type: TestMessageType = 'chatSummaryMessage' - - constructor(props: ChatMessageProps, tabID: string) { - super(tabID) - this.message = props.message - this.messageType = props.messageType - this.buttons = props.buttons || [] - this.messageId = props.messageId || undefined - this.canBeVoted = props.canBeVoted - this.filePath = props.filePath - } -} - -export class ChatInputEnabledMessage extends UiMessage { - override type: TestMessageType = 'chatInputEnabledMessage' - - constructor( - tabID: string, - readonly enabled: boolean - ) { - super(tabID) - } -} - -export class UpdatePlaceholderMessage extends UiMessage { - readonly newPlaceholder: string - override type: TestMessageType = 'updatePlaceholderMessage' - - constructor(tabID: string, newPlaceholder: string) { - super(tabID) - this.newPlaceholder = newPlaceholder - } -} - -export class CapabilityCardMessage extends ChatMessage { - constructor(tabID: string) { - super( - { - message: '', - messageType: 'answer', - informationCard: { - title: '/test - Unit test generation', - description: 'Generate unit tests for selected code', - content: { - body: `I can generate unit tests for the active file or open project in your IDE. - -I can do things like: -- Add unit tests for highlighted functions -- Generate tests for null and empty inputs - -To learn more, visit the [Amazon Q Developer User Guide](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/test-generation.html).`, - }, - icon: 'check-list' as MynahIcons, - }, - }, - tabID - ) - } -} - -export class ErrorMessage extends UiMessage { - readonly title!: string - readonly message!: string - override type: TestMessageType = 'errorMessage' - - constructor(title: string, message: string, tabID: string) { - super(tabID) - this.title = title - this.message = message - } -} - -export class BuildProgressMessage extends UiMessage { - readonly message: string | undefined - readonly codeGenerationId!: string - readonly messageId?: string - readonly followUps?: { - text?: string - options?: ChatItemAction[] - } - readonly fileList?: { - fileTreeTitle?: string - rootFolderTitle?: string - filePaths?: string[] - } - readonly codeReference?: CodeReference[] - readonly canBeVoted: boolean - readonly messageType: ChatItemType - override type: TestMessageType = 'buildProgressMessage' - - constructor({ - tabID, - messageType, - codeGenerationId, - message, - canBeVoted, - messageId, - followUps, - fileList, - codeReference, - }: SendBuildProgressMessageParams) { - super(tabID) - this.messageType = messageType - this.codeGenerationId = codeGenerationId - this.message = message - this.canBeVoted = canBeVoted - this.messageId = messageId - this.followUps = followUps - this.fileList = fileList - this.codeReference = codeReference - } -} - -export class AppToWebViewMessageDispatcher { - constructor(private readonly appsToWebViewMessagePublisher: MessagePublisher) {} - - public sendChatMessage(message: ChatMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendChatSummaryMessage(message: ChatSummaryMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendUpdatePlaceholder(message: UpdatePlaceholderMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendAuthenticationUpdate(message: AuthenticationUpdateMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendAuthNeededExceptionMessage(message: AuthNeededException) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendChatInputEnabled(message: ChatInputEnabledMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendErrorMessage(message: ErrorMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendBuildProgressMessage(message: BuildProgressMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendUpdatePromptProgress(message: UpdatePromptProgressMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } -} diff --git a/packages/core/src/amazonqTest/error.ts b/packages/core/src/amazonqTest/error.ts deleted file mode 100644 index a6694b35863..00000000000 --- a/packages/core/src/amazonqTest/error.ts +++ /dev/null @@ -1,67 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import { ToolkitError } from '../shared/errors' - -export const technicalErrorCustomerFacingMessage = - 'I am experiencing technical difficulties at the moment. Please try again in a few minutes.' -const defaultTestGenErrorMessage = 'Amazon Q encountered an error while generating tests. Try again later.' -export class TestGenError extends ToolkitError { - constructor( - error: string, - code: string, - public uiMessage: string - ) { - super(error, { code }) - } -} -export class ProjectZipError extends TestGenError { - constructor(error: string) { - super(error, 'ProjectZipError', defaultTestGenErrorMessage) - } -} -export class InvalidSourceZipError extends TestGenError { - constructor() { - super('Failed to create valid source zip', 'InvalidSourceZipError', defaultTestGenErrorMessage) - } -} -export class CreateUploadUrlError extends TestGenError { - constructor(errorMessage: string) { - super(errorMessage, 'CreateUploadUrlError', technicalErrorCustomerFacingMessage) - } -} -export class UploadTestArtifactToS3Error extends TestGenError { - constructor(error: string) { - super(error, 'UploadTestArtifactToS3Error', technicalErrorCustomerFacingMessage) - } -} -export class CreateTestJobError extends TestGenError { - constructor(error: string) { - super(error, 'CreateTestJobError', technicalErrorCustomerFacingMessage) - } -} -export class TestGenTimedOutError extends TestGenError { - constructor() { - super( - 'Test generation failed. Amazon Q timed out.', - 'TestGenTimedOutError', - technicalErrorCustomerFacingMessage - ) - } -} -export class TestGenStoppedError extends TestGenError { - constructor() { - super('Unit test generation cancelled.', 'TestGenCancelled', 'Unit test generation cancelled.') - } -} -export class TestGenFailedError extends TestGenError { - constructor(error?: string) { - super(error ?? 'Test generation failed', 'TestGenFailedError', error ?? technicalErrorCustomerFacingMessage) - } -} -export class ExportResultsArchiveError extends TestGenError { - constructor(error?: string) { - super(error ?? 'Test generation failed', 'ExportResultsArchiveError', technicalErrorCustomerFacingMessage) - } -} diff --git a/packages/core/src/amazonqTest/index.ts b/packages/core/src/amazonqTest/index.ts deleted file mode 100644 index 06f5ebb63f9..00000000000 --- a/packages/core/src/amazonqTest/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -export { default as MessengerUtils } from './chat/controller/messenger/messengerUtils' diff --git a/packages/core/src/amazonqTest/models/constants.ts b/packages/core/src/amazonqTest/models/constants.ts deleted file mode 100644 index 547cbdb3663..00000000000 --- a/packages/core/src/amazonqTest/models/constants.ts +++ /dev/null @@ -1,147 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import { ProgressField, MynahIcons, ChatItemButton } from '@aws/mynah-ui' -import { ButtonActions } from '../chat/controller/messenger/messengerUtils' -import { TestGenerationBuildStep } from '../../codewhisperer/models/constants' -import { ChatSessionManager } from '../chat/storages/chatSession' -import { BuildStatus } from '../chat/session/session' - -// For uniquely identifiying which chat messages should be routed to Test -export const testChat = 'testChat' - -export const maxUserPromptLength = 4096 // user prompt character limit from MPS and API model. - -export const cancelTestGenButton: ChatItemButton = { - id: ButtonActions.STOP_TEST_GEN, - text: 'Cancel', - icon: 'cancel' as MynahIcons, -} - -export const testGenProgressField: ProgressField = { - status: 'default', - value: -1, - text: 'Generating unit tests...', - actions: [cancelTestGenButton], -} - -export const testGenCompletedField: ProgressField = { - status: 'success', - value: 100, - text: 'Complete...', - actions: [], -} - -export const cancellingProgressField: ProgressField = { - status: 'warning', - text: 'Cancelling...', - value: -1, - actions: [], -} - -export const cancelBuildProgressButton: ChatItemButton = { - id: ButtonActions.STOP_BUILD, - text: 'Cancel', - icon: 'cancel' as MynahIcons, -} - -export const buildProgressField: ProgressField = { - status: 'default', - value: -1, - text: 'Executing...', - actions: [cancelBuildProgressButton], -} - -export const errorProgressField: ProgressField = { - status: 'error', - text: 'Error...Input needed', - value: -1, - actions: [cancelBuildProgressButton], -} - -export const testGenSummaryMessage = ( - fileName: string, - planSummary?: string -) => `Sure. This may take a few minutes. I'll share updates here as I work on this. - -**Generating unit tests for the following methods in \`${fileName}\`** -${planSummary ? `\n\n${planSummary}` : ''} -` - -const checkIcons = { - wait: '☐', - current: '☐', - done: '', - error: '❌', -} - -interface StepStatus { - step: TestGenerationBuildStep - status: 'wait' | 'current' | 'done' | 'error' -} - -const stepStatuses: StepStatus[] = [] - -export const testGenBuildProgressMessage = (currentStep: TestGenerationBuildStep, status?: string) => { - const session = ChatSessionManager.Instance.getSession() - const statusText = BuildStatus[session.buildStatus].toLowerCase() - const icon = session.buildStatus === BuildStatus.SUCCESS ? checkIcons['done'] : checkIcons['error'] - let message = `Sure. This may take a few minutes and I'll share updates on my progress here. -**Progress summary**\n\n` - - if (currentStep === TestGenerationBuildStep.START_STEP) { - return message.trim() - } - - updateStepStatuses(currentStep, status) - - if (currentStep >= TestGenerationBuildStep.RUN_BUILD) { - message += `${getIconForStep(TestGenerationBuildStep.RUN_BUILD)} Started build execution\n` - } - - if (currentStep >= TestGenerationBuildStep.RUN_EXECUTION_TESTS) { - message += `${getIconForStep(TestGenerationBuildStep.RUN_EXECUTION_TESTS)} Executing tests\n` - } - - if (currentStep >= TestGenerationBuildStep.FIXING_TEST_CASES && session.buildStatus === BuildStatus.FAILURE) { - message += `${getIconForStep(TestGenerationBuildStep.FIXING_TEST_CASES)} Fixing errors in tests\n\n` - } - - if (currentStep > TestGenerationBuildStep.PROCESS_TEST_RESULTS) { - message += `**Test case summary** -${session.shortAnswer?.testCoverage ? `- Unit test coverage ${session.shortAnswer?.testCoverage}%` : ``} -${icon} Build ${statusText} -${icon} Assertion ${statusText}` - // TODO: Update Assertion % - } - - return message.trim() -} -// TODO: Work on UX to show the build error in the progress message -const updateStepStatuses = (currentStep: TestGenerationBuildStep, status?: string) => { - for (let step = TestGenerationBuildStep.INSTALL_DEPENDENCIES; step <= currentStep; step++) { - const stepStatus: StepStatus = { - step: step, - status: 'wait', - } - - if (step === currentStep) { - stepStatus.status = status === 'failed' ? 'error' : 'current' - } else if (step < currentStep) { - stepStatus.status = 'done' - } - - const existingIndex = stepStatuses.findIndex((s) => s.step === step) - if (existingIndex !== -1) { - stepStatuses[existingIndex] = stepStatus - } else { - stepStatuses.push(stepStatus) - } - } -} - -const getIconForStep = (step: TestGenerationBuildStep) => { - const stepStatus = stepStatuses.find((s) => s.step === step) - return stepStatus ? checkIcons[stepStatus.status] : checkIcons.wait -} diff --git a/packages/core/src/applicationcomposer/constants.ts b/packages/core/src/applicationcomposer/constants.ts new file mode 100644 index 00000000000..1eb3852ad6a --- /dev/null +++ b/packages/core/src/applicationcomposer/constants.ts @@ -0,0 +1,10 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +const isLocalDev = false +const localhost = 'http://127.0.0.1:3000' +const cdn = 'https://ide-toolkits.app-composer.aws.dev' + +export { isLocalDev, localhost, cdn } diff --git a/packages/core/src/applicationcomposer/messageHandlers/generateResourceHandler.ts b/packages/core/src/applicationcomposer/messageHandlers/generateResourceHandler.ts index 6d3e96b81c3..fe31e40ef27 100644 --- a/packages/core/src/applicationcomposer/messageHandlers/generateResourceHandler.ts +++ b/packages/core/src/applicationcomposer/messageHandlers/generateResourceHandler.ts @@ -2,12 +2,6 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ -import { - GenerateAssistantResponseRequest, - SupplementaryWebLink, - Reference, - UserIntent, -} from '@amzn/codewhisperer-streaming' import { GenerateResourceRequestMessage, @@ -16,15 +10,13 @@ import { Command, MessageType, } from '../types' -import globals from '../../shared/extensionGlobals' import { getLogger } from '../../shared/logger/logger' -import { AmazonqNotFoundError, getAmazonqApi } from '../../amazonq/extApi' - -const TIMEOUT = 30_000 +import request from '../../shared/request' +import { isLocalDev, localhost, cdn } from '../constants' export async function generateResourceHandler(request: GenerateResourceRequestMessage, context: WebviewContext) { try { - const { chatResponse, references, metadata, isSuccess } = await generateResource(request.cfnType) + const { chatResponse, references, metadata, isSuccess } = await fetchExampleResource(request.cfnType) const responseMessage: GenerateResourceResponseMessage = { command: Command.GENERATE_RESOURCE, @@ -54,116 +46,18 @@ export async function generateResourceHandler(request: GenerateResourceRequestMe } } -async function generateResource(cfnType: string) { - let startTime = globals.clock.Date.now() - +async function fetchExampleResource(cfnType: string) { try { - const amazonqApi = await getAmazonqApi() - if (!amazonqApi) { - throw new AmazonqNotFoundError() - } - const request: GenerateAssistantResponseRequest = { - conversationState: { - currentMessage: { - userInputMessage: { - content: cfnType, - userIntent: UserIntent.GENERATE_CLOUDFORMATION_TEMPLATE, - }, - }, - chatTriggerType: 'MANUAL', - }, - } - - let response = '' - let metadata - let conversationId - let supplementaryWebLinks: SupplementaryWebLink[] = [] - let references: Reference[] = [] - - await amazonqApi.authApi.reauthIfNeeded() - - startTime = globals.clock.Date.now() - // TODO-STARLING - Revisit to see if timeout still needed prior to launch - const data = await timeout(amazonqApi.chatApi.chat(request), TIMEOUT) - const initialResponseTime = globals.clock.Date.now() - startTime - getLogger().debug(`CW Chat initial response: %O, ${initialResponseTime} ms`, data) - if (data['$metadata']) { - metadata = data['$metadata'] - } - - if (data.generateAssistantResponseResponse === undefined) { - getLogger().debug(`Error: Unexpected model response: %O`, data) - throw new Error('No model response') - } - - for await (const value of data.generateAssistantResponseResponse) { - if (value?.assistantResponseEvent?.content) { - try { - response += value.assistantResponseEvent.content - } catch (error: any) { - getLogger().debug(`Warning: Failed to parse content response: ${error.message}`) - throw new Error('Invalid model response') - } - } - if (value?.messageMetadataEvent?.conversationId) { - conversationId = value.messageMetadataEvent.conversationId - } - - const newWebLinks = value?.supplementaryWebLinksEvent?.supplementaryWebLinks - - if (newWebLinks && newWebLinks.length > 0) { - supplementaryWebLinks = supplementaryWebLinks.concat(newWebLinks) - } - - if (value.codeReferenceEvent?.references && value.codeReferenceEvent.references.length > 0) { - references = references.concat(value.codeReferenceEvent.references) - - // Code References are not expected for these single resource prompts - // As we don't yet have the workflows needed to accept references, create the properly structured - // CW Reference log event, we will reject responses that have code references - let errorMessage = 'Code references found for this response, rejecting.' - - if (conversationId) { - errorMessage += ` cID(${conversationId})` - } - - if (metadata?.requestId) { - errorMessage += ` rID(${metadata.requestId})` - } - - throw new Error(errorMessage) - } - } - - const elapsedTime = globals.clock.Date.now() - startTime - - getLogger().debug( - `CW Chat Debug message: - cfnType = "${cfnType}", - conversationId = ${conversationId}, - metadata = %O, - supplementaryWebLinks = %O, - references = %O, - response = "${response}", - initialResponse = ${initialResponseTime} ms, - elapsed time = ${elapsedTime} ms`, - metadata, - supplementaryWebLinks, - references - ) - + const source = isLocalDev ? localhost : cdn + const resp = request.fetch('GET', `${source}/examples/${convertCFNType(cfnType)}.json`, {}) return { - chatResponse: response, + chatResponse: await (await resp.response).text(), references: [], - metadata: { - ...metadata, - conversationId, - queryTime: elapsedTime, - }, + metadata: {}, isSuccess: true, } } catch (error: any) { - getLogger().debug(`CW Chat error: ${error.name} - ${error.message}`) + getLogger().debug(`Resource fetch error: ${error.name} - ${error.message}`) if (error.$metadata) { const { requestId, cfId, extendedRequestId } = error.$metadata getLogger().debug('%O', { requestId, cfId, extendedRequestId }) @@ -173,11 +67,11 @@ async function generateResource(cfnType: string) { } } -function timeout(promise: Promise, ms: number, timeoutError = new Error('Promise timed out')): Promise { - const _timeout = new Promise((_, reject) => { - globals.clock.setTimeout(() => { - reject(timeoutError) - }, ms) - }) - return Promise.race([promise, _timeout]) +function convertCFNType(cfnType: string): string { + const resourceParts = cfnType.split('::') + if (resourceParts.length !== 3) { + throw new Error('CFN type did not contain three parts') + } + + return resourceParts.join('_') } diff --git a/packages/core/src/applicationcomposer/webviewManager.ts b/packages/core/src/applicationcomposer/webviewManager.ts index 884039f907b..92bcbb55593 100644 --- a/packages/core/src/applicationcomposer/webviewManager.ts +++ b/packages/core/src/applicationcomposer/webviewManager.ts @@ -7,16 +7,11 @@ import * as vscode from 'vscode' import * as nls from 'vscode-nls' import request from '../shared/request' import { ApplicationComposer } from './composerWebview' +import { isLocalDev, localhost, cdn } from './constants' import { getLogger } from '../shared/logger/logger' const localize = nls.loadMessageBundle() -// TODO turn this into a flag to make local dev easier -// Change this to true for local dev -const isLocalDev = false -const localhost = 'http://127.0.0.1:3000' -const cdn = 'https://ide-toolkits.app-composer.aws.dev' - const enabledFeatures = ['ide-only', 'anything-resource', 'sfnV2', 'starling'] export class ApplicationComposerManager { diff --git a/packages/core/src/auth/activation.ts b/packages/core/src/auth/activation.ts index a7ee249ab97..8305610dff7 100644 --- a/packages/core/src/auth/activation.ts +++ b/packages/core/src/auth/activation.ts @@ -9,17 +9,28 @@ import { LoginManager } from './deprecated/loginManager' import { fromString } from './providers/credentials' import { initializeCredentialsProviderManager } from './utils' import { isAmazonQ, isSageMaker } from '../shared/extensionUtilities' +import { getLogger } from '../shared/logger/logger' +import { getErrorMsg } from '../shared/errors' -interface SagemakerCookie { +export interface SagemakerCookie { authMode?: 'Sso' | 'Iam' } export async function initialize(loginManager: LoginManager): Promise { if (isAmazonQ() && isSageMaker()) { - // 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') { - initializeCredentialsProviderManager() + 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') { + initializeCredentialsProviderManager() + } + } catch (e) { + const errMsg = getErrorMsg(e as Error) + if (errMsg?.includes("command 'sagemaker.parseCookies' not found")) { + getLogger().warn(`Failed to execute command "sagemaker.parseCookies": ${e}`) + } else { + throw e + } } } Auth.instance.onDidChangeActiveConnection(async (conn) => { diff --git a/packages/core/src/auth/auth.ts b/packages/core/src/auth/auth.ts index 59b3a21f840..053df50321e 100644 --- a/packages/core/src/auth/auth.ts +++ b/packages/core/src/auth/auth.ts @@ -182,6 +182,10 @@ export class Auth implements AuthService, ConnectionManager { return Object.values(this._declaredConnections) } + public getCurrentProfileId() { + return this.store.getCurrentProfileId() + } + @withTelemetryContext({ name: 'restorePreviousSession', class: authClassName }) public async restorePreviousSession(): Promise { const id = this.store.getCurrentProfileId() @@ -211,10 +215,34 @@ export class Auth implements AuthService, ConnectionManager { const provider = await this.getCredentialsProvider(id, profile) await this.authenticate(id, () => this.createCachedCredentials(provider), shouldInvalidate) - return this.getIamConnection(id, profile) + return await this.getIamConnection(id, profile) } } + /** + * Gets the SSO access token for a connection + * @param connection The SSO connection to get the token for + * @returns Promise resolving to the access token string + */ + @withTelemetryContext({ name: 'getSsoAccessToken', class: authClassName }) + public async getSsoAccessToken(connection: Pick): Promise { + const profile = this.store.getProfileOrThrow(connection.id) + + if (profile.type !== 'sso') { + throw new Error(`Connection ${connection.id} is not an SSO connection`) + } + + const provider = this.getSsoTokenProvider(connection.id, profile) + // Calling existing getToken private method - It will handle setting the connection state etc. + const token = await this._getToken(connection.id, provider) + + if (!token?.accessToken) { + throw new Error(`No access token available for connection ${connection.id}`) + } + + return token.accessToken + } + public async useConnection({ id }: Pick): Promise public async useConnection({ id }: Pick): Promise @withTelemetryContext({ name: 'useConnection', class: authClassName }) @@ -225,7 +253,8 @@ export class Auth implements AuthService, ConnectionManager { if (profile === undefined) { throw new Error(`Connection does not exist: ${id}`) } - const conn = profile.type === 'sso' ? this.getSsoConnection(id, profile) : this.getIamConnection(id, profile) + const conn = + profile.type === 'sso' ? this.getSsoConnection(id, profile) : await this.getIamConnection(id, profile) this.#activeConnection = conn this.#onDidChangeActiveConnection.fire(conn) @@ -677,7 +706,7 @@ export class Auth implements AuthService, ConnectionManager { if (profile.type === 'sso') { return this.getSsoConnection(id, profile) } else { - return this.getIamConnection(id, profile) + return await this.getIamConnection(id, profile) } } @@ -777,10 +806,13 @@ export class Auth implements AuthService, ConnectionManager { ) } - private getIamConnection( + private async getIamConnection( id: Connection['id'], profile: StoredProfile - ): IamConnection & StatefulConnection { + ): Promise { + // Get the provider to extract the endpoint URL + const provider = await this.getCredentialsProvider(id, profile) + const endpointUrl = provider.getEndpointUrl?.() return { id, type: 'iam', @@ -788,6 +820,7 @@ export class Auth implements AuthService, ConnectionManager { label: profile.metadata.label ?? (profile.type === 'iam' && profile.subtype === 'linked' ? profile.name : id), getCredentials: async () => this.getCredentials(id, await this.getCredentialsProvider(id, profile)), + endpointUrl, } } @@ -804,6 +837,8 @@ export class Auth implements AuthService, ConnectionManager { label: profile.metadata?.label ?? this.getSsoProfileLabel(profile), getToken: () => this.getToken(id, provider), getRegistration: () => provider.getClientRegistration(), + // SsoConnection is managed internally in the AWS Toolkit, so the endpointUrl can't be configured + endpointUrl: undefined, } } @@ -827,9 +862,10 @@ export class Auth implements AuthService, ConnectionManager { private async createCachedCredentials(provider: CredentialsProvider) { const providerId = provider.getCredentialsId() + getLogger().debug(`credentials: create cache credentials for ${provider.getProviderType()}`) globals.loginManager.store.invalidateCredentials(providerId) - const { credentials } = await globals.loginManager.store.upsertCredentials(providerId, provider) - await globals.loginManager.validateCredentials(credentials, provider.getDefaultRegion()) + const { credentials, endpointUrl } = await globals.loginManager.store.upsertCredentials(providerId, provider) + await globals.loginManager.validateCredentials(credentials, endpointUrl, provider.getDefaultRegion()) return credentials } @@ -919,10 +955,22 @@ export class Auth implements AuthService, ConnectionManager { if (previousState === 'valid') { // Non-token expiration errors can happen. We must log it here, otherwise they are lost. getLogger().warn(`auth: valid connection became invalid. Last error: %s`, this.#validationErrors.get(id)) - const timeout = new Timeout(60000) this.#invalidCredentialsTimeouts.set(id, timeout) + // Check if this is a SMUS profile - if so, skip the generic prompt + // as SMUS has its own reauthentication flow + const isSmusConnection = profile.type === 'sso' && 'domainUrl' in profile && 'domainId' in profile + if (isSmusConnection) { + getLogger().debug(`auth: Skipping generic reauthentication prompt for SMUS connection ${id}`) + // For SMUS connections, just throw the InvalidConnection error + // The SMUS auth provider will handle showing the appropriate prompt + throw new ToolkitError('Connection is invalid or expired. Try logging in again.', { + code: errorCode.invalidConnection, + cause: this.#validationErrors.get(id), + }) + } + const connLabel = profile.metadata.label ?? (profile.type === 'sso' ? this.getSsoProfileLabel(profile) : id) const message = localize( 'aws.auth.invalidConnection', @@ -1028,6 +1076,16 @@ export class Auth implements AuthService, ConnectionManager { } } } + + // Add conditional auto-login logic for SageMaker (jmkeyes@ guidance) + if (hasVendedIamCredentials() && isSageMaker()) { + // SageMaker auto-login logic - use 'ec2' source since SageMaker uses EC2-like instance credentials + const sagemakerProfileId = asString({ credentialSource: 'ec2', credentialTypeId: 'sagemaker-instance' }) + if ((await tryConnection(sagemakerProfileId)) === true) { + getLogger().info(`auth: automatically connected with SageMaker credentials`) + return + } + } } /** diff --git a/packages/core/src/auth/connection.ts b/packages/core/src/auth/connection.ts index 3e7752dd8e9..fea929fc8af 100644 --- a/packages/core/src/auth/connection.ts +++ b/packages/core/src/auth/connection.ts @@ -71,6 +71,18 @@ export const isBuilderIdConnection = (conn?: Connection): conn is SsoConnection export const isValidCodeCatalystConnection = (conn?: Connection): conn is SsoConnection => isSsoConnection(conn) && hasScopes(conn, scopesCodeCatalyst) +export const areCredentialsEqual = (creds1: any, creds2: any): boolean => { + if (!creds1 || !creds2) { + return creds1 === creds2 + } + + return ( + creds1.accessKeyId === creds2.accessKeyId && + creds1.secretAccessKey === creds2.secretAccessKey && + creds1.sessionToken === creds2.sessionToken + ) +} + export function hasScopes(target: SsoConnection | SsoProfile | string[], scopes: string[]): boolean { return scopes?.every((s) => (Array.isArray(target) ? target : target.scopes)?.includes(s)) } @@ -111,6 +123,7 @@ export function createSsoProfile( export interface SsoConnection extends SsoProfile { readonly id: string readonly label: string + readonly endpointUrl?: string | undefined /** * Retrieves a bearer token, refreshing or re-authenticating as-needed. @@ -129,6 +142,7 @@ export interface IamConnection { // This may change in the future after refactoring legacy implementations readonly id: string readonly label: string + readonly endpointUrl: string | undefined getCredentials(): Promise } diff --git a/packages/core/src/auth/credentials/store.ts b/packages/core/src/auth/credentials/store.ts index 2fd2d29b18b..ff963b09db0 100644 --- a/packages/core/src/auth/credentials/store.ts +++ b/packages/core/src/auth/credentials/store.ts @@ -12,13 +12,14 @@ import { CredentialsProviderManager } from '../providers/credentialsProviderMana export interface CachedCredentials { credentials: AWS.Credentials credentialsHashCode: string + endpointUrl?: string } /** * Simple cache for credentials */ export class CredentialsStore { - private readonly credentialsCache: { [key: string]: CachedCredentials } + public readonly credentialsCache: { [key: string]: CachedCredentials } public constructor() { this.credentialsCache = {} @@ -30,11 +31,16 @@ export class CredentialsStore { * If the expiration property does not exist, it is assumed to never expire. */ public isValid(key: string): boolean { + // Apply 60-second buffer similar to SSO token expiry logic + const expirationBufferMs = 60000 + if (this.credentialsCache[key]) { const expiration = this.credentialsCache[key].credentials.expiration - return expiration !== undefined ? expiration >= new globals.clock.Date() : true + const now = new globals.clock.Date() + const bufferedNow = new globals.clock.Date(now.getTime() + expirationBufferMs) + return expiration !== undefined ? expiration >= bufferedNow : true } - + getLogger().debug(`credentials: no credentials found for ${key}`) return false } @@ -89,13 +95,14 @@ export class CredentialsStore { credentialsId: CredentialsId, credentialsProvider: CredentialsProvider ): Promise { + getLogger().debug(`store: Fetch new credentials from provider with id: ${asString(credentialsId)}`) const credentials = { credentials: await credentialsProvider.getCredentials(), credentialsHashCode: credentialsProvider.getHashCode(), + endpointUrl: credentialsProvider.getEndpointUrl?.(), } this.credentialsCache[asString(credentialsId)] = credentials - return credentials } } diff --git a/packages/core/src/auth/credentials/types.ts b/packages/core/src/auth/credentials/types.ts index 79f3e623fcf..75a12a2d9b2 100644 --- a/packages/core/src/auth/credentials/types.ts +++ b/packages/core/src/auth/credentials/types.ts @@ -12,6 +12,7 @@ export const SharedCredentialsKeys = { AWS_SESSION_TOKEN: 'aws_session_token', CREDENTIAL_PROCESS: 'credential_process', CREDENTIAL_SOURCE: 'credential_source', + ENDPOINT_URL: 'endpoint_url', REGION: 'region', ROLE_ARN: 'role_arn', SOURCE_PROFILE: 'source_profile', diff --git a/packages/core/src/auth/credentials/utils.ts b/packages/core/src/auth/credentials/utils.ts index 885a4fb1f87..05a648d867d 100644 --- a/packages/core/src/auth/credentials/utils.ts +++ b/packages/core/src/auth/credentials/utils.ts @@ -21,7 +21,7 @@ import { isValidResponse } from '../../shared/wizards/wizard' const credentialsTimeout = 300000 // 5 minutes const credentialsProgressDelay = 1000 -export function asEnvironmentVariables(credentials: Credentials): NodeJS.ProcessEnv { +export function asEnvironmentVariables(credentials: Credentials, endpointUrl?: string): NodeJS.ProcessEnv { const environmentVariables: NodeJS.ProcessEnv = {} environmentVariables.AWS_ACCESS_KEY = credentials.accessKeyId @@ -30,6 +30,9 @@ export function asEnvironmentVariables(credentials: Credentials): NodeJS.Process environmentVariables.AWS_SECRET_ACCESS_KEY = credentials.secretAccessKey environmentVariables.AWS_SESSION_TOKEN = credentials.sessionToken environmentVariables.AWS_SECURITY_TOKEN = credentials.sessionToken + if (endpointUrl !== undefined) { + environmentVariables.AWS_ENDPOINT_URL = endpointUrl + } return environmentVariables } diff --git a/packages/core/src/auth/deprecated/loginManager.ts b/packages/core/src/auth/deprecated/loginManager.ts index b7c5a83d340..b2d3fb3c3c3 100644 --- a/packages/core/src/auth/deprecated/loginManager.ts +++ b/packages/core/src/auth/deprecated/loginManager.ts @@ -30,10 +30,11 @@ import { isAutomation } from '../../shared/vscode/env' import { Credentials } from '@aws-sdk/types' import { ToolkitError } from '../../shared/errors' import * as localizedText from '../../shared/localizedText' -import { DefaultStsClient } from '../../shared/clients/stsClient' +import { DefaultStsClient, type GetCallerIdentityResponse } from '../../shared/clients/stsClient' import { findAsync } from '../../shared/utilities/collectionUtils' import { telemetry } from '../../shared/telemetry/telemetry' import { withTelemetryContext } from '../../shared/telemetry/util' +import { localStackConnectionHeader, localStackConnectionString } from '../utils' const loginManagerClassName = 'LoginManager' /** @@ -65,19 +66,19 @@ export class LoginManager { try { provider = await getProvider(args.providerId) - - const credentials = (await this.store.upsertCredentials(args.providerId, provider))?.credentials + const { credentials, endpointUrl } = await this.store.upsertCredentials(args.providerId, provider) if (!credentials) { throw new Error(`No credentials found for id ${asString(args.providerId)}`) } - const accountId = await this.validateCredentials(credentials, provider.getDefaultRegion()) + const accountId = await this.validateCredentials(credentials, endpointUrl, provider.getDefaultRegion()) this.awsContext.credentialsShim = createCredentialsShim(this.store, args.providerId, credentials) await this.awsContext.setCredentials({ credentials, accountId: accountId, credentialsId: asString(args.providerId), defaultRegion: provider.getDefaultRegion(), + endpointUrl: provider.getEndpointUrl?.(), }) telemetryResult = 'Succeeded' @@ -111,16 +112,40 @@ export class LoginManager { } } - public async validateCredentials(credentials: Credentials, region = this.defaultCredentialsRegion) { - const stsClient = new DefaultStsClient(region, credentials) - const accountId = (await stsClient.getCallerIdentity()).Account + public async validateCredentials( + credentials: Credentials, + endpointUrl?: string, + region = this.defaultCredentialsRegion + ) { + const stsClient = new DefaultStsClient(region, credentials, endpointUrl) + const callerIdentity = await stsClient.getCallerIdentity() + await this.detectExternalConnection(callerIdentity) + // Validate presence of Account Id + const accountId = callerIdentity.Account if (!accountId) { + if (endpointUrl !== undefined) { + telemetry.auth_customEndpoint.emit({ source: 'validateCredentials', result: 'Failed' }) + } throw new Error('Could not determine Account Id for credentials') } + if (endpointUrl !== undefined) { + telemetry.auth_customEndpoint.emit({ source: 'validateCredentials', result: 'Succeeded' }) + } return accountId } + private async detectExternalConnection(callerIdentity: GetCallerIdentityResponse): Promise { + // @ts-ignore + const headers = callerIdentity.$response?.httpResponse?.headers + if (headers !== undefined && localStackConnectionHeader in headers) { + await globals.globalState.update('aws.toolkit.externalConnection', localStackConnectionString) + telemetry.auth_localstackEndpoint.emit({ source: 'validateCredentials', result: 'Succeeded' }) + } else { + await globals.globalState.update('aws.toolkit.externalConnection', undefined) + } + } + /** * Removes Credentials from the Toolkit. Essentially the Toolkit becomes "logged out". * diff --git a/packages/core/src/auth/index.ts b/packages/core/src/auth/index.ts index c180d603c67..a5a3ca0edd9 100644 --- a/packages/core/src/auth/index.ts +++ b/packages/core/src/auth/index.ts @@ -19,6 +19,7 @@ export { getTelemetryMetadataForConn, isIamConnection, isSsoConnection, + areCredentialsEqual, } from './connection' export { Auth } from './auth' export { CredentialsStore } from './credentials/store' diff --git a/packages/core/src/auth/providers/credentials.ts b/packages/core/src/auth/providers/credentials.ts index 56f1e6a2a00..2c86ffee4df 100644 --- a/packages/core/src/auth/providers/credentials.ts +++ b/packages/core/src/auth/providers/credentials.ts @@ -112,6 +112,10 @@ export interface CredentialsProvider { */ getTelemetryType(): CredentialType getDefaultRegion(): string | undefined + /** + * Gets the endpoint URL configured for this profile, if any. + */ + getEndpointUrl?(): string | undefined getHashCode(): string getCredentials(): Promise /** diff --git a/packages/core/src/auth/providers/envVarsCredentialsProvider.ts b/packages/core/src/auth/providers/envVarsCredentialsProvider.ts index dd9a78a7fcb..14cac0907a0 100644 --- a/packages/core/src/auth/providers/envVarsCredentialsProvider.ts +++ b/packages/core/src/auth/providers/envVarsCredentialsProvider.ts @@ -61,4 +61,9 @@ export class EnvVarsCredentialsProvider implements CredentialsProvider { } return this.credentials } + + public getEndpointUrl(): string | undefined { + const env = process.env as EnvironmentVariables + return env.AWS_ENDPOINT_URL?.toString() + } } diff --git a/packages/core/src/auth/providers/sharedCredentialsProvider.ts b/packages/core/src/auth/providers/sharedCredentialsProvider.ts index 717a151a3af..02d8f9b40f8 100644 --- a/packages/core/src/auth/providers/sharedCredentialsProvider.ts +++ b/packages/core/src/auth/providers/sharedCredentialsProvider.ts @@ -105,6 +105,10 @@ export class SharedCredentialsProvider implements CredentialsProvider { return this.profile[SharedCredentialsKeys.REGION] } + public getEndpointUrl(): string | undefined { + return this.profile[SharedCredentialsKeys.ENDPOINT_URL]?.trim() + } + public async canAutoConnect(): Promise { if (isSsoProfile(this.profile)) { const tokenProvider = SsoAccessTokenProvider.create({ @@ -406,12 +410,28 @@ export class SharedCredentialsProvider implements CredentialsProvider { `auth: Profile ${this.profileName} is missing source_profile for role assumption` ) } - // Use source profile to assume IAM role based on role ARN provided. + + // Check if we already have resolved credentials from patchSourceCredentials const sourceProfile = iniData[profile.source_profile!] - const stsClient = new DefaultStsClient(this.getDefaultRegion() ?? 'us-east-1', { - accessKeyId: sourceProfile.aws_access_key_id!, - secretAccessKey: sourceProfile.aws_secret_access_key!, - }) + let sourceCredentials: AWS.Credentials + + if (sourceProfile.aws_access_key_id && sourceProfile.aws_secret_access_key) { + // Source credentials have already been resolved + sourceCredentials = { + accessKeyId: sourceProfile.aws_access_key_id, + secretAccessKey: sourceProfile.aws_secret_access_key, + sessionToken: sourceProfile.aws_session_token, + } + } else { + // Source profile needs credential resolution - this should have been handled by patchSourceCredentials + // but if not, we need to resolve it here + const sourceProvider = new SharedCredentialsProvider(profile.source_profile!, this.sections) + sourceCredentials = await sourceProvider.getCredentials() + } + + // Use source credentials to assume IAM role based on role ARN provided. + const stsClient = new DefaultStsClient(this.getDefaultRegion() ?? 'us-east-1', sourceCredentials) + // Prompt for MFA Token if needed. const assumeRoleReq = { RoleArn: profile.role_arn, diff --git a/packages/core/src/auth/providers/ssoCredentialsProvider.ts b/packages/core/src/auth/providers/ssoCredentialsProvider.ts index f38dd0710a2..e04ce1a3c06 100644 --- a/packages/core/src/auth/providers/ssoCredentialsProvider.ts +++ b/packages/core/src/auth/providers/ssoCredentialsProvider.ts @@ -61,4 +61,9 @@ export class SsoCredentialsProvider implements CredentialsProvider { private async hasToken() { return (await this.tokenProvider.getToken()) !== undefined } + + // SsoCredentials are managed internally in the AWS Toolkit, so the endpointUrl can't be configured + public getEndpointUrl(): undefined { + return undefined + } } diff --git a/packages/core/src/auth/secondaryAuth.ts b/packages/core/src/auth/secondaryAuth.ts index 01ccf6b799a..f8ea5d9b44f 100644 --- a/packages/core/src/auth/secondaryAuth.ts +++ b/packages/core/src/auth/secondaryAuth.ts @@ -18,7 +18,7 @@ import { withTelemetryContext } from '../shared/telemetry/util' import { isNetworkError } from '../shared/errors' import globals from '../shared/extensionGlobals' -export type ToolId = 'codecatalyst' | 'codewhisperer' | 'testId' +export type ToolId = 'codecatalyst' | 'codewhisperer' | 'testId' | 'smus' let currentConn: Auth['activeConnection'] const auths = new Map() diff --git a/packages/core/src/auth/ui/statusBarItem.ts b/packages/core/src/auth/ui/statusBarItem.ts index a70a905ed6d..e253a6f427e 100644 --- a/packages/core/src/auth/ui/statusBarItem.ts +++ b/packages/core/src/auth/ui/statusBarItem.ts @@ -51,12 +51,6 @@ function handleDevSettings(statusBarItem: vscode.StatusBarItem, devSettings: Dev function updateItem(statusBarItem: vscode.StatusBarItem, devSettings: DevSettings): void { const company = getIdeProperties().company const connections = getAllConnectionsInUse(Auth.instance) - const connectedTooltip = localize( - 'AWS.credentials.statusbar.connected', - 'Connected to {0} with "{1}" (click to change)', - getIdeProperties().company, - connections[0]?.label - ) const disconnectedTooltip = localize( 'AWS.credentials.statusbar.disconnected', 'Click to connect to {0}', @@ -69,7 +63,25 @@ function updateItem(statusBarItem: vscode.StatusBarItem, devSettings: DevSetting statusBarItem.text = company statusBarItem.tooltip = disconnectedTooltip } else if (connections.length === 1) { - statusBarItem.text = getText(connections[0].label) + // Get the endpoint URL if available + const endpointUrl = connections[0].endpointUrl + const connectedTooltip = endpointUrl + ? localize( + 'AWS.credentials.statusbar.connected.endpoint', + 'Connected to {0} with "{1}" ({2}) (click to change)', + getIdeProperties().company, + connections[0]?.label, + endpointUrl + ) + : localize( + 'AWS.credentials.statusbar.connected', + 'Connected to {0} with "{1}" (click to change)', + getIdeProperties().company, + connections[0]?.label + ) + + const displayText = endpointUrl ? `${connections[0].label} (custom endpoint)` : connections[0].label + statusBarItem.text = getText(displayText) statusBarItem.tooltip = connectedTooltip } else { const expired = connections.filter((c) => c.state !== 'valid') diff --git a/packages/core/src/auth/utils.ts b/packages/core/src/auth/utils.ts index 9da35fa06e5..28e2bc1123e 100644 --- a/packages/core/src/auth/utils.ts +++ b/packages/core/src/auth/utils.ts @@ -40,6 +40,7 @@ import { hasScopes, scopesSsoAccountAccess, isSsoConnection, + IamConnection, } from './connection' import { Commands, placeholder } from '../shared/vscode/commands2' import { Auth } from './auth' @@ -71,7 +72,7 @@ export async function promptForConnection(auth: Auth, type?: 'iam' | 'iam-only' if (resp === 'addNewConnection') { // We could call this command directly, but it either lives in packages/toolkit or will at some point. const source: AuthSource = 'addConnectionQuickPick' // enforcing type sanity check - await vscode.commands.executeCommand('aws.toolkit.auth.manageConnections', placeholder, source) + await vscode.commands.executeCommand('aws.toolkit.auth.manageConnections', placeholder, source, undefined, true) return undefined } @@ -79,6 +80,18 @@ export async function promptForConnection(auth: Auth, type?: 'iam' | 'iam-only' return globals.awsContextCommands.onCommandEditCredentials() } + // If selected connection is SSO connection and has linked IAM profiles, show second quick pick with the linked IAM profiles + if (isSsoConnection(resp)) { + const linkedProfiles = await getLinkedIamProfiles(auth, resp) + + if (linkedProfiles.length > 0) { + const linkedResp = await showLinkedProfilePicker(linkedProfiles, resp) + if (linkedResp) { + return linkedResp + } + } + } + return resp } @@ -89,8 +102,9 @@ export async function promptForConnection(auth: Auth, type?: 'iam' | 'iam-only' export async function promptAndUseConnection(...[auth, type]: Parameters) { return telemetry.aws_setCredentials.run(async (span) => { let conn = await promptForConnection(auth, type) + // Returning because either conn is a valid connection, or the customer selected 'addNewConnection' or 'editCredentials' if (!conn) { - throw new CancellationError('user') + return } // HACK: We assume that if we are toolkit we want AWS account scopes. @@ -113,7 +127,11 @@ export async function promptAndUseConnection(...[auth, type]: Parameters vscode.QuickInputButton = () => return { tooltip: deleteConnection, iconPath: getIcon('vscode-trash') } } +async function getLinkedIamProfiles(auth: Auth, ssoConnection: SsoConnection): Promise { + const allConnections = await auth.listAndTraverseConnections().promise() + + return allConnections.filter( + (conn) => isIamConnection(conn) && conn.id.startsWith(`sso:${ssoConnection.id}#`) + ) as IamConnection[] +} + +/** + * Shows a quick pick with linked IAM profiles for a selected SSO connection + */ +async function showLinkedProfilePicker( + linkedProfiles: IamConnection[], + ssoConnection: SsoConnection +): Promise { + const title = `Select an IAM Role for ${ssoConnection.label}` + + const items: DataQuickPickItem[] = linkedProfiles.map((profile) => ({ + label: codicon`${getIcon('vscode-key')} ${profile.label}`, + description: 'IAM Credential, sourced from IAM Identity Center', + data: profile, + })) + + return await showQuickPick(items, { + title, + placeholder: 'Select an IAM role', + buttons: [createRefreshButton(), createExitButton()], + }) +} + export function createConnectionPrompter(auth: Auth, type?: 'iam' | 'iam-only' | 'sso') { const addNewConnection = { label: codicon`${getIcon('vscode-plus')} Add New Connection`, @@ -429,22 +476,25 @@ export function createConnectionPrompter(auth: Auth, type?: 'iam' | 'iam-only' | for await (const conn of connections) { if (conn.label.includes('profile:') && !hasShownEdit) { hasShownEdit = true - yield [toPickerItem(conn), editCredentials] + yield [await toPickerItem(conn), editCredentials] } else { - yield [toPickerItem(conn)] + yield [await toPickerItem(conn)] } } } - function toPickerItem(conn: Connection): DataQuickPickItem { + async function toPickerItem(conn: Connection): Promise> { const state = auth.getConnectionState(conn) // Only allow SSO connections to be deleted const deleteButton: vscode.QuickInputButton[] = conn.type === 'sso' ? [createDeleteConnectionButton()] : [] + // Get endpoint URL if available + const connLabel = conn.endpointUrl ? `${conn.label} (${conn.endpointUrl})` : conn.label if (state === 'valid') { + const label = codicon`${getConnectionIcon(conn)} ${connLabel}` return { data: conn, - label: codicon`${getConnectionIcon(conn)} ${conn.label}`, - description: getConnectionDescription(conn), + label: label, + description: await getConnectionDescription(conn), buttons: [...deleteButton], } } @@ -462,7 +512,7 @@ export function createConnectionPrompter(auth: Auth, type?: 'iam' | 'iam-only' | detail: getDetail(), data: conn, invalidSelection: state !== 'authenticating', - label: codicon`${getIcon('vscode-error')} ${conn.label}`, + label: codicon`${getIcon('vscode-error')} ${connLabel}`, buttons: [...deleteButton], description: state === 'authenticating' @@ -498,7 +548,7 @@ export function createConnectionPrompter(auth: Auth, type?: 'iam' | 'iam-only' | } } - function getConnectionDescription(conn: Connection) { + async function getConnectionDescription(conn: Connection) { if (conn.type === 'iam') { // TODO: implement a proper `getConnectionSource` method to discover where a connection came from const descSuffix = conn.id.startsWith('profile:') @@ -510,6 +560,14 @@ export function createConnectionPrompter(auth: Auth, type?: 'iam' | 'iam-only' | return `IAM Credential, ${descSuffix}` } + // If this is an SSO connection, check if it has linked IAM profiles + if (isSsoConnection(conn)) { + const linkedProfiles = await getLinkedIamProfiles(auth, conn) + if (linkedProfiles.length > 0) { + return `Has ${linkedProfiles.length} IAM role${linkedProfiles.length > 1 ? 's' : ''} (click to select)` + } + } + const toolAuths = getDependentAuths(conn) if (toolAuths.length === 0) { return undefined @@ -552,7 +610,14 @@ export class AuthNode implements TreeNode { const conn = this.resource.activeConnection const itemLabel = conn?.label !== undefined - ? localize('aws.auth.node.connected', `Connected with {0}`, conn.label) + ? conn?.endpointUrl !== undefined + ? localize( + 'aws.auth.node.connectedWithEndpoint', + `Connected with {0} ({1})`, + conn.label, + conn?.endpointUrl + ) + : localize('aws.auth.node.connected', `Connected with {0}`, conn.label) : localize('aws.auth.node.selectConnection', 'Select a connection...') const item = new vscode.TreeItem(itemLabel) @@ -825,3 +890,12 @@ export async function getAuthType() { } return authType } + +export const localStackConnectionHeader = 'x-localstack' +export const localStackConnectionString = 'localstack' + +export function isLocalStackConnection(): boolean { + return ( + globals.globalState.tryGet('aws.toolkit.externalConnection', String, undefined) === localStackConnectionString + ) +} diff --git a/packages/core/src/awsService/appBuilder/activation.ts b/packages/core/src/awsService/appBuilder/activation.ts index 5c4122b6d8b..01f01a1b4c8 100644 --- a/packages/core/src/awsService/appBuilder/activation.ts +++ b/packages/core/src/awsService/appBuilder/activation.ts @@ -13,7 +13,12 @@ import { activateViewsShared, registerToolView } from '../../awsexplorer/activat import { setContext } from '../../shared/vscode/setContext' import { fs } from '../../shared/fs/fs' import { AppBuilderRootNode } from './explorer/nodes/rootNode' -import { initWalkthroughProjectCommand, walkthroughContextString, getOrInstallCliWrapper } from './walkthrough' +import { + initWalkthroughProjectCommand, + walkthroughContextString, + getOrInstallCliWrapper, + installLocalStackExtension, +} from './walkthrough' import { getLogger } from '../../shared/logger/logger' import path from 'path' import { TreeNode } from '../../shared/treeview/resourceTreeDataProvider' @@ -24,6 +29,7 @@ import { getSyncWizard, runSync } from '../../shared/sam/sync' import { getDeployWizard, runDeploy } from '../../shared/sam/deploy' import { DeployTypeWizard } from './wizards/deployTypeWizard' import { createNewServerlessLandProject } from './serverlessLand/main' +import { lambdaToSam } from './lambda2sam/lambda2sam' export const templateToOpenAppComposer = 'aws.toolkit.appComposer.templateToOpenOnStart' /** @@ -126,6 +132,12 @@ async function setWalkthrough(walkthroughSelected: string = 'S3'): Promise async function registerAppBuilderCommands(context: ExtContext): Promise { const source = 'AppBuilderWalkthrough' context.extensionContext.subscriptions.push( + Commands.register({ id: 'aws.toolkit.lambda.convertToSam', autoconnect: true }, async (lambdaNode) => { + await telemetry.appbuilder_lambda2sam.run(async () => { + telemetry.record({ source: 'explorer' }) + await lambdaToSam(lambdaNode) + }) + }), Commands.register('aws.toolkit.installSAMCLI', async () => { await getOrInstallCliWrapper('sam-cli', source) }), @@ -135,6 +147,9 @@ async function registerAppBuilderCommands(context: ExtContext): Promise { Commands.register('aws.toolkit.installDocker', async () => { await getOrInstallCliWrapper('docker', source) }), + Commands.register('aws.toolkit.installLocalStack', async () => { + await installLocalStackExtension(source) + }), Commands.register('aws.toolkit.lambda.setWalkthroughToAPI', async () => { await setWalkthrough('API') }), diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts index 470169a7725..323f0dbd6c5 100644 --- a/packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts +++ b/packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts @@ -27,6 +27,7 @@ import { s3BucketType, } from '../../../../shared/cloudformation/cloudformation' import { ToolkitError } from '../../../../shared/errors' +import { ResourceTreeEntity } from '../samProject' const localize = nls.loadMessageBundle() export interface DeployedResource { @@ -77,8 +78,9 @@ export async function generateDeployedNode( deployedResource: any, regionCode: string, stackName: string, - resourceTreeEntity: any -): Promise { + resourceTreeEntity: ResourceTreeEntity, + location?: vscode.Uri +): Promise { let newDeployedResource: any const partitionId = globals.regionProvider.getPartitionId(regionCode) ?? defaultPartition try { @@ -90,7 +92,15 @@ export async function generateDeployedNode( try { configuration = (await defaultClient.getFunction(deployedResource.PhysicalResourceId)) .Configuration as Lambda.FunctionConfiguration - newDeployedResource = new LambdaFunctionNode(lambdaNode, regionCode, configuration) + newDeployedResource = new LambdaFunctionNode( + lambdaNode, + regionCode, + configuration, + undefined, + location ? vscode.Uri.joinPath(location, resourceTreeEntity.CodeUri ?? '').fsPath : undefined, + location, + deployedResource.LogicalResourceId + ) } catch (error: any) { getLogger().error('Error getting Lambda configuration: %O', error) throw ToolkitError.chain(error, 'Error getting Lambda configuration', { diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/deployedStack.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/deployedStack.ts index 1bf8381e097..3a533c722fb 100644 --- a/packages/core/src/awsService/appBuilder/explorer/nodes/deployedStack.ts +++ b/packages/core/src/awsService/appBuilder/explorer/nodes/deployedStack.ts @@ -5,9 +5,10 @@ import * as vscode from 'vscode' import { TreeNode } from '../../../../shared/treeview/resourceTreeDataProvider' import { getIcon } from '../../../../shared/icons' -import { CloudFormationClient, DescribeStacksCommand } from '@aws-sdk/client-cloudformation' +import { CloudFormationClient, DescribeStacksCommand, CloudFormationClientConfig } from '@aws-sdk/client-cloudformation' import { ToolkitError } from '../../../../shared/errors' import { getIAMConnection } from '../../../../auth/utils' +import globals from '../../../../shared/extensionGlobals' export class StackNameNode implements TreeNode { public readonly id = this.stackName @@ -46,7 +47,12 @@ export async function generateStackNode(stackName?: string, regionCode?: string) return [] } const cred = await connection.getCredentials() - const client = new CloudFormationClient({ region: regionCode, credentials: cred }) + const endpointUrl = globals.awsContext.getCredentialEndpointUrl() + const opts: CloudFormationClientConfig = { region: regionCode, credentials: cred } + if (endpointUrl !== undefined) { + opts.endpoint = endpointUrl + } + const client = new CloudFormationClient(opts) try { const command = new DescribeStacksCommand({ StackName: stackName }) const response = await client.send(command) diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/resourceNode.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/resourceNode.ts index bda7b69ac4f..fc9d4c48c14 100644 --- a/packages/core/src/awsService/appBuilder/explorer/nodes/resourceNode.ts +++ b/packages/core/src/awsService/appBuilder/explorer/nodes/resourceNode.ts @@ -17,13 +17,36 @@ import { generatePropertyNodes } from './propertyNode' import { generateDeployedNode } from './deployedNode' import { StackResource } from '../../../../lambda/commands/listSamResources' import { DeployedResourceNode } from './deployedNode' +import { LambdaFunctionNode } from '../../../../lambda/explorer/lambdaFunctionNode' +import { ToolkitError } from '../../../../shared/errors' enum ResourceTypeId { Function = 'function', + DeployedFunction = 'deployed-function', Api = 'api', Other = '', } +export async function generateLambdaNodeFromResource(resource: ResourceNode['resource']): Promise { + if (!resource.deployedResource || !resource.region || !resource.stackName || !resource.resource) { + throw new ToolkitError('Error getting Lambda info from Appbuilder Node, please check your connection') + } + const nodes = (await generateDeployedNode( + resource.deployedResource, + resource.region, + resource.stackName, + resource.resource, + resource.projectRoot + )) as DeployedResourceNode[] + if (nodes.length !== 1) { + throw new ToolkitError('Error getting Lambda info from Appbuilder Node, please check your connection') + } + // lambda function node or undefined + return nodes[0].resource?.explorerNode +} + +// from here, we should have a helper function to detect if lambda is deployed +// then return deployed node/normal node on each condition. export class ResourceNode implements TreeNode { public readonly id = this.resourceTreeEntity.Id private readonly type = this.resourceTreeEntity.Type @@ -43,6 +66,7 @@ export class ResourceNode implements TreeNode { return { resource: this.resourceTreeEntity, location: this.location.samTemplateUri, + projectRoot: this.location.projectRoot, workspaceFolder: this.location.workspaceFolder, region: this.region, stackName: this.stackName, @@ -56,12 +80,13 @@ export class ResourceNode implements TreeNode { let propertyNodes: TreeNode[] = [] if (this.deployedResource && this.region && this.stackName) { - deployedNodes = await generateDeployedNode( + deployedNodes = (await generateDeployedNode( this.deployedResource, this.region, this.stackName, - this.resourceTreeEntity - ) + this.resourceTreeEntity, + this.location.projectRoot + )) as DeployedResourceNode[] } if (this.resourceTreeEntity.Type === SERVERLESS_FUNCTION_TYPE) { propertyNodes = generatePropertyNodes(this.resourceTreeEntity) @@ -71,10 +96,7 @@ export class ResourceNode implements TreeNode { } public getTreeItem(): vscode.TreeItem { - // Determine the initial TreeItem collapsible state based on the type - const collapsibleState = this.deployedResource - ? vscode.TreeItemCollapsibleState.Collapsed - : vscode.TreeItemCollapsibleState.None + const collapsibleState = vscode.TreeItemCollapsibleState.Collapsed // Create the TreeItem with the determined collapsible state const item = new vscode.TreeItem(this.resourceTreeEntity.Id, collapsibleState) @@ -99,7 +121,11 @@ export class ResourceNode implements TreeNode { private getIconPath(): IconPath | undefined { switch (this.type) { case SERVERLESS_FUNCTION_TYPE: + if (this.deployedResource) { + return getIcon('aws-lambda-deployed-function') + } return getIcon('aws-lambda-function') + // add deployed lambda function type case s3BucketType: return getIcon('aws-s3-bucket') case appRunnerType: @@ -114,6 +140,9 @@ export class ResourceNode implements TreeNode { private getResourceId(): ResourceTypeId { switch (this.type) { case SERVERLESS_FUNCTION_TYPE: + if (this.deployedResource) { + return ResourceTypeId.DeployedFunction + } return ResourceTypeId.Function case 'Api': return ResourceTypeId.Api diff --git a/packages/core/src/awsService/appBuilder/lambda2sam/lambda2sam.ts b/packages/core/src/awsService/appBuilder/lambda2sam/lambda2sam.ts new file mode 100644 index 00000000000..20f7c372583 --- /dev/null +++ b/packages/core/src/awsService/appBuilder/lambda2sam/lambda2sam.ts @@ -0,0 +1,1006 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { LambdaFunctionNode } from '../../../lambda/explorer/lambdaFunctionNode' +import fs from '../../../shared/fs/fs' +import { getLogger } from '../../../shared/logger/logger' +import * as os from 'os' +import { + LAMBDA_FUNCTION_TYPE, + LAMBDA_LAYER_TYPE, + LAMBDA_URL_TYPE, + SERVERLESS_FUNCTION_TYPE, + SERVERLESS_LAYER_TYPE, + Template, + TemplateResources, + loadByContents, + save, + tryLoad, + ZipResourceProperties, + Resource, +} from '../../../shared/cloudformation/cloudformation' + +import { downloadUnzip, getLambdaClient, getCFNClient, isPermissionError } from '../utils' +import { openProjectInWorkspace } from '../walkthrough' +import { ToolkitError } from '../../../shared/errors' +import { ResourcesToImport, StackResource } from 'aws-sdk/clients/cloudformation' +import { SignatureV4 } from '@smithy/signature-v4' +import { Sha256 } from '@aws-crypto/sha256-js' +import { getIAMConnection } from '../../../auth/utils' +import globals from '../../../shared/extensionGlobals' +import { Runtime, telemetry } from '../../../shared/telemetry/telemetry' + +/** + * Information about a CloudFormation stack + */ +export interface StackInfo { + stackId: string + stackName: string + isSamTemplate: boolean + template: Template +} + +/** + * Main entry point for converting a Lambda function to a SAM project + */ +export async function lambdaToSam(lambdaNode: LambdaFunctionNode): Promise { + try { + // Show progress notification for the overall process + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: `Converting ${lambdaNode.name} to SAM project`, + cancellable: false, + }, + async (progress) => { + // 0. Prompt user for project location + const saveUri = await promptForProjectLocation() + if (!saveUri) { + getLogger().info('User canceled project location selection') + return + } + progress.report({ increment: 0, message: 'Checking stack association...' }) + + // 1. Determine which scenario applies to this Lambda function + telemetry.record({ runtime: lambdaNode?.configuration?.Runtime as Runtime | undefined }) + let stackInfo = await determineStackAssociation(lambdaNode) + + // 2. Handle the appropriate scenario + let samTemplate: Template + let sourceType: 'LambdaFunction' | 'SAMStack' | 'CFNStack' + let stackName: string | undefined + if (!stackInfo) { + telemetry.record({ action: 'deployStack' }) + // Scenario 1: Lambda doesn't belong to any stack + sourceType = 'LambdaFunction' + progress.report({ increment: 30, message: 'Generating template...' }) + // 2.1 call api to get CFN + let cfnTemplate: Template + let resourcesToImport: ResourcesToImport + try { + ;[cfnTemplate, resourcesToImport] = await callExternalApiForCfnTemplate(lambdaNode) + } catch (error) { + throw new ToolkitError(`Failed to generate template: ${error}`) + } + + // 2.2. Deploy the CFN template to create a stack + progress.report({ increment: 20, message: 'Deploying template...' }) + stackName = await promptForStackName(lambdaNode.name.replaceAll('_', '-')) + if (!stackName) { + throw new ToolkitError('Stack name not provided') + } + + stackInfo = await deployCfnTemplate( + cfnTemplate, + resourcesToImport, + stackName, + lambdaNode.regionCode + ) + samTemplate = { + AWSTemplateFormatVersion: stackInfo.template.AWSTemplateFormatVersion, + Transform: 'AWS::Serverless-2016-10-31', + Parameters: stackInfo.template.Parameters, + Globals: stackInfo.template.Globals, + Resources: stackInfo.template.Resources, + } + } else if (stackInfo.isSamTemplate) { + // Scenario 3: Lambda belongs to a stack deployed by SAM + sourceType = 'SAMStack' + progress.report({ increment: 50, message: 'Processing SAM template...' }) + samTemplate = stackInfo.template + stackName = stackInfo.stackName + } else { + // Scenario 2: Lambda belongs to a CFN stack + sourceType = 'CFNStack' + progress.report({ increment: 50, message: 'Creating SAM project from CFN...' }) + samTemplate = { + AWSTemplateFormatVersion: stackInfo.template.AWSTemplateFormatVersion, + Transform: 'AWS::Serverless-2016-10-31', + Parameters: stackInfo.template.Parameters, + Globals: stackInfo.template.Globals, + Resources: stackInfo.template.Resources, + } + stackName = stackInfo.stackName + } + + const projectUri = vscode.Uri.joinPath(saveUri[0], stackName) + + telemetry.record({ iac: sourceType }) + + // 3. Process Lambda functions in the template + if (!samTemplate.Resources) { + throw new ToolkitError('Template does not contain any resource, please retry') + } + + progress.report({ message: 'Downloading Lambda function code...' }) + await cfn2sam(samTemplate.Resources, projectUri, stackInfo, lambdaNode.regionCode) + + // 4. Save the SAM template + progress.report({ message: 'Saving SAM template...' }) + await save(samTemplate, vscode.Uri.joinPath(projectUri, 'template.yaml').fsPath) + + // 5. Create a basic README.md + // Use stack name from stackInfo if available, otherwise use the Lambda function name + progress.report({ message: 'Creating Readme...' }) + await createReadme(stackName, sourceType, projectUri) + + // 6. Create samconfig.toml + progress.report({ message: 'Creating SAM configuration...' }) + await createSAMConfig(stackName, lambdaNode.regionCode, projectUri) + + // 7. Open the project in VS Code + await openProjectInWorkspace(projectUri) + + // 8. Show success message + void vscode.window.showInformationMessage(`SAM project created successfully at ${projectUri.fsPath}`) + progress.report({ increment: 100, message: 'Done!' }) + } + ) + } catch (err) { + throw new ToolkitError(`Failed to convert Lambda to SAM: ${err instanceof Error ? err.message : String(err)}`) + } +} + +export async function createReadme( + stackName: string, + sourceType: 'LambdaFunction' | 'SAMStack' | 'CFNStack', + projectUri: vscode.Uri +) { + const warningSection = + sourceType !== 'LambdaFunction' + ? '' + : `**[Warning**: Currently only a subset of resource support converting to SAM, For any missing resources, please check the Lambda Console and add them manually to your SAM template. ]` + const lambda2SAMReadmeSource = 'resources/markdown/lambda2sam.md' + const readme = (await fs.readFileText(globals.context.asAbsolutePath(lambda2SAMReadmeSource))) + .replace(/\$\{sourceType\}/g, sourceType) + .replace(/\$\{stackName\}/g, stackName) + .replace(/\$\{warning\}/g, warningSection) + + await fs.writeFile(vscode.Uri.joinPath(projectUri, 'README.md'), readme) +} + +export async function createSAMConfig(stackName: string, region: string, projectUri: vscode.Uri) { + const samConfigContent = `# More information about the configuration file can be found here: +# https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html +version = 0.1 + +[default] +[default.global.parameters] +stack_name = "${stackName}" +region = "${region}"` + await fs.writeFile(vscode.Uri.joinPath(projectUri, 'samconfig.toml'), samConfigContent) +} + +/** + * Determines if the Lambda function is associated with a CloudFormation stack + * and if that stack was deployed using SAM + */ +export async function determineStackAssociation(lambdaNode: LambdaFunctionNode): Promise { + try { + // Get Lambda function details including tags + const lambdaClient = getLambdaClient(lambdaNode.regionCode) + const functionDetails = await lambdaClient.getFunction(lambdaNode.name) + + // Check if the Lambda function has CloudFormation stack tags + if (!functionDetails.Tags) { + // Lambda doesn't have any tags, so it's not part of a stack + return undefined + } + + // Look for the CloudFormation stack ID tag + const stackIdTag = functionDetails.Tags['aws:cloudformation:stack-id'] + if (!stackIdTag) { + // Lambda doesn't have a CloudFormation stack ID tag + return undefined + } + + // Get the stack name tag if available, otherwise extract from stack ID + let stackName = functionDetails.Tags['aws:cloudformation:stack-name'] + if (!stackName) { + // Extract stack name from stack ID + const stackIdParts = stackIdTag.split('/') + stackName = stackIdParts.length > 1 ? stackIdParts[1] : '' + } + + // Create CloudFormation client + const cfn = await getCFNClient(lambdaNode.regionCode) + + // stack could be in DELETE_COMPLETE status or doesn't exist + const describeStacksResult = await cfn.describeStacks({ StackName: stackIdTag }) + if (!describeStacksResult.Stacks || describeStacksResult.Stacks.length === 0) { + return undefined + } + if (describeStacksResult.Stacks![0].StackStatus === 'DELETE_COMPLETE') { + return undefined + } + // Get the original stack template + const templateResponse = await cfn.getTemplate({ + StackName: stackIdTag, + TemplateStage: 'Original', // Critical to get the original SAM template + }) + + const templateBody = templateResponse.TemplateBody || '{}' + const template = await loadByContents(templateBody, false) + + // Determine if it's a SAM template by checking for the transform + const isSamTemplate = ifSamTemplate(template) + + return { + stackId: stackIdTag, + stackName, + isSamTemplate, + template, + } + } catch (err) { + throw new ToolkitError(`Error determining stack association: ${err}, please try again`) + } +} + +/** + * Checks if a template is a SAM template by looking for the SAM transform + */ +export function ifSamTemplate(template: Template): boolean { + // Check for SAM transform + if (template.Transform) { + if (typeof template.Transform === 'string') { + return template.Transform.startsWith('AWS::Serverless') + } else if (typeof template.Transform === 'object' && Array.isArray(template.Transform)) { + // Handle case where Transform might be an array + return template.Transform.some((t: string) => typeof t === 'string' && t.startsWith('AWS::Serverless')) + } + } + + return false +} + +/** + * Calls the external API to generate a CloudFormation template for a Lambda function + * Note: This is a placeholder for the actual API call + */ +export async function callExternalApiForCfnTemplate( + lambdaNode: LambdaFunctionNode +): Promise<[Template, ResourcesToImport]> { + const conn = await getIAMConnection() + if (!conn || conn.type !== 'iam') { + return [{}, []] + } + + const cred = await conn.getCredentials() + const signer = new SignatureV4({ + credentials: cred, + region: lambdaNode.regionCode, + service: 'lambdaconsole', + sha256: Sha256, + }) + + // TODO: govcloud URL is in a slightly different format + const url = new URL( + `https://${lambdaNode.regionCode}.prod.topology.console.lambda.aws.a2z.com/lambda-api/topology/topology?lambdaArn=${lambdaNode.arn}` + ) + + const signedRequest = await signer.sign({ + method: 'GET', + headers: { + host: url.hostname, + }, + hostname: url.hostname, + path: url.pathname, + query: Object.fromEntries(url.searchParams), + protocol: url.protocol, + }) + + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + Accept: 'application/xml', + 'Content-Type': 'application/json', + ...signedRequest.headers, + }, + }) + + if (!response.ok) { + getLogger().error('Failed to retrieve generated CloudFormation template: %O', await response.json()) + throw new ToolkitError(`Failed to retrieve generated CloudFormation template ID: ${response.statusText}`) + } + + const data = await response.json() + if (!data.cloudFormationTemplateId) { + throw new ToolkitError('No template ID returned') + } + + let status: string | undefined = 'CREATE_IN_PROGRESS' + let getGeneratedTemplateResponse + let resourcesToImport: ResourcesToImport = [] + const cfn = await getCFNClient(lambdaNode.regionCode) + + // Wait for template generation to complete + while (status !== 'COMPLETE') { + getGeneratedTemplateResponse = await cfn.getGeneratedTemplate({ + Format: 'YAML', + GeneratedTemplateName: data.cloudFormationTemplateId, + }) + + status = getGeneratedTemplateResponse.Status + if (status === 'FAILED') { + throw new ToolkitError('CloudFormation template create status FAILED') + } + + // Add a small delay to avoid hitting API rate limits + if (status !== 'COMPLETE') { + await new Promise((resolve) => setTimeout(resolve, 1000)) + } + } + + // Get the generated template details to extract resource information + const describeGeneratedTemplateResponse = await cfn.describeGeneratedTemplate({ + GeneratedTemplateName: data.cloudFormationTemplateId, + }) + + if (describeGeneratedTemplateResponse.Status === 'FAILED') { + throw new ToolkitError('CloudFormation template describe request failed') + } + + // Build resourcesToImport from the generated template resources + if (describeGeneratedTemplateResponse.Resources) { + resourcesToImport = describeGeneratedTemplateResponse.Resources.filter( + (resource) => resource.LogicalResourceId && resource.ResourceType && resource.ResourceIdentifier + ).map((resource) => { + const resourceIdentifier = { ...resource.ResourceIdentifier! } + + // Fix Lambda function identifiers - extract function name from ARN + if (resource.ResourceType === 'AWS::Lambda::Function' && resourceIdentifier.FunctionName) { + // FunctionName might be returned as 'arn:aws:lambda:region:account:function:name' + // We need to extract just the function name + const functionNameOrArn = resourceIdentifier.FunctionName + if (functionNameOrArn.startsWith('arn:')) { + const arnParts = functionNameOrArn.split(':') + // ARN format: arn:aws:lambda:region:account:function:function-name + if (arnParts.length >= 7 && arnParts[5] === 'function') { + resourceIdentifier.FunctionName = arnParts[6] + } + } + } + + return { + ResourceType: resource.ResourceType!, + LogicalResourceId: resource.LogicalResourceId!, + ResourceIdentifier: resourceIdentifier, + } + }) + } + + const cfnTemplate = getGeneratedTemplateResponse!.TemplateBody + + const load = await tryLoad(vscode.Uri.from({ scheme: 'untitled' }), cfnTemplate) + if (!load.template || !load.template.Resources) { + throw new ToolkitError('Failed to load CloudFormation template') + } + + return [load.template, resourcesToImport] +} + +/** + * Prompts the user for a stack name + */ +export async function promptForStackName(defaultName: string): Promise { + return vscode.window.showInputBox({ + title: 'Enter Stack Name', + prompt: 'Enter a name for the CloudFormation stack', + value: `${defaultName}-stack`, + validateInput: (value) => { + if (!value) { + return 'Stack name is required' + } + if (!/^[a-zA-Z][a-zA-Z0-9-]*$/.test(value)) { + return 'Stack name must start with a letter and contain only letters, numbers, and hyphens' + } + return undefined + }, + }) +} + +async function promptForProjectLocation(): Promise { + return vscode.window.showOpenDialog({ + canSelectFolders: true, + canSelectFiles: false, + canSelectMany: false, + openLabel: 'Select SAM project location', + // if not workspace, use home dir + defaultUri: vscode.workspace.workspaceFolders?.[0]?.uri ?? vscode.Uri.file(os.homedir()), + }) +} + +/** + * Deploys a CloudFormation template to create a stack and imports the existing Lambda function + */ +export async function deployCfnTemplate( + template: Template, + resourcesToImport: ResourcesToImport, + stackName: string, + region: string +): Promise { + const cfn = await getCFNClient(region) + + removeUnwantedCodeParameters(template) + + // Convert template object to JSON string + const templateBody = JSON.stringify(template) + + // Create a change set to import the existing resources + const changeSetName = `ImportLambda-${Date.now()}` + const changeSetResponse = await cfn.createChangeSet({ + StackName: stackName, + ChangeSetName: changeSetName, + ChangeSetType: 'IMPORT', + TemplateBody: templateBody, + Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'], + ResourcesToImport: resourcesToImport, + }) + + if (!changeSetResponse.Id) { + throw new ToolkitError('Failed to create change set') + } + + // Wait for change set creation to complete + await cfn + .waitFor('changeSetCreateComplete', { + StackName: stackName, + ChangeSetName: changeSetName, + $waiter: { + delay: 2, + }, + }) + .catch(async (err: any) => { + // If the change set failed to create, get the status reason + const describeResponse = await cfn.describeChangeSet({ + StackName: stackName, + ChangeSetName: changeSetName, + }) + + throw new ToolkitError(`Change set creation failed: ${describeResponse.StatusReason || err.message}`) + }) + + // Execute the change set + await cfn.executeChangeSet({ + StackName: stackName, + ChangeSetName: changeSetName, + }) + + // Wait for stack import to complete + await cfn + .waitFor('stackImportComplete', { + StackName: stackName, + $waiter: { + delay: 2, + }, + }) + .catch(async () => { + // If the stack import failed, wait for stack update complete instead + // (AWS SDK might not have stackImportComplete waiter) + await cfn.waitFor('stackUpdateComplete', { + StackName: stackName, + $waiter: { + delay: 2, + }, + }) + }) + + // Get the stack ID + const describeStackResponse = await cfn.describeStacks({ + StackName: stackName, + }) + + if (!describeStackResponse.Stacks || !describeStackResponse.Stacks[0].StackId) { + throw new ToolkitError('Failed to get stack information') + } + + // Return information about the deployed stack + return { + stackId: describeStackResponse.Stacks[0].StackId, + stackName, + template, + isSamTemplate: false, + } +} + +export function removeUnwantedCodeParameters(template: Template) { + if (!template.Resources) { + throw new Error('No Resources found in template') + } + + const lambdaKey = Object.keys(template.Resources).find( + (key) => template.Resources![key]?.Type === 'AWS::Lambda::Function' + ) + + if (!lambdaKey) { + throw new Error('No Lambda function found in template') + } + + template.Resources[lambdaKey]!.Properties!.Code = { + ZipFile: '', + } + + template.Parameters = {} +} + +/** + * Extracts the logical ID from an intrinsic function like !Ref or !GetAtt + * Returns undefined if the value is not an intrinsic function + */ +export function extractLogicalIdFromIntrinsic(value: any): string | undefined { + // Check for Ref: { "Ref": "logicalId" } + if (typeof value === 'object' && value !== null && Object.keys(value).length === 1 && value.Ref) { + return value.Ref + } + + // Check for GetAtt: { "Fn::GetAtt": ["logicalId", "Arn"] } + if ( + typeof value === 'object' && + value !== null && + Object.keys(value).length === 1 && + value['Fn::GetAtt'] && + Array.isArray(value['Fn::GetAtt']) && + value['Fn::GetAtt'].length === 2 && + value['Fn::GetAtt'][1] === 'Arn' + ) { + return value['Fn::GetAtt'][0] + } + + return undefined +} + +/** + * the main tansform to convert a CFN template to a sam project + * @param resources the parsed CFN template + * @param projectDir selected local location for project + * @param stackInfo + * @param region + */ +export async function cfn2sam( + resources: TemplateResources, + projectDir: vscode.Uri, + stackInfo: StackInfo, + region: string +): Promise { + const lambdaProcess = processLambdaResources(resources, projectDir, stackInfo, region) + const lambdaLayerProcess = processLambdaLayerResources(resources, projectDir, stackInfo, region) + await Promise.all([lambdaProcess, lambdaLayerProcess]) + await processLambdaUrlResources(resources) +} + +/** + * Processes Lambda resources in a template, transforming AWS::Lambda::Function to AWS::Serverless::Function + */ +export function Lambda2Serverless(resourceProp: ZipResourceProperties, key: string): Resource { + const dlqConfig = resourceProp.DeadLetterConfig + return { + Type: SERVERLESS_FUNCTION_TYPE, + Metadata: resourceProp.Metadata, + Properties: { + ...resourceProp, + // Transform Tags from array to object format + Tags: resourceProp.Tags + ? Object.fromEntries( + resourceProp.Tags.filter((item: { Key: any; Value: any }) => item.Key !== 'lambda:createdBy').map( + (item: { Key: any; Value: any }) => [item.Key, item.Value] + ) + ) + : undefined, + // Remove Code property (S3 reference) + Code: undefined, + // Map TracingConfig.Mode to Tracing property + Tracing: resourceProp.TracingConfig?.Mode, + TracingConfig: undefined, + // Transform DeadLetterConfig to DeadLetterQueue + DeadLetterQueue: dlqConfig + ? { + Type: dlqConfig.TargetArn.split(':')[2] === 'sqs' ? 'SQS' : 'SNS', + TargetArn: dlqConfig.TargetArn, + } + : undefined, + // Set CodeUri to the local path + CodeUri: key, + }, + } +} + +/** + * Processes Lambda URL resources in a template, transforming AWS::Lambda::Url to AWS::Serverless::Function.FunctionUrlConfig + */ +export async function processLambdaResources( + resources: TemplateResources, + projectDir: vscode.Uri, + stackInfo: StackInfo, + region: string +): Promise { + await Promise.all( + Object.entries(resources).map(async ([key, resource]) => { + if (!resource) { + return + } + + const resourceProp = resource.Properties + if (!resourceProp || resourceProp.PackageType === 'Image') { + return + } + + if (resource.Type === LAMBDA_FUNCTION_TYPE) { + // Transform AWS::Lambda::Function to AWS::Serverless::Function + try { + await downloadLambdaFunctionCode(key, stackInfo, projectDir, region, resourceProp.FunctionName) + + // Transform to Serverless Function + resources[key] = Lambda2Serverless(resourceProp, key) + } catch (err) { + throw new ToolkitError( + `Failed to process Lambda function ${key}: ${err instanceof Error ? err.message : String(err)}` + ) + } + } else if (resource.Type === SERVERLESS_FUNCTION_TYPE) { + // Update CodeUri for AWS::Serverless::Function + try { + await downloadLambdaFunctionCode(key, stackInfo, projectDir, region, resourceProp.FunctionName) + // Update the CodeUri to point to the local directory + resourceProp.CodeUri = key + } catch (err) { + throw new ToolkitError( + `Failed to process Serverless function ${key}: ${err instanceof Error ? err.message : String(err)}` + ) + } + } + }) + ) +} + +/** + * Processes Lambda Layer resources in a template, transforming AWS::Lambda::LayerVersion to AWS::Serverless::LayerVersion + */ +export async function processLambdaLayerResources( + resources: TemplateResources, + projectDir: vscode.Uri, + stackInfo: StackInfo, + region: string +): Promise { + // Process each resource + await Promise.all( + Object.entries(resources).map(async ([key, resource]) => { + if (!resource || (resource.Type !== LAMBDA_LAYER_TYPE && resource.Type !== SERVERLESS_LAYER_TYPE)) { + return + } + + const resourceProp = resource.Properties + if (!resourceProp) { + return + } + + try { + // Download the layer code + await downloadLayerVersionResourceByName(key, stackInfo, projectDir, region) + + // Transform to Serverless LayerVersion + resources[key] = { + Type: SERVERLESS_LAYER_TYPE, + Properties: { + ...resourceProp, + // Remove Content property (S3 reference) + Content: undefined, + // Set ContentUri to the local path + ContentUri: key, + }, + } + + getLogger().info(`Successfully transformed Lambda Layer ${key} to Serverless LayerVersion`) + } catch (err) { + throw new ToolkitError( + `Failed to process Lambda Layer ${key}: ${err instanceof Error ? err.message : String(err)}` + ) + } + }) + ) +} + +/** + * Processes Lambda URL resources in a template, transforming AWS::Lambda::Url to AWS::Serverless::Function.FunctionUrlConfig + */ +export async function processLambdaUrlResources(resources: TemplateResources): Promise { + for (const [key, resource] of Object.entries(resources)) { + if (resource && resource.Type === LAMBDA_URL_TYPE) { + try { + const resourceProp = resource.Properties + if (!resourceProp) { + continue + } + + // Skip if Qualifier is present (not supported in FunctionUrlConfig) + if (resourceProp.Qualifier) { + getLogger().info( + `Skipping Lambda URL ${key} because Qualifier is not supported in FunctionUrlConfig` + ) + continue + } + + // Find the target function using TargetFunctionArn + const targetFunctionArn = resourceProp.TargetFunctionArn + if (!targetFunctionArn) { + getLogger().warn(`Lambda URL ${key} does not have a TargetFunctionArn`) + continue + } + + const targetFunctionKey = extractLogicalIdFromIntrinsic(targetFunctionArn) + if (!targetFunctionKey) { + getLogger().debug(`Could not extract logical ID from TargetFunctionArn in Lambda URL ${key}`) + continue + } + + const targetFunction = resources[targetFunctionKey] + // if MyLambdaFunction 's url is not formated as MyLambdaFunctionUrl, then we shouldn't transform it + if ( + !targetFunction || + targetFunction.Type !== SERVERLESS_FUNCTION_TYPE || + targetFunctionKey + 'Url' !== key + ) { + getLogger().debug(`Target function ${targetFunctionKey} not found or not a Serverless Function`) + continue + } + + // Add FunctionUrlConfig to the Serverless Function + if (!targetFunction.Properties) { + // skip if target function is not correctly setup + continue + } + + // Now we can safely add FunctionUrlConfig + if (targetFunction.Properties) { + targetFunction.Properties.FunctionUrlConfig = { + AuthType: resourceProp.AuthType, + Cors: resourceProp.Cors, + InvokeMode: resourceProp.InvokeMode, + } + } + + // Remove the original Lambda URL resource + delete resources[key] + + getLogger().info( + `Successfully transformed Lambda URL ${key} to FunctionUrlConfig in ${targetFunctionKey}` + ) + } catch (err) { + throw new ToolkitError( + `Failed to process Lambda URL ${key}: ${err instanceof Error ? err.message : String(err)}` + ) + } + } + } +} + +/** + * Download lambda function code based on logical resource ID or physical resrouce ID + * If logical id is given, it will try to find the physical id first and then download the code + * If physical id is given, it will download the code directly + * @param resourceName logical name of Lambda function in CFN template + * @param stackInfo + * @param targetDir Local location to store the code + * @param region + * @param physicalResourceId Physical name of Lambda function + */ +export async function downloadLambdaFunctionCode( + resourceName: string, // This is the logical name from CFN + stackInfo: StackInfo, + targetDir: vscode.Uri, + region: string, + physicalResourceId?: string +) { + try { + if (!physicalResourceId || typeof physicalResourceId !== 'string') { + physicalResourceId = await getPhysicalIdfromCFNResourceName( + resourceName, + region, + stackInfo.stackId, + LAMBDA_FUNCTION_TYPE + ) + if (!physicalResourceId) { + throw new ToolkitError(`Could not find physical resource ID for ${resourceName}`) + } + } + + const lambdaClient = getLambdaClient(region) + const functionDetails = await lambdaClient.getFunction(physicalResourceId) + + if (!functionDetails.Code || !functionDetails.Code.Location) { + throw new ToolkitError(`Could not determine code location for function: ${physicalResourceId}`) + } + + const outputPath = vscode.Uri.joinPath(targetDir, resourceName) + await downloadUnzip(functionDetails.Code.Location, outputPath) + + getLogger().info(`Successfully downloaded and extracted: ${resourceName}`) + } catch (err) { + throw new ToolkitError( + `Failed to download resource ${resourceName}: ${err instanceof Error ? err.message : String(err)}` + ) + } +} + +/** + * Get physical resource ID from CFN resource name + * @param name CFN resource name + * @param region + * @param stackId + * @returns Physical resrouce ID + */ +export async function getPhysicalIdfromCFNResourceName( + name: string, + region: string, + stackId: string, + resourceType: string +): Promise { + // Create CloudFormation client + const cfn = await getCFNClient(region) + + try { + // First try the exact match approach + let describeResult + try { + describeResult = await cfn.describeStackResource({ + StackName: stackId, + LogicalResourceId: name, + }) + } catch (error) { + // If it's a permission error, re-throw it immediately + if (isPermissionError(error)) { + throw error + } + // For other errors (like ResourceNotFound), continue to fuzzy matching + describeResult = undefined + } + + if (describeResult?.StackResourceDetail?.PhysicalResourceId) { + const physicalResourceId = describeResult.StackResourceDetail.PhysicalResourceId + getLogger().debug(`Resource ${name} found with exact match, physical ID: ${physicalResourceId}`) + return physicalResourceId + } + + // only do fuzzy matching for layer, function doesn't have random suffix + if (resourceType === LAMBDA_FUNCTION_TYPE || resourceType === SERVERLESS_FUNCTION_TYPE) { + throw new ToolkitError(`Could not find physical resource ID for ${name}`) + } + + // If exact match fails, get all resources and try fuzzy matching + getLogger().debug(`Resource ${name} not found with exact match, trying fuzzy match...`) + const resources = await cfn.describeStackResources({ + StackName: stackId, + }) + + if (!resources.StackResources || resources.StackResources.length === 0) { + getLogger().debug(`No resources found in stack ${stackId}`) + return undefined + } + + // Find resources that start with the given name (SAM transform often adds suffixes) + const matchingResources = resources.StackResources.filter((resource: StackResource) => + resource.LogicalResourceId.startsWith(name) + ) + + if (matchingResources.length === 0) { + // Try a more flexible approach - check if the resource name is a substring + const substringMatches = resources.StackResources.filter((resource: StackResource) => + resource.LogicalResourceId.includes(name) + ) + + if (substringMatches.length === 0) { + getLogger().debug(`No fuzzy matches found for resource ${name}`) + return undefined + } + + // Use the first substring match + const match = substringMatches[0] + getLogger().debug( + `Resource ${name} matched with ${match.LogicalResourceId} using substring match, physical ID: ${match.PhysicalResourceId}` + ) + return match.PhysicalResourceId + } + + // If we have multiple matches, prefer exact prefix match + // Sort by length to get the closest match (shortest additional suffix) + matchingResources.sort( + (a: StackResource, b: StackResource) => a.LogicalResourceId.length - b.LogicalResourceId.length + ) + + const bestMatch = matchingResources[0] + getLogger().debug( + `Resource ${name} matched with ${bestMatch.LogicalResourceId} using prefix match, physical ID: ${bestMatch.PhysicalResourceId}` + ) + return bestMatch.PhysicalResourceId + } catch (err) { + throw ToolkitError.chain(err, `Error finding physical ID for resource ${name}, please retry`) + } +} + +/** + * Download a Lambda Layer resource by name and stack info + * @param resourceName Layer's Logical name from CFN + * @param stackInfo + * @param targetDir local location to store + * @param region + */ +export async function downloadLayerVersionResourceByName( + resourceName: string, // This is the logical name from CFN + stackInfo: StackInfo, + targetDir: vscode.Uri, + region: string +) { + try { + const physicalResourceId = await getPhysicalIdfromCFNResourceName( + resourceName, + region, + stackInfo.stackId, + LAMBDA_LAYER_TYPE + ) + if (!physicalResourceId) { + throw new ToolkitError(`Could not find physical resource ID for ${resourceName}`) + } + + getLogger().debug(`Resource ${resourceName} has physical ID ${physicalResourceId} and type LayerVersion`) + + // Parse the ARN to extract layer name and version + // Format: arn:aws:lambda:region:account-id:layer:layer-name:version + const arnParts = physicalResourceId.split(':') + if (arnParts.length < 8) { + throw new ToolkitError(`Invalid layer ARN format: ${physicalResourceId}`) + } + + const layerName = arnParts[6] + const version = parseInt(arnParts[7], 10) + + if (isNaN(version)) { + throw new ToolkitError(`Invalid version number in layer ARN: ${physicalResourceId}`) + } + + getLogger().debug(`Extracted layer name: ${layerName}, version: ${version} from ARN`) + + const lambdaClient = getLambdaClient(region) + + // Get the layer version details directly using the extracted name and version + const layerDetails = await lambdaClient.getLayerVersion(layerName, version) + + if (!layerDetails.Content || !layerDetails.Content.Location) { + throw new ToolkitError(`Could not determine code location for layer: ${layerName}:${version}`) + } + + // Download Lambda layer code using the presigned URL + const presignedUrl = layerDetails.Content.Location + + // Use node-fetch to download from the presigned URL + const outputPath = vscode.Uri.joinPath(targetDir, resourceName) + await downloadUnzip(presignedUrl, outputPath) + + getLogger().info(`Successfully downloaded and extracted layer ${layerName}:${version} to: ${resourceName}`) + } catch (err) { + throw new ToolkitError( + `Failed to download resource ${resourceName}: ${err instanceof Error ? err.message : String(err)}, please retry` + ) + } +} diff --git a/packages/core/src/awsService/appBuilder/serverlessLand/main.ts b/packages/core/src/awsService/appBuilder/serverlessLand/main.ts index acce81a70f9..aac352b9764 100644 --- a/packages/core/src/awsService/appBuilder/serverlessLand/main.ts +++ b/packages/core/src/awsService/appBuilder/serverlessLand/main.ts @@ -15,6 +15,7 @@ import { ExtContext } from '../../../shared/extensions' import { addFolderToWorkspace } from '../../../shared/utilities/workspaceUtils' import { ToolkitError } from '../../../shared/errors' import { fs } from '../../../shared/fs/fs' +import { handleOverwriteConflict } from '../../../shared/utilities/messages' import { getPattern } from '../../../shared/utilities/downloadPatterns' import { MetadataManager } from './metadataManager' @@ -89,6 +90,9 @@ export async function launchProjectCreationWizard( export async function downloadPatternCode(config: CreateServerlessLandWizardForm, assetName: string): Promise { const fullAssetName = assetName + '.zip' const location = vscode.Uri.joinPath(config.location, config.name) + + await handleOverwriteConflict(location) + try { await getPattern(serverlessLandOwner, serverlessLandRepo, fullAssetName, location, true) } catch (error) { diff --git a/packages/core/src/awsService/appBuilder/utils.ts b/packages/core/src/awsService/appBuilder/utils.ts index 63b116b20eb..bdaa6293b30 100644 --- a/packages/core/src/awsService/appBuilder/utils.ts +++ b/packages/core/src/awsService/appBuilder/utils.ts @@ -19,8 +19,479 @@ import fs from '../../shared/fs/fs' import { getLogger } from '../../shared/logger/logger' import { RuntimeFamily, getFamily } from '../../lambda/models/samLambdaRuntime' import { showMessage } from '../../shared/utilities/messages' +import { DefaultLambdaClient } from '../../shared/clients/lambdaClient' +import AdmZip from 'adm-zip' +import { CloudFormation, Lambda } from 'aws-sdk' +import { isAwsError, UnknownError } from '../../shared/errors' const localize = nls.loadMessageBundle() +/** + * Interface for mapping AWS service actions to their required permissions + */ +interface PermissionMapping { + service: 'cloudformation' | 'lambda' + action: string + requiredPermissions: string[] + documentation?: string +} + +/** + * Comprehensive mapping of AWS service actions to their required permissions + */ +const PermissionMappings: PermissionMapping[] = [ + // CloudFormation permissions + { + service: 'cloudformation', + action: 'describeStacks', + requiredPermissions: ['cloudformation:DescribeStacks'], + documentation: 'https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_DescribeStacks.html', + }, + { + service: 'cloudformation', + action: 'getTemplate', + requiredPermissions: ['cloudformation:GetTemplate'], + documentation: 'https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_GetTemplate.html', + }, + { + service: 'cloudformation', + action: 'createChangeSet', + requiredPermissions: ['cloudformation:CreateChangeSet'], + documentation: 'https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_CreateChangeSet.html', + }, + { + service: 'cloudformation', + action: 'executeChangeSet', + requiredPermissions: ['cloudformation:ExecuteChangeSet'], + documentation: 'https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_ExecuteChangeSet.html', + }, + { + service: 'cloudformation', + action: 'describeChangeSet', + requiredPermissions: ['cloudformation:DescribeChangeSet'], + documentation: 'https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_DescribeChangeSet.html', + }, + { + service: 'cloudformation', + action: 'describeStackResources', + requiredPermissions: ['cloudformation:DescribeStackResources'], + documentation: + 'https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_DescribeStackResources.html', + }, + { + service: 'cloudformation', + action: 'describeStackResource', + requiredPermissions: ['cloudformation:DescribeStackResource'], + documentation: + 'https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_DescribeStackResource.html', + }, + { + service: 'cloudformation', + action: 'getGeneratedTemplate', + requiredPermissions: ['cloudformation:GetGeneratedTemplate'], + documentation: + 'https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_GetGeneratedTemplate.html', + }, + { + service: 'cloudformation', + action: 'describeGeneratedTemplate', + requiredPermissions: ['cloudformation:DescribeGeneratedTemplate'], + documentation: + 'https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_DescribeGeneratedTemplate.html', + }, + // Lambda permissions + { + service: 'lambda', + action: 'getFunction', + requiredPermissions: ['lambda:GetFunction'], + documentation: 'https://docs.aws.amazon.com/lambda/latest/api/API_GetFunction.html', + }, + { + service: 'lambda', + action: 'listFunctions', + requiredPermissions: ['lambda:ListFunctions'], + documentation: 'https://docs.aws.amazon.com/lambda/latest/api/API_ListFunctions.html', + }, + { + service: 'lambda', + action: 'getLayerVersion', + requiredPermissions: ['lambda:GetLayerVersion'], + documentation: 'https://docs.aws.amazon.com/lambda/latest/api/API_GetLayerVersion.html', + }, + { + service: 'lambda', + action: 'listLayerVersions', + requiredPermissions: ['lambda:ListLayerVersions'], + documentation: 'https://docs.aws.amazon.com/lambda/latest/api/API_ListLayerVersions.html', + }, + { + service: 'lambda', + action: 'listFunctionUrlConfigs', + requiredPermissions: ['lambda:GetFunctionUrlConfig'], + documentation: 'https://docs.aws.amazon.com/lambda/latest/api/API_GetFunctionUrlConfig.html', + }, + { + service: 'lambda', + action: 'updateFunctionCode', + requiredPermissions: ['lambda:UpdateFunctionCode'], + documentation: 'https://docs.aws.amazon.com/lambda/latest/api/API_UpdateFunctionCode.html', + }, + { + service: 'lambda', + action: 'deleteFunction', + requiredPermissions: ['lambda:DeleteFunction'], + documentation: 'https://docs.aws.amazon.com/lambda/latest/api/API_DeleteFunction.html', + }, + { + service: 'lambda', + action: 'invoke', + requiredPermissions: ['lambda:InvokeFunction'], + documentation: 'https://docs.aws.amazon.com/lambda/latest/api/API_Invoke.html', + }, +] + +/** + * Creates an enhanced error message for permission-related failures + */ +function createEnhancedPermissionError( + originalError: unknown, + service: 'cloudformation' | 'lambda', + action: string, + resourceArn?: string +): ToolkitError { + const mapping = PermissionMappings.find((m) => m.service === service && m.action === action) + + if (!mapping) { + return ToolkitError.chain(originalError, `Permission denied for ${service}:${action}`) + } + + const permissionsList = mapping.requiredPermissions.map((p) => ` - ${p}`).join('\n') + const resourceInfo = resourceArn ? `\nResource: ${resourceArn}` : '' + + const message = `Permission denied: Missing required permissions for ${service}:${action} + +Required permissions: +${permissionsList}${resourceInfo} + +To fix this issue: +1. Contact your AWS administrator to add the missing permissions +2. Add these permissions to your IAM user/role policy +3. If using IAM roles, ensure the role has these permissions attached + +${mapping.documentation ? `Documentation: ${mapping.documentation}` : ''}` + + return new ToolkitError(message, { + code: 'InsufficientPermissions', + cause: UnknownError.cast(originalError), + details: { + service, + action, + requiredPermissions: mapping.requiredPermissions, + resourceArn, + }, + }) +} + +/** + * Checks if an error is a permission-related error + */ +export function isPermissionError(error: unknown): boolean { + return ( + isAwsError(error) && + (error.code === 'AccessDeniedException' || + error.code === 'UnauthorizedOperation' || + error.code === 'Forbidden' || + error.code === 'AccessDenied' || + (error as any).statusCode === 403) + ) +} + +/** + * Enhanced Lambda client wrapper that provides better error messages for permission issues + */ +export class EnhancedLambdaClient { + constructor( + private readonly client: DefaultLambdaClient, + private readonly regionCode: string + ) {} + + async deleteFunction(name: string): Promise { + try { + return await this.client.deleteFunction(name) + } catch (error) { + if (isPermissionError(error)) { + throw createEnhancedPermissionError( + error, + 'lambda', + 'deleteFunction', + `arn:aws:lambda:${this.regionCode}:*:function:${name}` + ) + } + throw error + } + } + + async invoke(name: string, payload?: Lambda.InvocationRequest['Payload']): Promise { + try { + return await this.client.invoke(name, payload) + } catch (error) { + if (isPermissionError(error)) { + throw createEnhancedPermissionError( + error, + 'lambda', + 'invoke', + `arn:aws:lambda:${this.regionCode}:*:function:${name}` + ) + } + throw error + } + } + + async *listFunctions(): AsyncIterableIterator { + try { + yield* this.client.listFunctions() + } catch (error) { + if (isPermissionError(error)) { + throw createEnhancedPermissionError(error, 'lambda', 'listFunctions') + } + throw error + } + } + + async getFunction(name: string): Promise { + try { + return await this.client.getFunction(name) + } catch (error) { + if (isPermissionError(error)) { + throw createEnhancedPermissionError( + error, + 'lambda', + 'getFunction', + `arn:aws:lambda:${this.regionCode}:*:function:${name}` + ) + } + throw error + } + } + + async getLayerVersion(name: string, version: number): Promise { + try { + return await this.client.getLayerVersion(name, version) + } catch (error) { + if (isPermissionError(error)) { + throw createEnhancedPermissionError( + error, + 'lambda', + 'getLayerVersion', + `arn:aws:lambda:${this.regionCode}:*:layer:${name}:${version}` + ) + } + throw error + } + } + + async *listLayerVersions(name: string): AsyncIterableIterator { + try { + yield* this.client.listLayerVersions(name) + } catch (error) { + if (isPermissionError(error)) { + throw createEnhancedPermissionError( + error, + 'lambda', + 'listLayerVersions', + `arn:aws:lambda:${this.regionCode}:*:layer:${name}` + ) + } + throw error + } + } + + async getFunctionUrlConfigs(name: string): Promise { + try { + return await this.client.getFunctionUrlConfigs(name) + } catch (error) { + if (isPermissionError(error)) { + throw createEnhancedPermissionError( + error, + 'lambda', + 'listFunctionUrlConfigs', + `arn:aws:lambda:${this.regionCode}:*:function:${name}` + ) + } + throw error + } + } + + async updateFunctionCode(name: string, zipFile: Uint8Array): Promise { + try { + return await this.client.updateFunctionCode(name, zipFile) + } catch (error) { + if (isPermissionError(error)) { + throw createEnhancedPermissionError( + error, + 'lambda', + 'updateFunctionCode', + `arn:aws:lambda:${this.regionCode}:*:function:${name}` + ) + } + throw error + } + } +} + +/** + * Enhanced CloudFormation client wrapper that provides better error messages for permission issues + */ +export class EnhancedCloudFormationClient { + constructor( + private readonly client: CloudFormation, + private readonly regionCode: string + ) {} + + async describeStacks(params: CloudFormation.DescribeStacksInput): Promise { + try { + return await this.client.describeStacks(params).promise() + } catch (error) { + if (isPermissionError(error)) { + const stackArn = params.StackName + ? `arn:aws:cloudformation:${this.regionCode}:*:stack/${params.StackName}/*` + : undefined + throw createEnhancedPermissionError(error, 'cloudformation', 'describeStacks', stackArn) + } + throw error + } + } + + async getTemplate(params: CloudFormation.GetTemplateInput): Promise { + try { + return await this.client.getTemplate(params).promise() + } catch (error) { + if (isPermissionError(error)) { + const stackArn = params.StackName + ? `arn:aws:cloudformation:${this.regionCode}:*:stack/${params.StackName}/*` + : undefined + throw createEnhancedPermissionError(error, 'cloudformation', 'getTemplate', stackArn) + } + throw error + } + } + + async createChangeSet(params: CloudFormation.CreateChangeSetInput): Promise { + try { + return await this.client.createChangeSet(params).promise() + } catch (error) { + if (isPermissionError(error)) { + const stackArn = params.StackName + ? `arn:aws:cloudformation:${this.regionCode}:*:stack/${params.StackName}/*` + : undefined + throw createEnhancedPermissionError(error, 'cloudformation', 'createChangeSet', stackArn) + } + throw error + } + } + + async executeChangeSet( + params: CloudFormation.ExecuteChangeSetInput + ): Promise { + try { + return await this.client.executeChangeSet(params).promise() + } catch (error) { + if (isPermissionError(error)) { + const stackArn = params.StackName + ? `arn:aws:cloudformation:${this.regionCode}:*:stack/${params.StackName}/*` + : undefined + throw createEnhancedPermissionError(error, 'cloudformation', 'executeChangeSet', stackArn) + } + throw error + } + } + + async describeChangeSet( + params: CloudFormation.DescribeChangeSetInput + ): Promise { + try { + return await this.client.describeChangeSet(params).promise() + } catch (error) { + if (isPermissionError(error)) { + const stackArn = params.StackName + ? `arn:aws:cloudformation:${this.regionCode}:*:stack/${params.StackName}/*` + : undefined + throw createEnhancedPermissionError(error, 'cloudformation', 'describeChangeSet', stackArn) + } + throw error + } + } + + async describeStackResources( + params: CloudFormation.DescribeStackResourcesInput + ): Promise { + try { + return await this.client.describeStackResources(params).promise() + } catch (error) { + if (isPermissionError(error)) { + const stackArn = params.StackName + ? `arn:aws:cloudformation:${this.regionCode}:*:stack/${params.StackName}/*` + : undefined + throw createEnhancedPermissionError(error, 'cloudformation', 'describeStackResources', stackArn) + } + throw error + } + } + + async describeStackResource( + params: CloudFormation.DescribeStackResourceInput + ): Promise { + try { + return await this.client.describeStackResource(params).promise() + } catch (error) { + if (isPermissionError(error)) { + const stackArn = params.StackName + ? `arn:aws:cloudformation:${this.regionCode}:*:stack/${params.StackName}/*` + : undefined + throw createEnhancedPermissionError(error, 'cloudformation', 'describeStackResource', stackArn) + } + throw error + } + } + + async getGeneratedTemplate( + params: CloudFormation.GetGeneratedTemplateInput + ): Promise { + try { + return await this.client.getGeneratedTemplate(params).promise() + } catch (error) { + if (isPermissionError(error)) { + throw createEnhancedPermissionError(error, 'cloudformation', 'getGeneratedTemplate') + } + throw error + } + } + + async describeGeneratedTemplate( + params: CloudFormation.DescribeGeneratedTemplateInput + ): Promise { + try { + return await this.client.describeGeneratedTemplate(params).promise() + } catch (error) { + if (isPermissionError(error)) { + throw createEnhancedPermissionError(error, 'cloudformation', 'describeGeneratedTemplate') + } + throw error + } + } + + async waitFor(state: string, params: any): Promise { + try { + return await this.client.waitFor(state as any, params).promise() + } catch (error) { + if (isPermissionError(error)) { + // For waitFor operations, we'll provide a generic permission error since the specific action varies + throw createEnhancedPermissionError(error, 'cloudformation', 'describeStacks') + } + throw error + } + } +} + export async function runOpenTemplate(arg?: TreeNode) { const templateUri = arg ? (arg.resource as SamAppLocation).samTemplateUri : await promptUserForTemplate() if (!templateUri || !(await fs.exists(templateUri))) { @@ -98,19 +569,24 @@ export async function getLambdaHandlerFile( }) } + // if this function is used to get handler from a just downloaded lambda function zip. codeUri will be '' + if (codeUri !== '') { + folderUri = vscode.Uri.joinPath(folderUri, codeUri) + } + const handlerParts = handler.split('.') // sample: app.lambda_handler -> app.rb if (family === RuntimeFamily.Ruby) { // Ruby supports namespace/class handlers as well, but the path is // guaranteed to be slash-delimited so we can assume the first part is // the path - return vscode.Uri.joinPath(folderUri, codeUri, handlerParts.slice(0, handlerParts.length - 1).join('/') + '.rb') + return vscode.Uri.joinPath(folderUri, handlerParts.slice(0, handlerParts.length - 1).join('/') + '.rb') } // sample:app.lambda_handler -> app.py if (family === RuntimeFamily.Python) { // Otherwise (currently Node.js and Python) handle dot-delimited paths - return vscode.Uri.joinPath(folderUri, codeUri, handlerParts.slice(0, handlerParts.length - 1).join('/') + '.py') + return vscode.Uri.joinPath(folderUri, handlerParts.slice(0, handlerParts.length - 1).join('/') + '.py') } // sample: app.handler -> app.mjs/app.js @@ -120,8 +596,8 @@ export async function getLambdaHandlerFile( const handlerPath = path.dirname(handlerName) const handlerFile = path.basename(handlerName) const pattern = new vscode.RelativePattern( - vscode.Uri.joinPath(folderUri, codeUri, handlerPath), - `${handlerFile}.{js,mjs}` + vscode.Uri.joinPath(folderUri, handlerPath), + `${handlerFile}.{js,mjs,cjs,ts}` ) return searchHandlerFile(folderUri, pattern) } @@ -129,14 +605,14 @@ export async function getLambdaHandlerFile( // sample: ImageResize::ImageResize.Function::FunctionHandler -> Function.cs if (family === RuntimeFamily.DotNet) { const handlerName = path.basename(handler.split('::')[1].replaceAll('.', '/')) - const pattern = new vscode.RelativePattern(vscode.Uri.joinPath(folderUri, codeUri), `${handlerName}.cs`) + const pattern = new vscode.RelativePattern(folderUri, `${handlerName}.cs`) return searchHandlerFile(folderUri, pattern) } // sample: resizer.App::handleRequest -> App.java if (family === RuntimeFamily.Java) { const handlerName = handler.split('::')[0].replaceAll('.', '/') - const pattern = new vscode.RelativePattern(vscode.Uri.joinPath(folderUri, codeUri), `**/${handlerName}.java`) + const pattern = new vscode.RelativePattern(folderUri, `**/${handlerName}.java`) return searchHandlerFile(folderUri, pattern) } } @@ -199,3 +675,35 @@ export async function deployTypePrompt() { } return selected } + +export async function downloadUnzip(url: string, destination: vscode.Uri) { + const response = await fetch(url) + if (!response.ok) { + throw new Error(`Failed to download Lambda layer code: ${response.statusText}`) + } + + // Get the response as an ArrayBuffer + const arrayBuffer = await response.arrayBuffer() + const zipBuffer = Buffer.from(arrayBuffer) + + // Create AdmZip instance with the buffer + const zip = new AdmZip(zipBuffer) + + // Create output directory if it doesn't exist + if (!(await fs.exists(destination))) { + await fs.mkdir(destination) + } + + // Extract zip contents to output path + zip.extractAllTo(destination.fsPath, true) +} + +export function getLambdaClient(region: string): EnhancedLambdaClient { + const originalClient = new DefaultLambdaClient(region) + return new EnhancedLambdaClient(originalClient, region) +} + +export async function getCFNClient(regionCode: string): Promise { + const originalClient = await globals.sdkClientBuilder.createAwsService(CloudFormation, {}, regionCode) + return new EnhancedCloudFormationClient(originalClient, regionCode) +} diff --git a/packages/core/src/awsService/appBuilder/walkthrough.ts b/packages/core/src/awsService/appBuilder/walkthrough.ts index 098e78584f6..e1a0954864b 100644 --- a/packages/core/src/awsService/appBuilder/walkthrough.ts +++ b/packages/core/src/awsService/appBuilder/walkthrough.ts @@ -16,7 +16,7 @@ import { ToolkitError } from '../../shared/errors' import { createSingleFileDialog } from '../../shared/ui/common/openDialog' import { fs } from '../../shared/fs/fs' import path from 'path' -import { telemetry } from '../../shared/telemetry/telemetry' +import { telemetry, ToolId } from '../../shared/telemetry/telemetry' import { minSamCliVersionForAppBuilderSupport } from '../../shared/sam/cli/samCliValidator' import { SamCliInfoInvocation } from '../../shared/sam/cli/samCliInfo' @@ -347,3 +347,34 @@ export async function getOrInstallCliWrapper(toolId: AwsClis, source: string) { } }) } + +export async function installLocalStackExtension(source: string) { + await telemetry.appBuilder_installTool.run(async (span) => { + // TODO: Update `ToolId` accepted values: https://github.com/aws/aws-toolkit-common/blob/8c88537fae2ac7e6524fb2b29ae336c606850eeb/telemetry/definitions/commonDefinitions.json#L2215-L2221 + // @ts-ignore + const toolId: ToolId = 'localstack' + span.record({ source, toolId }) + const extensionId = 'localstack.localstack' + const extension = vscode.extensions.getExtension(extensionId) + if (extension) { + void vscode.window.showInformationMessage( + localize( + 'AWS.toolkit.lambda.walkthrough.localStackExtension.alreadyInstalled', + 'LocalStack extension is already installed' + ) + ) + } else { + try { + await vscode.commands.executeCommand('workbench.extensions.installExtension', extensionId) + void vscode.window.showInformationMessage( + localize( + 'AWS.toolkit.lambda.walkthrough.localStackExtension.installSuccessful', + 'LocalStack extension has been installed' + ) + ) + } catch (err) { + throw ToolkitError.chain(err, 'Failed to install LocalStack extension') + } + } + }) +} diff --git a/packages/core/src/awsService/cloudWatchLogs/activation.ts b/packages/core/src/awsService/cloudWatchLogs/activation.ts index 4c960bb1d03..03cf23c235c 100644 --- a/packages/core/src/awsService/cloudWatchLogs/activation.ts +++ b/packages/core/src/awsService/cloudWatchLogs/activation.ts @@ -23,10 +23,13 @@ import { clearDocument, closeSession, tailLogGroup } from './commands/tailLogGro import { LiveTailDocumentProvider } from './document/liveTailDocumentProvider' import { LiveTailSessionRegistry } from './registry/liveTailSessionRegistry' import { DeployedResourceNode } from '../appBuilder/explorer/nodes/deployedNode' -import { isTreeNode } from '../../shared/treeview/resourceTreeDataProvider' +import { isTreeNode, TreeNode } from '../../shared/treeview/resourceTreeDataProvider' import { getLogger } from '../../shared/logger/logger' import { ToolkitError } from '../../shared/errors' import { LiveTailCodeLensProvider } from './document/liveTailCodeLensProvider' +import { generateLambdaNodeFromResource } from '../appBuilder/explorer/nodes/resourceNode' +import { LambdaFunctionNode } from '../../lambda/explorer/lambdaFunctionNode' +import { getSourceNode } from '../../shared/utilities/treeNodeUtils' export const liveTailRegistry = LiveTailSessionRegistry.instance export const liveTailCodeLensProvider = new LiveTailCodeLensProvider(liveTailRegistry) @@ -132,14 +135,18 @@ export async function activate(context: vscode.ExtensionContext, configuration: await clearDocument(document) }), - Commands.register('aws.appBuilder.searchLogs', async (node: DeployedResourceNode) => { + Commands.register('aws.appBuilder.searchLogs', async (node: DeployedResourceNode | TreeNode) => { try { - const logGroupInfo = isTreeNode(node) - ? { - regionName: node.resource.regionCode, - groupName: getFunctionLogGroupName(node.resource.explorerNode.configuration), - } - : undefined + let tmpNode: LambdaFunctionNode | undefined = getSourceNode(node) + if (!tmpNode && isTreeNode(node)) { + // failed to extract, meaning this is appbuilder function node + tmpNode = await generateLambdaNodeFromResource(node.resource as any) + } + const logGroupInfo = { + regionName: tmpNode.regionCode, + groupName: getFunctionLogGroupName(tmpNode.configuration), + } + const source: string = logGroupInfo ? 'AppBuilderSearchLogs' : 'CommandPaletteSearchLogs' await searchLogGroup(registry, source, logGroupInfo) } catch (err) { diff --git a/packages/core/src/awsService/cloudWatchLogs/registry/liveTailSession.ts b/packages/core/src/awsService/cloudWatchLogs/registry/liveTailSession.ts index 6ec785e76c6..a2364f07460 100644 --- a/packages/core/src/awsService/cloudWatchLogs/registry/liveTailSession.ts +++ b/packages/core/src/awsService/cloudWatchLogs/registry/liveTailSession.ts @@ -6,6 +6,7 @@ import * as vscode from 'vscode' import * as AWS from '@aws-sdk/types' import { CloudWatchLogsClient, + type CloudWatchLogsClientConfig, StartLiveTailCommand, StartLiveTailResponseStream, } from '@aws-sdk/client-cloudwatch-logs' @@ -53,12 +54,17 @@ export class LiveTailSession { this._logGroupArn = configuration.logGroupArn this.logStreamFilter = configuration.logStreamFilter this.logEventFilterPattern = configuration.logEventFilterPattern + const cwlClientProps: CloudWatchLogsClientConfig = { + credentials: configuration.awsCredentials, + region: configuration.region, + customUserAgent: getUserAgent(), + } + const endpointUrl = globals.awsContext.getCredentialEndpointUrl() + if (endpointUrl !== undefined) { + cwlClientProps.endpoint = endpointUrl + } this.liveTailClient = { - cwlClient: new CloudWatchLogsClient({ - credentials: configuration.awsCredentials, - region: configuration.region, - customUserAgent: getUserAgent(), - }), + cwlClient: new CloudWatchLogsClient(cwlClientProps), abortController: new AbortController(), } this._maxLines = LiveTailSession.settings.get('limit', 10000) diff --git a/packages/core/src/awsService/sagemaker/activation.ts b/packages/core/src/awsService/sagemaker/activation.ts new file mode 100644 index 00000000000..da8392ebad4 --- /dev/null +++ b/packages/core/src/awsService/sagemaker/activation.ts @@ -0,0 +1,82 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as path from 'path' +import * as vscode from 'vscode' +import { Commands } from '../../shared/vscode/commands2' +import { SagemakerSpaceNode } from './explorer/sagemakerSpaceNode' +import { SagemakerParentNode } from './explorer/sagemakerParentNode' +import * as uriHandlers from './uriHandlers' +import { openRemoteConnect, filterSpaceAppsByDomainUserProfiles, stopSpace } from './commands' +import { updateIdleFile, startMonitoringTerminalActivity, ActivityCheckInterval } from './utils' +import { ExtContext } from '../../shared/extensions' +import { telemetry } from '../../shared/telemetry/telemetry' +import { isSageMaker, UserActivity } from '../../shared/extensionUtilities' + +let terminalActivityInterval: NodeJS.Timeout | undefined + +export async function activate(ctx: ExtContext): Promise { + ctx.extensionContext.subscriptions.push( + uriHandlers.register(ctx), + Commands.register('aws.sagemaker.openRemoteConnection', async (node: SagemakerSpaceNode) => { + if (!validateNode(node)) { + return + } + await telemetry.sagemaker_openRemoteConnection.run(async () => { + await openRemoteConnect(node, ctx.extensionContext) + }) + }), + + Commands.register('aws.sagemaker.filterSpaceApps', async (node: SagemakerParentNode) => { + await telemetry.sagemaker_filterSpaces.run(async () => { + await filterSpaceAppsByDomainUserProfiles(node) + }) + }), + + Commands.register('aws.sagemaker.stopSpace', async (node: SagemakerSpaceNode) => { + if (!validateNode(node)) { + return + } + await telemetry.sagemaker_stopSpace.run(async () => { + await stopSpace(node, ctx.extensionContext) + }) + }) + ) + + // If running in SageMaker AI Space, track user activity for autoshutdown feature + if (isSageMaker('SMAI')) { + // Use /tmp/ directory so the file is cleared on each reboot to prevent stale timestamps. + const tmpDirectory = '/tmp/' + const idleFilePath = path.join(tmpDirectory, '.sagemaker-last-active-timestamp') + + const userActivity = new UserActivity(ActivityCheckInterval) + userActivity.onUserActivity(() => updateIdleFile(idleFilePath)) + + terminalActivityInterval = startMonitoringTerminalActivity(idleFilePath) + + // Write initial timestamp + await updateIdleFile(idleFilePath) + + ctx.extensionContext.subscriptions.push(userActivity, { + dispose: () => { + if (terminalActivityInterval) { + clearInterval(terminalActivityInterval) + terminalActivityInterval = undefined + } + }, + }) + } +} + +/** + * Checks if a node is undefined and shows a warning message if so. + */ +function validateNode(node: unknown): 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/awsService/sagemaker/commands.ts b/packages/core/src/awsService/sagemaker/commands.ts new file mode 100644 index 00000000000..64266c556e1 --- /dev/null +++ b/packages/core/src/awsService/sagemaker/commands.ts @@ -0,0 +1,210 @@ +/*! + * 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 { SagemakerConstants } from './explorer/constants' +import { SagemakerParentNode } from './explorer/sagemakerParentNode' +import { DomainKeyDelimiter } from './utils' +import { startVscodeRemote } from '../../shared/extensions/ssh' +import { getLogger } from '../../shared/logger/logger' +import { SagemakerSpaceNode, tryRefreshNode } from './explorer/sagemakerSpaceNode' +import { isRemoteWorkspace } from '../../shared/vscode/env' +import _ from 'lodash' +import { prepareDevEnvConnection, tryRemoteConnection } from './model' +import { ExtContext } from '../../shared/extensions' +import { SagemakerClient } from '../../shared/clients/sagemaker' +import { ToolkitError } from '../../shared/errors' +import { showConfirmationMessage } from '../../shared/utilities/messages' +import { RemoteSessionError } from '../../shared/remoteSession' +import { ConnectFromRemoteWorkspaceMessage, InstanceTypeError } from './constants' +import { SagemakerUnifiedStudioSpaceNode } from '../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpaceNode' + +const localize = nls.loadMessageBundle() + +export async function filterSpaceAppsByDomainUserProfiles(parentNode: SagemakerParentNode): Promise { + if (parentNode.domainUserProfiles.size === 0) { + // if parentNode has not been expanded, domainUserProfiles will be empty + // if so, this will attempt to populate domainUserProfiles + await parentNode.updateChildren() + if (parentNode.domainUserProfiles.size === 0) { + getLogger().info(SagemakerConstants.NoSpaceToFilter) + void vscode.window.showInformationMessage(SagemakerConstants.NoSpaceToFilter) + return + } + } + + // Sort by domain name and user profile + const sortedDomainUserProfiles = new Map( + [...parentNode.domainUserProfiles].sort((a, b) => { + const domainNameA = a[1].domain.DomainName || '' + const domainNameB = b[1].domain.DomainName || '' + + const [_domainIdA, userProfileA] = a[0].split(DomainKeyDelimiter) + const [_domainIdB, userProfileB] = b[0].split(DomainKeyDelimiter) + + return domainNameA.localeCompare(domainNameB) || userProfileA.localeCompare(userProfileB) + }) + ) + + const previousSelection = await parentNode.getSelectedDomainUsers() + const items: (vscode.QuickPickItem & { key: string })[] = [] + + for (const [key, userMetadata] of sortedDomainUserProfiles) { + const [_, userProfile] = key.split(DomainKeyDelimiter) + items.push({ + label: userProfile, + detail: `In domain: ${userMetadata.domain?.DomainName}`, + picked: previousSelection.has(key), + key, + }) + } + + const placeholder = localize(SagemakerConstants.FilterPlaceholderKey, SagemakerConstants.FilterPlaceholderMessage) + const result = await vscode.window.showQuickPick(items, { + placeHolder: placeholder, + canPickMany: true, + matchOnDetail: true, + }) + + if (!result) { + return // User canceled. + } + + const newSelection = result.map((r) => r.key) + if (newSelection.length !== previousSelection.size || newSelection.some((key) => !previousSelection.has(key))) { + parentNode.saveSelectedDomainUsers(newSelection) + await vscode.commands.executeCommand('aws.refreshAwsExplorerNode', parentNode) + } +} + +export async function deeplinkConnect( + ctx: ExtContext, + connectionIdentifier: string, + session: string, + wsUrl: string, + token: string, + domain: string +) { + getLogger().debug( + `sm:deeplinkConnect: connectionIdentifier: ${connectionIdentifier} session: ${session} wsUrl: ${wsUrl} token: ${token}` + ) + + if (isRemoteWorkspace()) { + void vscode.window.showErrorMessage(ConnectFromRemoteWorkspaceMessage) + return + } + + try { + const remoteEnv = await prepareDevEnvConnection( + connectionIdentifier, + ctx.extensionContext, + 'sm_dl', + false /* isSMUS */, + undefined /* node */, + session, + wsUrl, + token, + domain + ) + + await startVscodeRemote( + remoteEnv.SessionProcess, + remoteEnv.hostname, + '/home/sagemaker-user', + remoteEnv.vscPath, + 'sagemaker-user' + ) + } catch (err: any) { + getLogger().error( + `sm:OpenRemoteConnect: Unable to connect to target space with arn: ${connectionIdentifier} error: ${err}` + ) + + if (![RemoteSessionError.MissingExtension, RemoteSessionError.ExtensionVersionTooLow].includes(err.code)) { + throw err + } + } +} + +export async function stopSpace( + node: SagemakerSpaceNode | SagemakerUnifiedStudioSpaceNode, + ctx: vscode.ExtensionContext, + sageMakerClient?: SagemakerClient +) { + const spaceName = node.spaceApp.SpaceName! + const confirmed = await showConfirmationMessage({ + prompt: `You are about to stop this space. Any active resource will also be stopped. Are you sure you want to stop the space?`, + confirm: 'Stop Space', + cancel: 'Cancel', + type: 'warning', + }) + + if (!confirmed) { + return + } + // In case of SMUS, we pass in a SM Client and for SM AI, it creates a new SM Client. + const client = sageMakerClient ? sageMakerClient : new SagemakerClient(node.regionCode) + try { + await client.deleteApp({ + DomainId: node.spaceApp.DomainId!, + SpaceName: spaceName, + AppType: node.spaceApp.App!.AppType!, + AppName: node.spaceApp.App?.AppName, + }) + } catch (err) { + const error = err as Error + if (error.name === 'AccessDeniedException') { + throw new ToolkitError('You do not have permission to stop spaces. Please contact your administrator', { + cause: error, + code: error.name, + }) + } else { + throw new ToolkitError(`Failed to stop space ${spaceName}: ${(error as Error).message}`, { + cause: error, + code: error.name, + }) + } + } + await tryRefreshNode(node) +} + +export async function openRemoteConnect( + node: SagemakerSpaceNode | SagemakerUnifiedStudioSpaceNode, + ctx: vscode.ExtensionContext, + sageMakerClient?: SagemakerClient +) { + if (isRemoteWorkspace()) { + void vscode.window.showErrorMessage(ConnectFromRemoteWorkspaceMessage) + return + } + + if (node.getStatus() === 'Stopped') { + // In case of SMUS, we pass in a SM Client and for SM AI, it creates a new SM Client. + const client = sageMakerClient ? sageMakerClient : new SagemakerClient(node.regionCode) + + try { + await client.startSpace(node.spaceApp.SpaceName!, node.spaceApp.DomainId!) + await tryRefreshNode(node) + const appType = node.spaceApp.SpaceSettingsSummary?.AppType + if (!appType) { + throw new ToolkitError('AppType is undefined for the selected space. Cannot start remote connection.', { + code: 'undefinedAppType', + }) + } + await client.waitForAppInService(node.spaceApp.DomainId!, node.spaceApp.SpaceName!, appType) + await tryRemoteConnection(node, ctx) + } catch (err: any) { + // Ignore InstanceTypeError since it means the user decided not to use an instanceType with more memory + if (err.code !== InstanceTypeError) { + throw new ToolkitError(`Remote connection failed: ${(err as Error).message}`, { + cause: err as Error, + code: err.code, + }) + } + } + } else if (node.getStatus() === 'Running') { + await tryRemoteConnection(node, ctx) + } +} diff --git a/packages/core/src/awsService/sagemaker/constants.ts b/packages/core/src/awsService/sagemaker/constants.ts new file mode 100644 index 00000000000..1fc51a1d20d --- /dev/null +++ b/packages/core/src/awsService/sagemaker/constants.ts @@ -0,0 +1,31 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +export const ConnectFromRemoteWorkspaceMessage = + 'Unable to establish new remote connection. Your last active VS Code window is connected to a remote workspace. To open a new SageMaker Studio connection, select your local VS Code window and try again.' + +export const InstanceTypeError = 'InstanceTypeError' + +export const InstanceTypeMinimum = 'ml.t3.large' + +export const InstanceTypeInsufficientMemory: Record = { + 'ml.t3.medium': 'ml.t3.large', + 'ml.c7i.large': 'ml.c7i.xlarge', + 'ml.c6i.large': 'ml.c6i.xlarge', + 'ml.c6id.large': 'ml.c6id.xlarge', + 'ml.c5.large': 'ml.c5.xlarge', +} + +export const InstanceTypeInsufficientMemoryMessage = ( + spaceName: string, + chosenInstanceType: string, + recommendedInstanceType: string +) => { + return `Unable to create app for [${spaceName}] because instanceType [${chosenInstanceType}] is not supported for remote access enabled spaces. Use instanceType with at least 8 GiB memory. Would you like to start your space with instanceType [${recommendedInstanceType}]?` +} + +export const InstanceTypeNotSelectedMessage = (spaceName: string) => { + return `No instanceType specified for [${spaceName}]. ${InstanceTypeMinimum} is the default instance type, which meets minimum 8 GiB memory requirements for remote access. Continuing will start your space with instanceType [${InstanceTypeMinimum}] and remotely connect.` +} diff --git a/packages/core/src/awsService/sagemaker/credentialMapping.ts b/packages/core/src/awsService/sagemaker/credentialMapping.ts new file mode 100644 index 00000000000..3eb54feed36 --- /dev/null +++ b/packages/core/src/awsService/sagemaker/credentialMapping.ts @@ -0,0 +1,203 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as path from 'path' +import * as os from 'os' +import { fs } from '../../shared/fs/fs' +import globals from '../../shared/extensionGlobals' +import { ToolkitError } from '../../shared/errors' +import { DevSettings } from '../../shared/settings' +import { Auth } from '../../auth/auth' +import { SpaceMappings, SsmConnectionInfo } from './types' +import { getLogger } from '../../shared/logger/logger' +import { parseArn } from './detached-server/utils' +import { SagemakerUnifiedStudioSpaceNode } from '../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpaceNode' +import { SageMakerUnifiedStudioSpacesParentNode } from '../../sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpacesParentNode' + +const mappingFileName = '.sagemaker-space-profiles' +const mappingFilePath = path.join(os.homedir(), '.aws', mappingFileName) + +export async function loadMappings(): Promise { + try { + if (!(await fs.existsFile(mappingFilePath))) { + return {} + } + + const raw = await fs.readFileText(mappingFilePath) + return raw ? JSON.parse(raw) : {} + } catch (error) { + getLogger().error(`Failed to load space mappings from ${mappingFilePath}:`, error) + return {} + } +} + +export async function saveMappings(data: SpaceMappings): Promise { + try { + await fs.writeFile(mappingFilePath, JSON.stringify(data, undefined, 2), { + mode: 0o600, + atomic: true, + }) + } catch (error) { + getLogger().error(`Failed to save space mappings to ${mappingFilePath}:`, error) + } +} + +/** + * Persists the current profile to the appropriate space mapping based on connection type and profile format. + * @param spaceArn - The arn for the SageMaker space. + */ +export async function persistLocalCredentials(spaceArn: string): Promise { + const currentProfileId = Auth.instance.getCurrentProfileId() + if (!currentProfileId) { + throw new ToolkitError('No current profile ID available for saving space credentials.') + } + + if (currentProfileId.startsWith('sso:')) { + const credentials = globals.loginManager.store.credentialsCache[currentProfileId] + await setSpaceSsoProfile( + spaceArn, + credentials.credentials.accessKeyId, + credentials.credentials.secretAccessKey, + credentials.credentials.sessionToken ?? '' + ) + } else { + await setSpaceIamProfile(spaceArn, currentProfileId) + } +} + +/** + * Persists the current selected SMUS Project Role creds to the appropriate space mapping. + * @param spaceArn - The identifier for the SageMaker Space. + */ +export async function persistSmusProjectCreds(spaceArn: string, node: SagemakerUnifiedStudioSpaceNode): Promise { + const nodeParent = node.getParent() as SageMakerUnifiedStudioSpacesParentNode + const authProvider = nodeParent.getAuthProvider() + const projectId = nodeParent.getProjectId() + const projectAuthProvider = await authProvider.getProjectCredentialProvider(projectId) + await projectAuthProvider.getCredentials() + await setSmusSpaceSsoProfile(spaceArn, projectId) + // Trigger SSH credential refresh for the project + projectAuthProvider.startProactiveCredentialRefresh() +} + +/** + * Persists deep link credentials for a SageMaker space using a derived refresh URL based on environment. + * + * @param spaceArn - ARN of the SageMaker space. + * @param domain - The domain ID associated with the space. + * @param session - SSM session ID. + * @param wsUrl - SSM WebSocket URL. + * @param token - Bearer token for the session. + */ +export async function persistSSMConnection( + spaceArn: string, + domain: string, + session?: string, + wsUrl?: string, + token?: string +): Promise { + const { region } = parseArn(spaceArn) + const endpoint = DevSettings.instance.get('endpoints', {})['sagemaker'] ?? '' + + // TODO: Hardcoded to 'jupyterlab' due to a bug in Studio that only supports refreshing + // the token for both CodeEditor and JupyterLab Apps in the jupyterlab subdomain. + // This will be fixed shortly after NYSummit launch to support refresh URL in CodeEditor subdomain. + const appSubDomain = 'jupyterlab' + + let envSubdomain: string + + if (endpoint.includes('beta')) { + envSubdomain = 'devo' + } else if (endpoint.includes('gamma')) { + envSubdomain = 'loadtest' + } else { + envSubdomain = 'studio' + } + + // Use the standard AWS domain for 'studio' (prod). + // For non-prod environments, use the obfuscated domain 'asfiovnxocqpcry.com'. + const baseDomain = + envSubdomain === 'studio' + ? `studio.${region}.sagemaker.aws` + : `${envSubdomain}.studio.${region}.asfiovnxocqpcry.com` + + const refreshUrl = `https://studio-${domain}.${baseDomain}/${appSubDomain}` + await setSpaceCredentials(spaceArn, refreshUrl, { + sessionId: session ?? '-', + url: wsUrl ?? '-', + token: token ?? '-', + }) +} + +/** + * Sets or updates an IAM credential profile for a given space. + * @param spaceArn - The name of the SageMaker space. + * @param profileName - The local AWS profile name to associate. + */ +export async function setSpaceIamProfile(spaceArn: string, profileName: string): Promise { + const data = await loadMappings() + data.localCredential ??= {} + data.localCredential[spaceArn] = { type: 'iam', profileName } + await saveMappings(data) +} + +/** + * Sets or updates an SSO credential profile for a given space. + * @param spaceArn - The arn of the SageMaker space. + * @param accessKey - Temporary access key from SSO. + * @param secret - Temporary secret key from SSO. + * @param token - Session token from SSO. + */ +export async function setSpaceSsoProfile( + spaceArn: string, + accessKey: string, + secret: string, + token: string +): Promise { + const data = await loadMappings() + data.localCredential ??= {} + data.localCredential[spaceArn] = { type: 'sso', accessKey, secret, token } + await saveMappings(data) +} + +/** + * Sets the SM Space to map to SageMaker Unified Studio Project. + * @param spaceArn - The arn of the SageMaker Unified Studio space. + * @param projectId - The project ID associated with the SageMaker Unified Studio space. + */ +export async function setSmusSpaceSsoProfile(spaceArn: string, projectId: string): Promise { + const data = await loadMappings() + data.localCredential ??= {} + data.localCredential[spaceArn] = { type: 'sso', smusProjectId: projectId } + await saveMappings(data) +} + +/** + * Stores SSM connection information for a given space, typically from a deep link session. + * This initializes the request as 'fresh' and includes a refresh URL if provided. + * @param spaceArn - The arn of the SageMaker space. + * @param refreshUrl - URL to use for refreshing session tokens. + * @param credentials - The session information used to initiate the connection. + */ +export async function setSpaceCredentials( + spaceArn: string, + refreshUrl: string, + credentials: SsmConnectionInfo +): Promise { + const data = await loadMappings() + data.deepLink ??= {} + + data.deepLink[spaceArn] = { + refreshUrl, + requests: { + 'initial-connection': { + ...credentials, + status: 'fresh', + }, + }, + } + + await saveMappings(data) +} diff --git a/packages/core/src/awsService/sagemaker/detached-server/credentials.ts b/packages/core/src/awsService/sagemaker/detached-server/credentials.ts new file mode 100644 index 00000000000..748679309c8 --- /dev/null +++ b/packages/core/src/awsService/sagemaker/detached-server/credentials.ts @@ -0,0 +1,67 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { fromIni } from '@aws-sdk/credential-providers' +import { LocalCredentialProfile } from '../types' +import { readMapping } from './utils' + +/** + * Resolves AWS credentials for a given SageMaker space connection identifier + * using the 'lc' (local connection) credential mapping. + * + * Supported profile types: + * - 'iam': Looks up credentials from AWS config using profile name. + * - 'sso': Uses accessKey, secret, and sessionToken from the mapping file. + * + * @param connectionIdentifier - The ARN or space ID used to locate the profile in the mapping. + * @returns A Promise that resolves to AWS credentials compatible with AWS SDK v3. + * @throws If the profile is missing, malformed, or unsupported. + */ +export async function resolveCredentialsFor(connectionIdentifier: string): Promise { + const mapping = await readMapping() + const profile = mapping.localCredential?.[connectionIdentifier] as LocalCredentialProfile + + if (!profile) { + throw new Error(`No profile found for "${connectionIdentifier}"`) + } + + switch (profile.type) { + case 'iam': { + const name = profile.profileName?.split(':')[1] + if (!name) { + throw new Error(`Invalid IAM profile name for "${connectionIdentifier}"`) + } + return fromIni({ profile: name }) + } + case 'sso': { + if ('accessKey' in profile && 'secret' in profile && 'token' in profile) { + const { accessKey, secret, token } = profile + if (!accessKey || !secret || !token) { + throw new Error(`Missing SSO credentials for "${connectionIdentifier}"`) + } + return { + accessKeyId: accessKey, + secretAccessKey: secret, + sessionToken: token, + } + } else if ('smusProjectId' in profile) { + // Handle SMUS project ID case + const { accessKey, secret, token } = mapping.smusProjects?.[profile.smusProjectId] || {} + if (!accessKey || !secret || !token) { + throw new Error(`Missing ProjectRole credentials for SMUS Space "${connectionIdentifier}"`) + } + return { + accessKeyId: accessKey, + secretAccessKey: secret, + sessionToken: token, + } + } else { + throw new Error(`Missing SSO credentials for "${connectionIdentifier}"`) + } + } + default: + throw new Error(`Unsupported profile type "${profile}"`) + } +} diff --git a/packages/core/src/awsService/sagemaker/detached-server/errorPage.ts b/packages/core/src/awsService/sagemaker/detached-server/errorPage.ts new file mode 100644 index 00000000000..bff3e62ae61 --- /dev/null +++ b/packages/core/src/awsService/sagemaker/detached-server/errorPage.ts @@ -0,0 +1,148 @@ +/*! + * 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 { randomUUID } from 'crypto' +import { join } from 'path' +import { promises as fs } from 'fs' +import os from 'os' +import { SageMakerServiceException } from '@amzn/sagemaker-client' +import { open } from './utils' + +export enum ExceptionType { + ACCESS_DENIED = 'AccessDeniedException', + DEFAULT = 'Default', + EXPIRED_TOKEN = 'ExpiredTokenException', + INTERNAL_FAILURE = 'InternalFailure', + RESOURCE_LIMIT_EXCEEDED = 'ResourceLimitExceeded', + THROTTLING = 'ThrottlingException', + VALIDATION = 'ValidationException', +} + +export const getVSCodeErrorTitle = (error: SageMakerServiceException): string => { + const exceptionType = error.name as ExceptionType + + if (exceptionType in ErrorText.StartSession) { + return ErrorText.StartSession[exceptionType].Title + } + + return ErrorText.StartSession[ExceptionType.DEFAULT].Title +} + +export const getVSCodeErrorText = (error: SageMakerServiceException, isSmus?: boolean): string => { + const exceptionType = error.name as ExceptionType + + switch (exceptionType) { + case ExceptionType.ACCESS_DENIED: + case ExceptionType.VALIDATION: + return ErrorText.StartSession[exceptionType].Text.replace('{message}', error.message) + case ExceptionType.EXPIRED_TOKEN: + // Use SMUS-specific message if in SMUS context + return isSmus + ? ErrorText.StartSession[ExceptionType.EXPIRED_TOKEN].SmusText + : ErrorText.StartSession[exceptionType].Text + case ExceptionType.INTERNAL_FAILURE: + case ExceptionType.RESOURCE_LIMIT_EXCEEDED: + case ExceptionType.THROTTLING: + return ErrorText.StartSession[exceptionType].Text + default: + return ErrorText.StartSession[ExceptionType.DEFAULT].Text.replace('{exceptionType}', exceptionType) + } +} + +export const ErrorText = { + StartSession: { + [ExceptionType.ACCESS_DENIED]: { + Title: 'Remote access denied', + Text: 'Unable to connect because: [{message}]', + }, + [ExceptionType.DEFAULT]: { + Title: 'Unexpected system error', + Text: 'We encountered an unexpected error: [{exceptionType}]. Please contact your administrator and provide them with this error so they can investigate the issue.', + }, + [ExceptionType.EXPIRED_TOKEN]: { + Title: 'Authentication expired', + Text: 'Your session has expired. Please refresh your credentials and try again.', + SmusText: + 'Your session has expired. This is likely due to network connectivity issues after machine sleep/resume. Please wait 10-30 seconds for automatic credential refresh, then try again. If the issue persists, try reconnecting through AWS Toolkit.', + }, + [ExceptionType.INTERNAL_FAILURE]: { + Title: 'Failed to connect remotely to VSCode', + Text: 'Unable to establish remote connection to VSCode. This could be due to several factors. Please try again by clicking the VSCode button. If the problem persists, please contact your admin.', + }, + [ExceptionType.RESOURCE_LIMIT_EXCEEDED]: { + Title: 'Connection limit reached', + Text: 'You have 10 active remote connections to this space. Stop an existing connection to start a new one.', + }, + [ExceptionType.THROTTLING]: { + Title: 'Too many connection attempts', + Text: "You're connecting too quickly. Wait a moment and try again.", + }, + [ExceptionType.VALIDATION]: { + Title: 'Configuration error', + Text: 'The operation cannot be completed due to: [{message}]', + }, + }, +} + +export async function openErrorPage(title: string, message: string) { + const html = ` + + + + ${title} + + + +
+
+
${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..cfac5984e9b --- /dev/null +++ b/packages/core/src/awsService/sagemaker/detached-server/utils.ts @@ -0,0 +1,168 @@ +/*! + * 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' +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> = [] + +/** + * 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 }) + 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..dd445f344fb --- /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 { GetCallerIdentityResponse } from 'aws-sdk/clients/sts' +import { DescribeDomainResponse } from '@amzn/sagemaker-client' +import { SagemakerClient, SagemakerSpaceApp } from '../../../shared/clients/sagemaker' +import { DefaultStsClient } from '../../../shared/clients/stsClient' +import globals from '../../../shared/extensionGlobals' +import { AWSTreeNodeBase } from '../../../shared/treeview/nodes/awsTreeNodeBase' +import { PlaceholderNode } from '../../../shared/treeview/nodes/placeholderNode' +import { makeChildrenNodes } from '../../../shared/treeview/utils' +import { updateInPlace } from '../../../shared/utilities/collectionUtils' +import { isRemoteWorkspace } from '../../../shared/vscode/env' +import { SagemakerConstants } from './constants' +import { SagemakerSpaceNode } from './sagemakerSpaceNode' +import { getDomainSpaceKey, getDomainUserProfileKey, getSpaceAppsForUserProfile } from '../utils' +import { PollingSet } from '../../../shared/utilities/pollingSet' +import { getRemoteAppMetadata } from '../remoteUtils' + +export const parentContextValue = 'awsSagemakerParentNode' + +export type SelectedDomainUsers = [string, string[]][] +export type SelectedDomainUsersByRegion = [string, SelectedDomainUsers][] + +export interface UserProfileMetadata { + domain: DescribeDomainResponse +} +export class SagemakerParentNode extends AWSTreeNodeBase { + protected sagemakerSpaceNodes: Map + protected stsClient: DefaultStsClient + public override readonly contextValue: string = parentContextValue + domainUserProfiles: Map = new Map() + spaceApps: Map = new Map() + callerIdentity: GetCallerIdentityResponse = {} + public readonly pollingSet: PollingSet = new PollingSet(5000, this.updatePendingNodes.bind(this)) + + 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..cd0c1e43173 --- /dev/null +++ b/packages/core/src/awsService/sagemaker/model.ts @@ -0,0 +1,238 @@ +/*! + * 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 +) { + const spaceArn = (await node.getSpaceArn()) as string + const isSMUS = node instanceof SagemakerUnifiedStudioSpaceNode + const remoteEnv = await prepareDevEnvConnection(spaceArn, ctx, 'sm_lc', isSMUS, node) + try { + 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 +) { + 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) + } + + 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) => !line.split(' ')[0].split(',').includes(hostname)) + + 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..17c3c512272 --- /dev/null +++ b/packages/core/src/awsService/sagemaker/uriHandlers.ts @@ -0,0 +1,40 @@ +/*! + * 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 + ) + }) + } + + return vscode.Disposable.from(ctx.uriHandler.onPath('/connect/sagemaker', connectHandler, parseConnectParams)) +} + +export function parseConnectParams(query: SearchParams) { + const params = query.getFromKeysOrThrow( + 'connection_identifier', + 'domain', + 'user_profile', + 'session', + 'ws_url', + 'cell-number', + 'token' + ) + return params +} 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/codewhisperer/activation.ts b/packages/core/src/codewhisperer/activation.ts index d6dd7fdc61d..e037657958d 100644 --- a/packages/core/src/codewhisperer/activation.ts +++ b/packages/core/src/codewhisperer/activation.ts @@ -23,6 +23,7 @@ import { enableCodeSuggestions, toggleCodeSuggestions, showReferenceLog, + showLogs, showSecurityScan, showLearnMore, showSsoSignIn, @@ -48,7 +49,6 @@ import { regenerateFix, ignoreAllIssues, focusIssue, - showExploreAgentsView, showCodeIssueGroupingQuickPick, selectRegionProfileCommand, } from './commands/basicCommands' @@ -157,21 +157,6 @@ export async function activate(context: ExtContext): Promise { } } - if (configurationChangeEvent.affectsConfiguration('amazonQ.shareContentWithAWS')) { - if (auth.isEnterpriseSsoInUse()) { - await vscode.window - .showInformationMessage( - CodeWhispererConstants.ssoConfigAlertMessageShareData, - CodeWhispererConstants.settingsLearnMore - ) - .then(async (resp) => { - if (resp === CodeWhispererConstants.settingsLearnMore) { - void openUrl(vscode.Uri.parse(CodeWhispererConstants.learnMoreUri)) - } - }) - } - } - if (configurationChangeEvent.affectsConfiguration('editor.inlineSuggest.enabled')) { await vscode.window .showInformationMessage( @@ -299,7 +284,7 @@ export async function activate(context: ExtContext): Promise { ), vscode.window.registerWebviewViewProvider(ReferenceLogViewProvider.viewType, ReferenceLogViewProvider.instance), showReferenceLog.register(), - showExploreAgentsView.register(), + showLogs.register(), vscode.languages.registerCodeLensProvider( [...CodeWhispererConstants.platformLanguageIds], ReferenceInlineProvider.instance diff --git a/packages/core/src/codewhisperer/client/codewhisperer.ts b/packages/core/src/codewhisperer/client/codewhisperer.ts index 051254d1873..22ae0447d0a 100644 --- a/packages/core/src/codewhisperer/client/codewhisperer.ts +++ b/packages/core/src/codewhisperer/client/codewhisperer.ts @@ -227,6 +227,7 @@ export class DefaultCodeWhispererClient { product: 'CodeWhisperer', // TODO: update this? clientId: getClientId(globals.globalState), ideVersion: extensionVersion, + pluginVersion: extensionVersion, }, profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, } diff --git a/packages/core/src/codewhisperer/client/user-service-2.json b/packages/core/src/codewhisperer/client/user-service-2.json index 714937ed402..619ce74aa5b 100644 --- a/packages/core/src/codewhisperer/client/user-service-2.json +++ b/packages/core/src/codewhisperer/client/user-service-2.json @@ -29,6 +29,23 @@ "documentation": "

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

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

API to get code transformation status.

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

API to get current usage limits

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

List workspace metadata based on a workspace root

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

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

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

API to stop code transformation status.

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

API to update usage limits for enterprise customers

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

Reason for AccessDeniedException

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

Type of agent task

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

ToolUse Request

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

Indicates whether this message is a cache point

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

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

" } }, "documentation": "

Markdown text message.

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

Indicates Participant in Chat conversation

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

The type of content

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

Workspace context truncation schemes based on usecase

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

Unique identifier for the chat conversation stream

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

Unique identifier for remote workspace

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

Holds the history of chat messages.

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

Trigger Reason for Chat

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

Unique identifier for the agent task execution

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

Structure to represent the current state of a chat conversation.

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

Represents the state of an Editor

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

Structure to represent get code transformation response.

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

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

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

Number of days remaining until the usage metrics reset

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

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

", + "documentation": "

Image bytes

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

Types of input that can be processed by the model

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

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

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

Reason for InternalServerException

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

The origin context for which to list available models

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

Maximum number of models to return in a single response

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

Token for retrieving the next page of results

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

ARN of the profile to use for model filtering

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

Provider of AI models

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

List of available models

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

Token for retrieving the next page of results

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

Metadata for a single memory entry

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

Status of user memory

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

Unique identifier for the chat message

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

Unique identifier for the model

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

User-facing display name

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

Description of the model

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

Rate multiplier of the model

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

Unit for the rate multiplier

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

Limits on token usage for this model

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

List of input types supported by this model

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

Whether the model supports prompt caching

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

Unique identifier for the model

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

Identifier for the model Name

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

Provider of AI models

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

Indicates Range / Span in a Text Document

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

Reasoning content that was encrypted by the model provider

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

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

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

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

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

Recommendations with references setting for CodeWhisperer

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

DocumentSymbols parsed from a text document

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

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

" } }, "documentation": "

Represents an IDE retrieved relevant Text Document / File

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

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

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

Reason for ServiceQuotaExceededException

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

List of supported input types for the model

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

Reason for ThrottlingException

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

Maximum number of input tokens the model can process

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

Maximum number of output tokens the model can produce

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

Information about a tool that can be used.

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

The name for the tool.

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

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

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

Images associated with the Chat Message.

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

Unique identifier for the model used in this conversation

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

Indicates whether to add a cache point after the current message

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

Client cache config

" } }, "documentation": "

Structure to represent a chat input message from User.

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

Reason for ValidationException

", - "enum": ["INVALID_CONVERSATION_ID", "CONTENT_LENGTH_EXCEEDS_THRESHOLD", "INVALID_KMS_GRANT"] + "enum": [ + "INVALID_CONVERSATION_ID", + "CONTENT_LENGTH_EXCEEDS_THRESHOLD", + "INVALID_KMS_GRANT", + "INVALID_MODEL_ID" + ] }, "WorkspaceContext": { "type": "structure", diff --git a/packages/core/src/codewhisperer/commands/basicCommands.ts b/packages/core/src/codewhisperer/commands/basicCommands.ts index f2b67c49593..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' @@ -149,17 +146,29 @@ export const showReferenceLog = Commands.declare( } ) -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/invokeRecommendation.ts b/packages/core/src/codewhisperer/commands/invokeRecommendation.ts new file mode 100644 index 00000000000..37fcb965774 --- /dev/null +++ b/packages/core/src/codewhisperer/commands/invokeRecommendation.ts @@ -0,0 +1,45 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { vsCodeState, ConfigurationEntry } from '../models/model' +import { resetIntelliSenseState } from '../util/globalStateUtil' +import { DefaultCodeWhispererClient } from '../client/codewhisperer' +import { RecommendationHandler } from '../service/recommendationHandler' +import { session } from '../util/codeWhispererSession' +import { RecommendationService } from '../service/recommendationService' + +/** + * This function is for manual trigger CodeWhisperer + */ + +export async function invokeRecommendation( + editor: vscode.TextEditor, + client: DefaultCodeWhispererClient, + config: ConfigurationEntry +) { + if (!editor || !config.isManualTriggerEnabled) { + return + } + + /** + * Skip when output channel gains focus and invoke + */ + if (editor.document.languageId === 'Log') { + return + } + /** + * When using intelliSense, if invocation position changed, reject previous active recommendations + */ + if (vsCodeState.isIntelliSenseActive && editor.selection.active !== session.startPos) { + resetIntelliSenseState( + config.isManualTriggerEnabled, + config.isAutomatedTriggerEnabled, + RecommendationHandler.instance.isValidResponse() + ) + } + + await RecommendationService.instance.generateRecommendation(client, editor, 'OnDemand', config, undefined) +} diff --git a/packages/core/src/codewhisperer/commands/onAcceptance.ts b/packages/core/src/codewhisperer/commands/onAcceptance.ts new file mode 100644 index 00000000000..e13c197cefd --- /dev/null +++ b/packages/core/src/codewhisperer/commands/onAcceptance.ts @@ -0,0 +1,85 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { vsCodeState, OnRecommendationAcceptanceEntry } from '../models/model' +import { runtimeLanguageContext } from '../util/runtimeLanguageContext' +import { CodeWhispererTracker } from '../tracker/codewhispererTracker' +import { CodeWhispererCodeCoverageTracker } from '../tracker/codewhispererCodeCoverageTracker' +import { getLogger } from '../../shared/logger/logger' +import { handleExtraBrackets } from '../util/closingBracketUtil' +import { RecommendationHandler } from '../service/recommendationHandler' +import { ReferenceLogViewProvider } from '../service/referenceLogViewProvider' +import { ReferenceHoverProvider } from '../service/referenceHoverProvider' +import path from 'path' + +/** + * This function is called when user accepts a intelliSense suggestion or an inline suggestion + */ +export async function onAcceptance(acceptanceEntry: OnRecommendationAcceptanceEntry) { + RecommendationHandler.instance.cancelPaginatedRequest() + /** + * Format document + */ + if (acceptanceEntry.editor) { + const languageContext = runtimeLanguageContext.getLanguageContext( + acceptanceEntry.editor.document.languageId, + path.extname(acceptanceEntry.editor.document.fileName) + ) + const start = acceptanceEntry.range.start + const end = acceptanceEntry.range.end + + // codewhisperer will be doing editing while formatting. + // formatting should not trigger consoals auto trigger + vsCodeState.isCodeWhispererEditing = true + /** + * Mitigation to right context handling mainly for auto closing bracket use case + */ + try { + await handleExtraBrackets(acceptanceEntry.editor, end, start) + } catch (error) { + getLogger().error(`${error} in handleAutoClosingBrackets`) + } + // move cursor to end of suggestion before doing code format + // after formatting, the end position will still be editor.selection.active + acceptanceEntry.editor.selection = new vscode.Selection(end, end) + + vsCodeState.isCodeWhispererEditing = false + CodeWhispererTracker.getTracker().enqueue({ + time: new Date(), + fileUrl: acceptanceEntry.editor.document.uri, + originalString: acceptanceEntry.editor.document.getText(new vscode.Range(start, end)), + startPosition: start, + endPosition: end, + requestId: acceptanceEntry.requestId, + sessionId: acceptanceEntry.sessionId, + index: acceptanceEntry.acceptIndex, + triggerType: acceptanceEntry.triggerType, + completionType: acceptanceEntry.completionType, + language: languageContext.language, + }) + const insertedCoderange = new vscode.Range(start, end) + CodeWhispererCodeCoverageTracker.getTracker(languageContext.language)?.countAcceptedTokens( + insertedCoderange, + acceptanceEntry.editor.document.getText(insertedCoderange), + acceptanceEntry.editor.document.fileName + ) + if (acceptanceEntry.references !== undefined) { + const referenceLog = ReferenceLogViewProvider.getReferenceLog( + acceptanceEntry.recommendation, + acceptanceEntry.references, + acceptanceEntry.editor + ) + ReferenceLogViewProvider.instance.addReferenceLog(referenceLog) + ReferenceHoverProvider.instance.addCodeReferences( + acceptanceEntry.recommendation, + acceptanceEntry.references + ) + } + } + + // at the end of recommendation acceptance, report user decisions and clear recommendations. + RecommendationHandler.instance.reportUserDecisions(acceptanceEntry.acceptIndex) +} diff --git a/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts b/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts new file mode 100644 index 00000000000..d193af056f7 --- /dev/null +++ b/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts @@ -0,0 +1,145 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as CodeWhispererConstants from '../models/constants' +import { vsCodeState, OnRecommendationAcceptanceEntry } from '../models/model' +import { runtimeLanguageContext } from '../util/runtimeLanguageContext' +import { CodeWhispererTracker } from '../tracker/codewhispererTracker' +import { CodeWhispererCodeCoverageTracker } from '../tracker/codewhispererCodeCoverageTracker' +import { getLogger } from '../../shared/logger/logger' +import { RecommendationHandler } from '../service/recommendationHandler' +import { sleep } from '../../shared/utilities/timeoutUtils' +import { handleExtraBrackets } from '../util/closingBracketUtil' +import { Commands } from '../../shared/vscode/commands2' +import { isInlineCompletionEnabled } from '../util/commonUtil' +import { onAcceptance } from './onAcceptance' +import * as codewhispererClient from '../client/codewhisperer' +import { + CodewhispererCompletionType, + CodewhispererLanguage, + CodewhispererTriggerType, +} from '../../shared/telemetry/telemetry.gen' +import { ReferenceLogViewProvider } from '../service/referenceLogViewProvider' +import { ReferenceHoverProvider } from '../service/referenceHoverProvider' +import { ImportAdderProvider } from '../service/importAdderProvider' +import { session } from '../util/codeWhispererSession' +import path from 'path' +import { RecommendationService } from '../service/recommendationService' +import { Container } from '../service/serviceContainer' +import { telemetry } from '../../shared/telemetry/telemetry' +import { TelemetryHelper } from '../util/telemetryHelper' +import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' + +export const acceptSuggestion = Commands.declare( + 'aws.amazonq.accept', + (context: vscode.ExtensionContext) => + async ( + range: vscode.Range, + effectiveRange: vscode.Range, + acceptIndex: number, + recommendation: string, + requestId: string, + sessionId: string, + triggerType: CodewhispererTriggerType, + completionType: CodewhispererCompletionType, + language: CodewhispererLanguage, + references: codewhispererClient.References + ) => { + telemetry.record({ + traceId: TelemetryHelper.instance.traceId, + }) + + RecommendationService.instance.incrementAcceptedCount() + const editor = vscode.window.activeTextEditor + await Container.instance.lineAnnotationController.refresh(editor, 'codewhisperer') + const onAcceptanceFunc = isInlineCompletionEnabled() ? onInlineAcceptance : onAcceptance + await onAcceptanceFunc({ + editor, + range, + effectiveRange, + acceptIndex, + recommendation, + requestId, + sessionId, + triggerType, + completionType, + language, + references, + }) + } +) +/** + * This function is called when user accepts a intelliSense suggestion or an inline suggestion + */ +export async function onInlineAcceptance(acceptanceEntry: OnRecommendationAcceptanceEntry) { + RecommendationHandler.instance.cancelPaginatedRequest() + RecommendationHandler.instance.disposeInlineCompletion() + + if (acceptanceEntry.editor) { + await sleep(CodeWhispererConstants.vsCodeCursorUpdateDelay) + const languageContext = runtimeLanguageContext.getLanguageContext( + acceptanceEntry.editor.document.languageId, + path.extname(acceptanceEntry.editor.document.fileName) + ) + const start = acceptanceEntry.range.start + const end = acceptanceEntry.editor.selection.active + + vsCodeState.isCodeWhispererEditing = true + /** + * Mitigation to right context handling mainly for auto closing bracket use case + */ + try { + // Do not handle extra bracket if there is a right context merge + if (acceptanceEntry.recommendation === session.recommendations[acceptanceEntry.acceptIndex].content) { + await handleExtraBrackets(acceptanceEntry.editor, end, acceptanceEntry.effectiveRange.start) + } + await ImportAdderProvider.instance.onAcceptRecommendation( + acceptanceEntry.editor, + session.recommendations[acceptanceEntry.acceptIndex], + start.line + ) + } catch (error) { + getLogger().error(`${error} in handling extra brackets or imports`) + } finally { + vsCodeState.isCodeWhispererEditing = false + } + + CodeWhispererTracker.getTracker().enqueue({ + time: new Date(), + fileUrl: acceptanceEntry.editor.document.uri, + originalString: acceptanceEntry.editor.document.getText(new vscode.Range(start, end)), + startPosition: start, + endPosition: end, + requestId: acceptanceEntry.requestId, + sessionId: acceptanceEntry.sessionId, + index: acceptanceEntry.acceptIndex, + triggerType: acceptanceEntry.triggerType, + completionType: acceptanceEntry.completionType, + language: languageContext.language, + }) + const insertedCoderange = new vscode.Range(start, end) + CodeWhispererCodeCoverageTracker.getTracker(languageContext.language)?.countAcceptedTokens( + insertedCoderange, + acceptanceEntry.editor.document.getText(insertedCoderange), + acceptanceEntry.editor.document.fileName + ) + UserWrittenCodeTracker.instance.onQFinishesEdits() + if (acceptanceEntry.references !== undefined) { + const referenceLog = ReferenceLogViewProvider.getReferenceLog( + acceptanceEntry.recommendation, + acceptanceEntry.references, + acceptanceEntry.editor + ) + ReferenceLogViewProvider.instance.addReferenceLog(referenceLog) + ReferenceHoverProvider.instance.addCodeReferences( + acceptanceEntry.recommendation, + acceptanceEntry.references + ) + } + + RecommendationHandler.instance.reportUserDecisions(acceptanceEntry.acceptIndex) + } +} diff --git a/packages/core/src/codewhisperer/commands/startSecurityScan.ts b/packages/core/src/codewhisperer/commands/startSecurityScan.ts index d04fe6effc3..bd081face38 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() } 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 56e54a97a8a..e5effb1b583 100644 --- a/packages/core/src/codewhisperer/commands/startTransformByQ.ts +++ b/packages/core/src/codewhisperer/commands/startTransformByQ.ts @@ -5,7 +5,6 @@ import * as vscode from 'vscode' import * as fs from 'fs' // eslint-disable-line no-restricted-imports -import os from 'os' import path from 'path' import { getLogger } from '../../shared/logger/logger' import * as CodeWhispererConstants from '../models/constants' @@ -20,6 +19,7 @@ import { TransformationType, TransformationCandidateProject, RegionProfile, + sessionJobHistory, } from '../models/model' import { createZipManifest, @@ -78,6 +78,12 @@ 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() @@ -474,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() @@ -724,21 +747,31 @@ export async function postTransformationJob() { }) } - if (transformByQState.getPayloadFilePath()) { - // delete original upload ZIP at very end of transformation - fs.rmSync(transformByQState.getPayloadFilePath(), { force: true }) - } - // delete temporary build logs file - const logFilePath = path.join(os.tmpdir(), 'build-logs.txt') - if (fs.existsSync(logFilePath)) { - fs.rmSync(logFilePath, { 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() + ) + } } export async function transformationJobErrorHandler(error: any) { @@ -748,22 +781,9 @@ export async function transformationJobErrorHandler(error: any) { transformByQState.setToFailed() transformByQState.setPolledJobStatus('FAILED') // jobFailureErrorNotification should always be defined here - const displayedErrorMessage = - transformByQState.getJobFailureErrorNotification() ?? CodeWhispererConstants.failedToCompleteJobNotification transformByQState.setJobFailureErrorChatMessage( transformByQState.getJobFailureErrorChatMessage() ?? CodeWhispererConstants.failedToCompleteJobChatMessage ) - void vscode.window - .showErrorMessage(displayedErrorMessage, CodeWhispererConstants.amazonQFeedbackText) - .then((choice) => { - if (choice === CodeWhispererConstants.amazonQFeedbackText) { - void submitFeedback( - placeholder, - CodeWhispererConstants.amazonQFeedbackKey, - getFeedbackCommentData() - ) - } - }) } else { transformByQState.setToCancelled() transformByQState.setPolledJobStatus('CANCELLED') diff --git a/packages/core/src/codewhisperer/index.ts b/packages/core/src/codewhisperer/index.ts index d782b2abefe..066e5ca2fcb 100644 --- a/packages/core/src/codewhisperer/index.ts +++ b/packages/core/src/codewhisperer/index.ts @@ -36,6 +36,7 @@ export { codeWhispererClient, } from './client/codewhisperer' export { listCodeWhispererCommands, listCodeWhispererCommandsId } from './ui/statusBarMenu' +export { InlineCompletionService } from './service/inlineCompletionService' export { refreshStatusBar, CodeWhispererStatusBarManager } from './service/statusBar' export { SecurityIssueHoverProvider } from './service/securityIssueHoverProvider' export { SecurityIssueCodeActionProvider } from './service/securityIssueCodeActionProvider' @@ -46,29 +47,44 @@ export { IssueItem, SeverityItem, } from './service/securityIssueTreeViewProvider' +export { onAcceptance } from './commands/onAcceptance' export { CodeWhispererTracker } from './tracker/codewhispererTracker' export { CodeWhispererUserGroupSettings } from './util/userGroupUtil' export { session } from './util/codeWhispererSession' +export { onInlineAcceptance } from './commands/onInlineAcceptance' export { stopTransformByQ } from './commands/startTransformByQ' export { featureDefinitions, FeatureConfigProvider } from '../shared/featureConfig' export { ReferenceInlineProvider } from './service/referenceInlineProvider' export { ReferenceHoverProvider } from './service/referenceHoverProvider' +export { CWInlineCompletionItemProvider } from './service/inlineCompletionItemProvider' +export { ClassifierTrigger } from './service/classifierTrigger' export { ReferenceLogViewProvider } from './service/referenceLogViewProvider' +export { RecommendationService } from './service/recommendationService' export { ImportAdderProvider } from './service/importAdderProvider' export { LicenseUtil } from './util/licenseUtil' export { SecurityIssueProvider } from './service/securityIssueProvider' export { listScanResults, mapToAggregatedList, pollScanJobStatus } from './service/securityScanHandler' export { TelemetryHelper } from './util/telemetryHelper' export { LineSelection, LineTracker } from './tracker/lineTracker' +export { BM25Okapi } from './util/supplementalContext/rankBm25' export { runtimeLanguageContext, RuntimeLanguageContext } from './util/runtimeLanguageContext' export * as startSecurityScan from './commands/startSecurityScan' +export * from './util/supplementalContext/utgUtils' +export * from './util/supplementalContext/crossFileContextUtil' +export * from './util/editorContext' +export { acceptSuggestion } from './commands/onInlineAcceptance' export * from './util/showSsoPrompt' export * from './util/securityScanLanguageContext' export * from './util/importAdderUtil' +export * from './util/globalStateUtil' export * from './util/zipUtil' export * from './util/diagnosticsUtil' export * from './util/commonUtil' +export * from './util/closingBracketUtil' +export * from './util/supplementalContext/codeParsingUtil' +export * from './util/supplementalContext/supplementalContextUtil' export * from './util/codewhispererSettings' +export * as supplementalContextUtil from './util/supplementalContext/supplementalContextUtil' export * from './service/diagnosticsProvider' export * as diagnosticsProvider from './service/diagnosticsProvider' export * from './ui/codeWhispererNodes' @@ -86,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 9f8d4a5c950..81736d478da 100644 --- a/packages/core/src/codewhisperer/models/constants.ts +++ b/packages/core/src/codewhisperer/models/constants.ts @@ -138,10 +138,16 @@ export const runningSecurityScan = 'Reviewing project for code issues...' export const runningFileScan = 'Reviewing current file for code issues...' +export const noSuggestions = 'No suggestions from Amazon Q' + export const noInlineSuggestionsMsg = 'No suggestions from Amazon Q' export const licenseFilter = 'Amazon Q suggestions were filtered due to reference settings' +/** + * the interval of the background thread invocation, which is triggered by the timer + */ +export const defaultCheckPeriodMillis = 1000 * 60 * 5 /** * Key bindings JSON file path */ @@ -184,6 +190,9 @@ export const identityPoolID = 'us-east-1:70717e99-906f-4add-908c-bd9074a2f5b9' */ export const inlineCompletionsDebounceDelay = 200 +// add 200ms more delay on top of inline default 30-50ms +export const inlineSuggestionShowDelay = 200 + export const referenceLog = 'Code Reference Log' export const suggestionDetailReferenceText = (licenses: string) => @@ -515,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' @@ -544,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.' @@ -579,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 = - "I wasn't able to parse the dependency upgrade file. Check that it's configured properly and try again. For an example of the required dependency upgrade file format, see the [documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/code-transformation.html#dependency-upgrade-file)." +export const invalidCustomVersionsFileMessage = (errorMessage: string) => + `The dependency upgrade file provided is malformed: ${errorMessage}. Check that it is configured properly and try again. For an example of the required dependency upgrade file format, see the [documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/code-transformation.html#dependency-upgrade-file).` export const invalidMetadataFileErrorParsing = "It looks like the .sct file you provided isn't valid. Make sure that you've uploaded the .zip file you retrieved from your schema conversion in AWS DMS." @@ -734,7 +743,7 @@ export const cleanTestCompileErrorNotification = `Amazon Q could not run \`mvn c 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.\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." + "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.' @@ -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 72483feec51..bcfa50c6a71 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' @@ -43,6 +42,8 @@ interface VsCodeState { lastUserModificationTime: number isFreeTierLimitReached: boolean + + lastManualTriggerTime: number } export const vsCodeState: VsCodeState = { @@ -53,6 +54,7 @@ export const vsCodeState: VsCodeState = { isRecommendationsActive: false, lastUserModificationTime: 0, isFreeTierLimitReached: false, + lastManualTriggerTime: 0, } export interface CodeWhispererConfig { @@ -372,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, @@ -680,7 +633,7 @@ export class ZipManifest { dependenciesRoot: string = 'dependencies/' version: string = '1.0' hilCapabilities: string[] = ['HIL_1pDependency_VersionUpgrade'] - transformCapabilities: string[] = ['EXPLAINABILITY_V1', 'SELECTIVE_TRANSFORMATION_V2', 'CLIENT_SIDE_BUILD'] + transformCapabilities: string[] = ['EXPLAINABILITY_V1', 'SELECTIVE_TRANSFORMATION_V2', 'CLIENT_SIDE_BUILD', 'IDE'] noInteractiveMode: boolean = true dependencyUpgradeConfigFile?: string = undefined compilationsJsonFile: string = 'compilations.json' @@ -754,6 +707,8 @@ export class TransformByQState { private targetJDKVersion: JDKVersion | undefined = undefined + private jdkVersionToPath: Map = new Map() + private customBuildCommand: string = '' private sourceDB: DB | undefined = undefined @@ -775,6 +730,7 @@ export class TransformByQState { private planFilePath: string = '' private summaryFilePath: string = '' private preBuildLogFilePath: string = '' + private jobHistoryPath: string = '' private resultArchiveFilePath: string = '' private projectCopyFilePath: string = '' @@ -806,6 +762,8 @@ export class TransformByQState { private intervalId: NodeJS.Timeout | undefined = undefined + private refreshInProgress: boolean = false + public isNotStarted() { return this.transformByQState === TransformByQStatus.NotStarted } @@ -830,6 +788,10 @@ export class TransformByQState { return this.transformByQState === TransformByQStatus.PartiallySucceeded } + public isRefreshInProgress() { + return this.refreshInProgress + } + public getHasSeenTransforming() { return this.hasSeenTransforming } @@ -874,6 +836,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 } @@ -918,6 +888,10 @@ export class TransformByQState { return this.summaryFilePath } + public getJobHistoryPath() { + return this.jobHistoryPath + } + public getResultArchiveFilePath() { return this.resultArchiveFilePath } @@ -954,6 +928,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 } @@ -1006,6 +986,10 @@ export class TransformByQState { this.transformByQState = TransformByQStatus.PartiallySucceeded } + public setRefreshInProgress(inProgress: boolean) { + this.refreshInProgress = inProgress + } + public setHasSeenTransforming(hasSeen: boolean) { this.hasSeenTransforming = hasSeen } @@ -1086,6 +1070,10 @@ export class TransformByQState { this.summaryFilePath = filePath } + public setJobHistoryPath(filePath: string) { + this.jobHistoryPath = filePath + } + public setResultArchiveFilePath(filePath: string) { this.resultArchiveFilePath = filePath } @@ -1152,6 +1140,7 @@ export class TransformByQState { public setJobDefaults() { this.setToNotStarted() + this.refreshInProgress = false this.hasSeenTransforming = false this.jobFailureErrorNotification = undefined this.jobFailureErrorChatMessage = undefined @@ -1168,6 +1157,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 ce33bfd925d..24d58d7f588 100644 --- a/packages/core/src/codewhisperer/region/regionProfileManager.ts +++ b/packages/core/src/codewhisperer/region/regionProfileManager.ts @@ -69,7 +69,7 @@ export class RegionProfileManager { constructor(private readonly profileProvider: () => Promise) { super( 'aws.amazonq.regionProfiles.cache', - 60000, + 3600000, { resource: { locked: false, @@ -77,7 +77,7 @@ export class RegionProfileManager { result: undefined, }, }, - { timeout: 15000, interval: 1500, truthy: true } + { timeout: 15000, interval: 500, truthy: true } ) } @@ -287,38 +287,6 @@ export class RegionProfileManager { } await this.switchRegionProfile(previousSelected, 'reload') - - // cross-validation - // jitter of 0 ~ 5 second - const jitterInSec = Math.floor(Math.random() * 6) - const jitterInMs = jitterInSec * 1000 - setTimeout(async () => { - 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', - }) - }) - }, jitterInMs) } private loadPersistedRegionProfle(): { [label: string]: RegionProfile } { diff --git a/packages/core/src/codewhisperer/service/classifierTrigger.ts b/packages/core/src/codewhisperer/service/classifierTrigger.ts new file mode 100644 index 00000000000..842d5312e68 --- /dev/null +++ b/packages/core/src/codewhisperer/service/classifierTrigger.ts @@ -0,0 +1,609 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import os from 'os' +import * as vscode from 'vscode' +import { CodewhispererAutomatedTriggerType } from '../../shared/telemetry/telemetry' +import { extractContextForCodeWhisperer } from '../util/editorContext' +import { TelemetryHelper } from '../util/telemetryHelper' +import { ProgrammingLanguage } from '../client/codewhispereruserclient' + +interface normalizedCoefficients { + readonly lineNum: number + readonly lenLeftCur: number + readonly lenLeftPrev: number + readonly lenRight: number +} +/* + uses ML classifier to determine if user input should trigger CWSPR service + */ +export class ClassifierTrigger { + static #instance: ClassifierTrigger + + public static get instance() { + return (this.#instance ??= new this()) + } + + // ML classifier trigger threshold + private triggerThreshold = 0.43 + + // ML classifier coefficients + // os coefficient + private osCoefficientMap: Readonly> = { + 'Mac OS X': -0.1552, + 'Windows 10': -0.0238, + Windows: 0.0412, + win32: -0.0559, + } + + // trigger type coefficient + private triggerTypeCoefficientMap: Readonly> = { + SpecialCharacters: 0.0209, + Enter: 0.2853, + } + + private languageCoefficientMap: Readonly> = { + java: -0.4622, + javascript: -0.4688, + python: -0.3052, + typescript: -0.6084, + tsx: -0.6084, + jsx: -0.4688, + shell: -0.4718, + ruby: -0.7356, + sql: -0.4937, + rust: -0.4309, + kotlin: -0.4739, + php: -0.3917, + csharp: -0.3475, + go: -0.3504, + scala: -0.534, + cpp: -0.1734, + json: 0, + yaml: -0.3, + tf: -0.55, + } + + // other metadata coefficient + private lineNumCoefficient = -0.0416 + private lengthOfLeftCurrentCoefficient = -1.1747 + private lengthOfLeftPrevCoefficient = 0.4033 + private lengthOfRightCoefficient = -0.3321 + private prevDecisionAcceptCoefficient = 0.5397 + private prevDecisionRejectCoefficient = -0.1656 + private prevDecisionOtherCoefficient = 0 + private ideVscode = -0.1905 + private lengthLeft0To5 = -0.8756 + private lengthLeft5To10 = -0.5463 + private lengthLeft10To20 = -0.4081 + private lengthLeft20To30 = -0.3272 + private lengthLeft30To40 = -0.2442 + private lengthLeft40To50 = -0.1471 + + // intercept of logistic regression classifier + private intercept = 0.3738713 + + private maxx: normalizedCoefficients = { + lineNum: 4631.0, + lenLeftCur: 157.0, + lenLeftPrev: 176.0, + lenRight: 10239.0, + } + + private minn: normalizedCoefficients = { + lineNum: 0.0, + lenLeftCur: 0.0, + lenLeftPrev: 0.0, + lenRight: 0.0, + } + + // character and keywords coefficient + private charCoefficient: Readonly> = { + throw: 1.5868, + ';': -1.268, + any: -1.1565, + '7': -1.1347, + false: -1.1307, + nil: -1.0653, + elif: 1.0122, + '9': -1.0098, + pass: -1.0058, + True: -1.0002, + False: -0.9434, + '6': -0.9222, + true: -0.9142, + None: -0.9027, + '8': -0.9013, + break: -0.8475, + '}': -0.847, + '5': -0.8414, + '4': -0.8197, + '1': -0.8085, + '\\': -0.8019, + static: -0.7748, + '0': -0.77, + end: -0.7617, + '(': 0.7239, + '/': -0.7104, + where: -0.6981, + readonly: -0.6741, + async: -0.6723, + '3': -0.654, + continue: -0.6413, + struct: -0.64, + try: -0.6369, + float: -0.6341, + using: 0.6079, + '@': 0.6016, + '|': 0.5993, + impl: 0.5808, + private: -0.5746, + for: 0.5741, + '2': -0.5634, + let: -0.5187, + foreach: 0.5186, + select: -0.5148, + export: -0.5, + mut: -0.4921, + ')': -0.463, + ']': -0.4611, + when: 0.4602, + virtual: -0.4583, + extern: -0.4465, + catch: 0.4446, + new: 0.4394, + val: -0.4339, + map: 0.4284, + case: 0.4271, + throws: 0.4221, + null: -0.4197, + protected: -0.4133, + q: 0.4125, + except: 0.4115, + ': ': 0.4072, + '^': -0.407, + ' ': 0.4066, + $: 0.3981, + this: 0.3962, + switch: 0.3947, + '*': -0.3931, + module: 0.3912, + array: 0.385, + '=': 0.3828, + p: 0.3728, + ON: 0.3708, + '`': 0.3693, + u: 0.3658, + a: 0.3654, + require: 0.3646, + '>': -0.3644, + const: -0.3476, + o: 0.3423, + sizeof: 0.3416, + object: 0.3362, + w: 0.3345, + print: 0.3344, + range: 0.3336, + if: 0.3324, + abstract: -0.3293, + var: -0.3239, + i: 0.321, + while: 0.3138, + J: 0.3137, + c: 0.3118, + await: -0.3072, + from: 0.3057, + f: 0.302, + echo: 0.2995, + '#': 0.2984, + e: 0.2962, + r: 0.2925, + mod: 0.2893, + loop: 0.2874, + t: 0.2832, + '~': 0.282, + final: -0.2816, + del: 0.2785, + override: -0.2746, + ref: -0.2737, + h: 0.2693, + m: 0.2681, + '{': 0.2674, + implements: 0.2672, + inline: -0.2642, + match: 0.2613, + with: -0.261, + x: 0.2597, + namespace: -0.2596, + operator: 0.2573, + double: -0.2563, + source: -0.2482, + import: -0.2419, + NULL: -0.2399, + l: 0.239, + or: 0.2378, + s: 0.2366, + then: 0.2354, + W: 0.2354, + y: 0.2333, + local: 0.2288, + is: 0.2282, + n: 0.2254, + '+': -0.2251, + G: 0.223, + public: -0.2229, + WHERE: 0.2224, + list: 0.2204, + Q: 0.2204, + '[': 0.2136, + VALUES: 0.2134, + H: 0.2105, + g: 0.2094, + else: -0.208, + bool: -0.2066, + long: -0.2059, + R: 0.2025, + S: 0.2021, + d: 0.2003, + V: 0.1974, + K: -0.1961, + '<': 0.1958, + debugger: -0.1929, + NOT: -0.1911, + b: 0.1907, + boolean: -0.1891, + z: -0.1866, + LIKE: -0.1793, + raise: 0.1782, + L: 0.1768, + fn: 0.176, + delete: 0.1714, + unsigned: -0.1675, + auto: -0.1648, + finally: 0.1616, + k: 0.1599, + as: 0.156, + instanceof: 0.1558, + '&': 0.1554, + E: 0.1551, + M: 0.1542, + I: 0.1503, + Y: 0.1493, + typeof: 0.1475, + j: 0.1445, + INTO: 0.1442, + IF: 0.1437, + next: 0.1433, + undef: -0.1427, + THEN: -0.1416, + v: 0.1415, + C: 0.1383, + P: 0.1353, + AND: -0.1345, + constructor: 0.1337, + void: -0.1336, + class: -0.1328, + defer: 0.1316, + begin: 0.1306, + FROM: -0.1304, + SET: 0.1291, + decimal: -0.1278, + friend: 0.1277, + SELECT: -0.1265, + event: 0.1259, + lambda: 0.1253, + enum: 0.1215, + A: 0.121, + lock: 0.1187, + ensure: 0.1184, + '%': 0.1177, + isset: 0.1175, + O: 0.1174, + '.': 0.1146, + UNION: -0.1145, + alias: -0.1129, + template: -0.1102, + WHEN: 0.1093, + rescue: 0.1083, + DISTINCT: -0.1074, + trait: -0.1073, + D: 0.1062, + in: 0.1045, + internal: -0.1029, + ',': 0.1027, + static_cast: 0.1016, + do: -0.1005, + OR: 0.1003, + AS: -0.1001, + interface: 0.0996, + super: 0.0989, + B: 0.0963, + U: 0.0962, + T: 0.0943, + CALL: -0.0918, + BETWEEN: -0.0915, + N: 0.0897, + yield: 0.0867, + done: -0.0857, + string: -0.0837, + out: -0.0831, + volatile: -0.0819, + retry: 0.0816, + '?': -0.0796, + number: -0.0791, + short: 0.0787, + sealed: -0.0776, + package: 0.0765, + OPEN: -0.0756, + base: 0.0735, + and: 0.0729, + exit: 0.0726, + _: 0.0721, + keyof: -0.072, + def: 0.0713, + crate: -0.0706, + '-': -0.07, + FUNCTION: 0.0692, + declare: -0.0678, + include: 0.0671, + COUNT: -0.0669, + INDEX: -0.0666, + CLOSE: -0.0651, + fi: -0.0644, + uint: 0.0624, + params: 0.0575, + HAVING: 0.0575, + byte: -0.0575, + clone: -0.0552, + char: -0.054, + func: 0.0538, + never: -0.053, + unset: -0.0524, + unless: -0.051, + esac: -0.0509, + shift: -0.0507, + require_once: 0.0486, + ELSE: -0.0477, + extends: 0.0461, + elseif: 0.0452, + mutable: -0.0451, + asm: 0.0449, + '!': 0.0446, + LIMIT: 0.0444, + ushort: -0.0438, + '"': -0.0433, + Z: 0.0431, + exec: -0.0431, + IS: -0.0429, + DECLARE: -0.0425, + __LINE__: -0.0424, + BEGIN: -0.0418, + typedef: 0.0414, + EXIT: -0.0412, + "'": 0.041, + function: -0.0393, + dyn: -0.039, + wchar_t: -0.0388, + unique: -0.0383, + include_once: 0.0367, + stackalloc: 0.0359, + RETURN: -0.0356, + const_cast: 0.035, + MAX: 0.0341, + assert: -0.0331, + JOIN: -0.0328, + use: 0.0318, + GET: 0.0317, + VIEW: 0.0314, + move: 0.0308, + typename: 0.0308, + die: 0.0305, + asserts: -0.0304, + reinterpret_cast: -0.0302, + USING: -0.0289, + elsif: -0.0285, + FIRST: -0.028, + self: -0.0278, + RETURNING: -0.0278, + symbol: -0.0273, + OFFSET: 0.0263, + bigint: 0.0253, + register: -0.0237, + union: -0.0227, + return: -0.0227, + until: -0.0224, + endfor: -0.0213, + implicit: -0.021, + LOOP: 0.0195, + pub: 0.0182, + global: 0.0179, + EXCEPTION: 0.0175, + delegate: 0.0173, + signed: -0.0163, + FOR: 0.0156, + unsafe: 0.014, + NEXT: -0.0133, + IN: 0.0129, + MIN: -0.0123, + go: -0.0112, + type: -0.0109, + explicit: -0.0107, + eval: -0.0104, + int: -0.0099, + CASE: -0.0096, + END: 0.0084, + UPDATE: 0.0074, + default: 0.0072, + chan: 0.0068, + fixed: 0.0066, + not: -0.0052, + X: -0.0047, + endforeach: 0.0031, + goto: 0.0028, + empty: 0.0022, + checked: 0.0012, + F: -0.001, + } + + public getThreshold() { + return this.triggerThreshold + } + + public recordClassifierResultForManualTrigger(editor: vscode.TextEditor) { + this.shouldTriggerFromClassifier(undefined, editor, undefined, true) + } + + public recordClassifierResultForAutoTrigger( + editor: vscode.TextEditor, + triggerType?: CodewhispererAutomatedTriggerType, + event?: vscode.TextDocumentChangeEvent + ) { + if (!triggerType) { + return + } + this.shouldTriggerFromClassifier(event, editor, triggerType, true) + } + + public shouldTriggerFromClassifier( + event: vscode.TextDocumentChangeEvent | undefined, + editor: vscode.TextEditor, + autoTriggerType: string | undefined, + shouldRecordResult: boolean = false + ): boolean { + const fileContext = extractContextForCodeWhisperer(editor) + const osPlatform = this.normalizeOsName(os.platform(), os.version()) + const char = event ? event.contentChanges[0].text : '' + const lineNum = editor.selection.active.line + const classifierResult = this.getClassifierResult( + fileContext.leftFileContent, + fileContext.rightFileContent, + osPlatform, + autoTriggerType, + char, + lineNum, + fileContext.programmingLanguage + ) + + const threshold = this.getThreshold() + + const shouldTrigger = classifierResult > threshold + if (shouldRecordResult) { + TelemetryHelper.instance.setClassifierResult(classifierResult) + TelemetryHelper.instance.setClassifierThreshold(threshold) + } + return shouldTrigger + } + + private getClassifierResult( + leftContext: string, + rightContext: string, + os: string, + triggerType: string | undefined, + char: string, + lineNum: number, + language: ProgrammingLanguage + ): number { + const leftContextLines = leftContext.split(/\r?\n/) + const leftContextAtCurrentLine = leftContextLines[leftContextLines.length - 1] + const tokens = leftContextAtCurrentLine.trim().split(' ') + let keyword = '' + const lastToken = tokens[tokens.length - 1] + if (lastToken && lastToken.length > 1) { + keyword = lastToken + } + const lengthOfLeftCurrent = leftContextLines[leftContextLines.length - 1].length + const lengthOfLeftPrev = leftContextLines[leftContextLines.length - 2]?.length ?? 0 + const lengthOfRight = rightContext.trim().length + + const triggerTypeCoefficient: number = this.triggerTypeCoefficientMap[triggerType || ''] ?? 0 + const osCoefficient: number = this.osCoefficientMap[os] ?? 0 + const charCoefficient: number = this.charCoefficient[char] ?? 0 + const keyWordCoefficient: number = this.charCoefficient[keyword] ?? 0 + const ideCoefficient = this.ideVscode + + const previousDecision = TelemetryHelper.instance.getLastTriggerDecisionForClassifier() + const languageCoefficients = Object.values(this.languageCoefficientMap) + const avrgCoefficient = + languageCoefficients.length > 0 + ? languageCoefficients.reduce((a, b) => a + b) / languageCoefficients.length + : 0 + const languageCoefficient = this.languageCoefficientMap[language.languageName] ?? avrgCoefficient + + let previousDecisionCoefficient = 0 + if (previousDecision === 'Accept') { + previousDecisionCoefficient = this.prevDecisionAcceptCoefficient + } else if (previousDecision === 'Reject') { + previousDecisionCoefficient = this.prevDecisionRejectCoefficient + } else if (previousDecision === 'Discard' || previousDecision === 'Empty') { + previousDecisionCoefficient = this.prevDecisionOtherCoefficient + } + + let leftContextLengthCoefficient = 0 + if (leftContext.length >= 0 && leftContext.length < 5) { + leftContextLengthCoefficient = this.lengthLeft0To5 + } else if (leftContext.length >= 5 && leftContext.length < 10) { + leftContextLengthCoefficient = this.lengthLeft5To10 + } else if (leftContext.length >= 10 && leftContext.length < 20) { + leftContextLengthCoefficient = this.lengthLeft10To20 + } else if (leftContext.length >= 20 && leftContext.length < 30) { + leftContextLengthCoefficient = this.lengthLeft20To30 + } else if (leftContext.length >= 30 && leftContext.length < 40) { + leftContextLengthCoefficient = this.lengthLeft30To40 + } else if (leftContext.length >= 40 && leftContext.length < 50) { + leftContextLengthCoefficient = this.lengthLeft40To50 + } + + const result = + (this.lengthOfRightCoefficient * (lengthOfRight - this.minn.lenRight)) / + (this.maxx.lenRight - this.minn.lenRight) + + (this.lengthOfLeftCurrentCoefficient * (lengthOfLeftCurrent - this.minn.lenLeftCur)) / + (this.maxx.lenLeftCur - this.minn.lenLeftCur) + + (this.lengthOfLeftPrevCoefficient * (lengthOfLeftPrev - this.minn.lenLeftPrev)) / + (this.maxx.lenLeftPrev - this.minn.lenLeftPrev) + + (this.lineNumCoefficient * (lineNum - this.minn.lineNum)) / (this.maxx.lineNum - this.minn.lineNum) + + osCoefficient + + triggerTypeCoefficient + + charCoefficient + + keyWordCoefficient + + ideCoefficient + + this.intercept + + previousDecisionCoefficient + + languageCoefficient + + leftContextLengthCoefficient + + return sigmoid(result) + } + + private normalizeOsName(name: string, version: string | undefined): string { + const lowercaseName = name.toLowerCase() + if (lowercaseName.includes('windows')) { + if (!version) { + return 'Windows' + } else if (version.includes('Windows NT 10') || version.startsWith('10')) { + return 'Windows 10' + } else if (version.includes('6.1')) { + return 'Windows 7' + } else if (version.includes('6.3')) { + return 'Windows 8.1' + } else { + return 'Windows' + } + } else if ( + lowercaseName.includes('macos') || + lowercaseName.includes('mac os') || + lowercaseName.includes('darwin') + ) { + return 'Mac OS X' + } else if (lowercaseName.includes('linux')) { + return 'Linux' + } else { + return name + } + } +} + +const sigmoid = (x: number) => { + return 1 / (1 + Math.exp(-x)) +} diff --git a/packages/core/src/codewhisperer/service/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/inlineCompletionItemProvider.ts b/packages/core/src/codewhisperer/service/inlineCompletionItemProvider.ts new file mode 100644 index 00000000000..a6c424c321d --- /dev/null +++ b/packages/core/src/codewhisperer/service/inlineCompletionItemProvider.ts @@ -0,0 +1,194 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import vscode, { Position } from 'vscode' +import { getPrefixSuffixOverlap } from '../util/commonUtil' +import { Recommendation } from '../client/codewhisperer' +import { session } from '../util/codeWhispererSession' +import { TelemetryHelper } from '../util/telemetryHelper' +import { runtimeLanguageContext } from '../util/runtimeLanguageContext' +import { ReferenceInlineProvider } from './referenceInlineProvider' +import { ImportAdderProvider } from './importAdderProvider' +import { application } from '../util/codeWhispererApplication' +import path from 'path' +import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' + +export class CWInlineCompletionItemProvider implements vscode.InlineCompletionItemProvider { + private activeItemIndex: number | undefined + private nextMove: number + private recommendations: Recommendation[] + private requestId: string + private startPos: Position + private nextToken: string + + private _onDidShow: vscode.EventEmitter = new vscode.EventEmitter() + public readonly onDidShow: vscode.Event = this._onDidShow.event + + public constructor( + itemIndex: number | undefined, + firstMove: number, + recommendations: Recommendation[], + requestId: string, + startPos: Position, + nextToken: string + ) { + this.activeItemIndex = itemIndex + this.nextMove = firstMove + this.recommendations = recommendations + this.requestId = requestId + this.startPos = startPos + this.nextToken = nextToken + } + + get getActiveItemIndex() { + return this.activeItemIndex + } + + public clearActiveItemIndex() { + this.activeItemIndex = undefined + } + + // iterate suggestions and stop at index 0 or index len - 1 + private getIteratingIndexes() { + const len = this.recommendations.length + const startIndex = this.activeItemIndex ? this.activeItemIndex : 0 + const index = [] + if (this.nextMove === 0) { + for (let i = 0; i < len; i++) { + index.push((startIndex + i) % len) + } + } else if (this.nextMove === -1) { + for (let i = startIndex - 1; i >= 0; i--) { + index.push(i) + } + index.push(startIndex) + } else { + for (let i = startIndex + 1; i < len; i++) { + index.push(i) + } + index.push(startIndex) + } + return index + } + + truncateOverlapWithRightContext(document: vscode.TextDocument, suggestion: string, pos: vscode.Position): string { + const trimmedSuggestion = suggestion.trim() + // limit of 5000 for right context matching + const rightContext = document.getText(new vscode.Range(pos, document.positionAt(document.offsetAt(pos) + 5000))) + const overlap = getPrefixSuffixOverlap(trimmedSuggestion, rightContext) + const overlapIndex = suggestion.lastIndexOf(overlap) + if (overlapIndex >= 0) { + const truncated = suggestion.slice(0, overlapIndex) + return truncated.trim().length ? truncated : '' + } else { + return suggestion + } + } + + getInlineCompletionItem( + document: vscode.TextDocument, + r: Recommendation, + start: vscode.Position, + end: vscode.Position, + index: number, + prefix: string + ): vscode.InlineCompletionItem | undefined { + if (!r.content.startsWith(prefix)) { + return undefined + } + const effectiveStart = document.positionAt(document.offsetAt(start) + prefix.length) + const truncatedSuggestion = this.truncateOverlapWithRightContext(document, r.content, end) + if (truncatedSuggestion.length === 0) { + if (session.getSuggestionState(index) !== 'Showed') { + session.setSuggestionState(index, 'Discard') + } + return undefined + } + TelemetryHelper.instance.lastSuggestionInDisplay = truncatedSuggestion + return { + insertText: truncatedSuggestion, + range: new vscode.Range(start, end), + command: { + command: 'aws.amazonq.accept', + title: 'On acceptance', + arguments: [ + new vscode.Range(start, end), + new vscode.Range(effectiveStart, end), + index, + truncatedSuggestion, + this.requestId, + session.sessionId, + session.triggerType, + session.getCompletionType(index), + runtimeLanguageContext.getLanguageContext(document.languageId, path.extname(document.fileName)) + .language, + r.references, + ], + }, + } + } + + // the returned completion items will always only contain one valid item + // this is to trace the current index of visible completion item + // so that reference tracker can show + // This hack can be removed once inlineCompletionAdditions API becomes public + provideInlineCompletionItems( + document: vscode.TextDocument, + position: vscode.Position, + _context: vscode.InlineCompletionContext, + _token: vscode.CancellationToken + ): vscode.ProviderResult { + if (position.line < 0 || position.isBefore(this.startPos)) { + application()._clearCodeWhispererUIListener.fire() + this.activeItemIndex = undefined + return + } + + // There's a chance that the startPos is no longer valid in the current document (e.g. + // when CodeWhisperer got triggered by 'Enter', the original startPos is with indentation + // but then this indentation got removed by VSCode when another new line is inserted, + // before the code reaches here). In such case, we need to update the startPos to be a + // valid one. Otherwise, inline completion which utilizes this position will function + // improperly. + const start = document.validatePosition(this.startPos) + const end = position + const iteratingIndexes = this.getIteratingIndexes() + const prefix = document.getText(new vscode.Range(start, end)).replace(/\r\n/g, '\n') + const matchedCount = session.recommendations.filter( + (r) => r.content.length > 0 && r.content.startsWith(prefix) && r.content !== prefix + ).length + for (const i of iteratingIndexes) { + const r = session.recommendations[i] + const item = this.getInlineCompletionItem(document, r, start, end, i, prefix) + if (item === undefined) { + continue + } + this.activeItemIndex = i + session.setSuggestionState(i, 'Showed') + ReferenceInlineProvider.instance.setInlineReference(this.startPos.line, r.content, r.references) + ImportAdderProvider.instance.onShowRecommendation(document, this.startPos.line, r) + this.nextMove = 0 + TelemetryHelper.instance.setFirstSuggestionShowTime() + session.setPerceivedLatency() + UserWrittenCodeTracker.instance.onQStartsMakingEdits() + this._onDidShow.fire() + if (matchedCount >= 2 || this.nextToken !== '') { + const result = [item] + for (let j = 0; j < matchedCount - 1; j++) { + result.push({ + insertText: `${ + typeof item.insertText === 'string' ? item.insertText : item.insertText.value + }${j}`, + range: item.range, + }) + } + return result + } + return [item] + } + application()._clearCodeWhispererUIListener.fire() + this.activeItemIndex = undefined + return [] + } +} diff --git a/packages/core/src/codewhisperer/service/inlineCompletionService.ts b/packages/core/src/codewhisperer/service/inlineCompletionService.ts new file mode 100644 index 00000000000..cd37663af49 --- /dev/null +++ b/packages/core/src/codewhisperer/service/inlineCompletionService.ts @@ -0,0 +1,163 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as vscode from 'vscode' +import { CodeSuggestionsState, ConfigurationEntry, GetRecommendationsResponse, vsCodeState } from '../models/model' +import * as CodeWhispererConstants from '../models/constants' +import { DefaultCodeWhispererClient } from '../client/codewhisperer' +import { RecommendationHandler } from './recommendationHandler' +import { CodewhispererAutomatedTriggerType, CodewhispererTriggerType } from '../../shared/telemetry/telemetry' +import { showTimedMessage } from '../../shared/utilities/messages' +import { getLogger } from '../../shared/logger/logger' +import { TelemetryHelper } from '../util/telemetryHelper' +import { AuthUtil } from '../util/authUtil' +import { shared } from '../../shared/utilities/functionUtils' +import { ClassifierTrigger } from './classifierTrigger' +import { session } from '../util/codeWhispererSession' +import { noSuggestions } from '../models/constants' +import { CodeWhispererStatusBarManager } from './statusBar' + +export class InlineCompletionService { + private maxPage = 100 + private statusBar: CodeWhispererStatusBarManager + private _showRecommendationTimer?: NodeJS.Timer + + constructor(statusBar: CodeWhispererStatusBarManager = CodeWhispererStatusBarManager.instance) { + this.statusBar = statusBar + + RecommendationHandler.instance.onDidReceiveRecommendation((e) => { + this.startShowRecommendationTimer() + }) + + CodeSuggestionsState.instance.onDidChangeState(() => { + return this.statusBar.refreshStatusBar() + }) + } + + static #instance: InlineCompletionService + + public static get instance() { + return (this.#instance ??= new this()) + } + + filePath(): string | undefined { + return RecommendationHandler.instance.documentUri?.fsPath + } + + private sharedTryShowRecommendation = shared( + RecommendationHandler.instance.tryShowRecommendation.bind(RecommendationHandler.instance) + ) + + private startShowRecommendationTimer() { + if (this._showRecommendationTimer) { + clearInterval(this._showRecommendationTimer) + this._showRecommendationTimer = undefined + } + this._showRecommendationTimer = setInterval(() => { + const delay = Date.now() - vsCodeState.lastUserModificationTime + if (delay < CodeWhispererConstants.inlineSuggestionShowDelay) { + return + } + this.sharedTryShowRecommendation() + .catch((e) => { + getLogger().error('tryShowRecommendation failed: %s', (e as Error).message) + }) + .finally(() => { + if (this._showRecommendationTimer) { + clearInterval(this._showRecommendationTimer) + this._showRecommendationTimer = undefined + } + }) + }, CodeWhispererConstants.showRecommendationTimerPollPeriod) + } + + async getPaginatedRecommendation( + client: DefaultCodeWhispererClient, + editor: vscode.TextEditor, + triggerType: CodewhispererTriggerType, + config: ConfigurationEntry, + autoTriggerType?: CodewhispererAutomatedTriggerType, + event?: vscode.TextDocumentChangeEvent + ): Promise { + if (vsCodeState.isCodeWhispererEditing || RecommendationHandler.instance.isSuggestionVisible()) { + return { + result: 'Failed', + errorMessage: 'Amazon Q is already running', + recommendationCount: 0, + } + } + + // Call report user decisions once to report recommendations leftover from last invocation. + RecommendationHandler.instance.reportUserDecisions(-1) + TelemetryHelper.instance.setInvokeSuggestionStartTime() + ClassifierTrigger.instance.recordClassifierResultForAutoTrigger(editor, autoTriggerType, event) + + const triggerChar = event?.contentChanges[0]?.text + if (autoTriggerType === 'SpecialCharacters' && triggerChar) { + TelemetryHelper.instance.setTriggerCharForUserTriggerDecision(triggerChar) + } + const isAutoTrigger = triggerType === 'AutoTrigger' + if (AuthUtil.instance.isConnectionExpired()) { + await AuthUtil.instance.notifyReauthenticate(isAutoTrigger) + return { + result: 'Failed', + errorMessage: 'auth', + recommendationCount: 0, + } + } + + await this.statusBar.setLoading() + + RecommendationHandler.instance.checkAndResetCancellationTokens() + RecommendationHandler.instance.documentUri = editor.document.uri + let response: GetRecommendationsResponse = { + result: 'Failed', + errorMessage: undefined, + recommendationCount: 0, + } + try { + let page = 0 + while (page < this.maxPage) { + response = await RecommendationHandler.instance.getRecommendations( + client, + editor, + triggerType, + config, + autoTriggerType, + true, + page + ) + if (RecommendationHandler.instance.checkAndResetCancellationTokens()) { + RecommendationHandler.instance.reportUserDecisions(-1) + await vscode.commands.executeCommand('aws.amazonq.refreshStatusBar') + if (triggerType === 'OnDemand' && session.recommendations.length === 0) { + void showTimedMessage(response.errorMessage ? response.errorMessage : noSuggestions, 2000) + } + return { + result: 'Failed', + errorMessage: 'cancelled', + recommendationCount: 0, + } + } + if (!RecommendationHandler.instance.hasNextToken()) { + break + } + page++ + } + } catch (error) { + getLogger().error(`Error ${error} in getPaginatedRecommendation`) + } + await vscode.commands.executeCommand('aws.amazonq.refreshStatusBar') + if (triggerType === 'OnDemand' && session.recommendations.length === 0) { + void showTimedMessage(response.errorMessage ? response.errorMessage : noSuggestions, 2000) + } + TelemetryHelper.instance.tryRecordClientComponentLatency() + + return { + result: 'Succeeded', + errorMessage: undefined, + recommendationCount: session.recommendations.length, + } + } +} diff --git a/packages/core/src/codewhisperer/service/keyStrokeHandler.ts b/packages/core/src/codewhisperer/service/keyStrokeHandler.ts new file mode 100644 index 00000000000..312e31c248a --- /dev/null +++ b/packages/core/src/codewhisperer/service/keyStrokeHandler.ts @@ -0,0 +1,267 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { DefaultCodeWhispererClient } from '../client/codewhisperer' +import * as CodeWhispererConstants from '../models/constants' +import { ConfigurationEntry } from '../models/model' +import { getLogger } from '../../shared/logger/logger' +import { RecommendationHandler } from './recommendationHandler' +import { CodewhispererAutomatedTriggerType } from '../../shared/telemetry/telemetry' +import { getTabSizeSetting } from '../../shared/utilities/editorUtilities' +import { isInlineCompletionEnabled } from '../util/commonUtil' +import { ClassifierTrigger } from './classifierTrigger' +import { extractContextForCodeWhisperer } from '../util/editorContext' +import { RecommendationService } from './recommendationService' + +/** + * This class is for CodeWhisperer auto trigger + */ +export class KeyStrokeHandler { + /** + * Special character which automated triggers codewhisperer + */ + public specialChar: string + /** + * Key stroke count for automated trigger + */ + + private idleTriggerTimer?: NodeJS.Timer + + public lastInvocationTime?: number + + constructor() { + this.specialChar = '' + } + + static #instance: KeyStrokeHandler + + public static get instance() { + return (this.#instance ??= new this()) + } + + public startIdleTimeTriggerTimer( + event: vscode.TextDocumentChangeEvent, + editor: vscode.TextEditor, + client: DefaultCodeWhispererClient, + config: ConfigurationEntry + ) { + if (this.idleTriggerTimer) { + clearInterval(this.idleTriggerTimer) + this.idleTriggerTimer = undefined + } + if (!this.shouldTriggerIdleTime()) { + return + } + this.idleTriggerTimer = setInterval(() => { + const duration = (Date.now() - RecommendationHandler.instance.lastInvocationTime) / 1000 + if (duration < CodeWhispererConstants.invocationTimeIntervalThreshold) { + return + } + + this.invokeAutomatedTrigger('IdleTime', editor, client, config, event) + .catch((e) => { + getLogger().error('invokeAutomatedTrigger failed: %s', (e as Error).message) + }) + .finally(() => { + if (this.idleTriggerTimer) { + clearInterval(this.idleTriggerTimer) + this.idleTriggerTimer = undefined + } + }) + }, CodeWhispererConstants.idleTimerPollPeriod) + } + + public shouldTriggerIdleTime(): boolean { + if (isInlineCompletionEnabled() && RecommendationService.instance.isRunning) { + return false + } + return true + } + + async processKeyStroke( + event: vscode.TextDocumentChangeEvent, + editor: vscode.TextEditor, + client: DefaultCodeWhispererClient, + config: ConfigurationEntry + ): Promise { + try { + if (!config.isAutomatedTriggerEnabled) { + return + } + + // Skip when output channel gains focus and invoke + if (editor.document.languageId === 'Log') { + return + } + + const { rightFileContent } = extractContextForCodeWhisperer(editor) + const rightContextLines = rightFileContent.split(/\r?\n/) + const rightContextAtCurrentLine = rightContextLines[0] + // we do not want to trigger when there is immediate right context on the same line + // with "}" being an exception because of IDE auto-complete + if ( + rightContextAtCurrentLine.length && + !rightContextAtCurrentLine.startsWith(' ') && + rightContextAtCurrentLine.trim() !== '}' && + rightContextAtCurrentLine.trim() !== ')' + ) { + return + } + + let triggerType: CodewhispererAutomatedTriggerType | undefined + const changedSource = new DefaultDocumentChangedType(event.contentChanges).checkChangeSource() + + switch (changedSource) { + case DocumentChangedSource.EnterKey: { + triggerType = 'Enter' + break + } + case DocumentChangedSource.SpecialCharsKey: { + triggerType = 'SpecialCharacters' + break + } + case DocumentChangedSource.RegularKey: { + triggerType = ClassifierTrigger.instance.shouldTriggerFromClassifier(event, editor, triggerType) + ? 'Classifier' + : undefined + break + } + default: { + break + } + } + + if (triggerType) { + await this.invokeAutomatedTrigger(triggerType, editor, client, config, event) + } + } catch (error) { + getLogger().verbose(`Automated Trigger Exception : ${error}`) + } + } + + async invokeAutomatedTrigger( + autoTriggerType: CodewhispererAutomatedTriggerType, + editor: vscode.TextEditor, + client: DefaultCodeWhispererClient, + config: ConfigurationEntry, + event: vscode.TextDocumentChangeEvent + ): Promise { + if (!editor) { + return + } + + // RecommendationHandler.instance.reportUserDecisionOfRecommendation(editor, -1) + await RecommendationService.instance.generateRecommendation( + client, + editor, + 'AutoTrigger', + config, + autoTriggerType + ) + } +} + +export abstract class DocumentChangedType { + constructor(protected readonly contentChanges: ReadonlyArray) { + this.contentChanges = contentChanges + } + + abstract checkChangeSource(): DocumentChangedSource + + // Enter key should always start with ONE '\n' or '\r\n' and potentially following spaces due to IDE reformat + protected isEnterKey(str: string): boolean { + if (str.length === 0) { + return false + } + return ( + (str.startsWith('\r\n') && str.substring(2).trim() === '') || + (str[0] === '\n' && str.substring(1).trim() === '') + ) + } + + // Tab should consist of space char only ' ' and the length % tabSize should be 0 + protected isTabKey(str: string): boolean { + const tabSize = getTabSizeSetting() + if (str.length % tabSize === 0 && str.trim() === '') { + return true + } + return false + } + + protected isUserTypingSpecialChar(str: string): boolean { + return ['(', '()', '[', '[]', '{', '{}', ':'].includes(str) + } + + protected isSingleLine(str: string): boolean { + let newLineCounts = 0 + for (const ch of str) { + if (ch === '\n') { + newLineCounts += 1 + } + } + + // since pressing Enter key possibly will generate string like '\n ' due to indention + if (this.isEnterKey(str)) { + return true + } + if (newLineCounts >= 1) { + return false + } + return true + } +} + +export class DefaultDocumentChangedType extends DocumentChangedType { + constructor(contentChanges: ReadonlyArray) { + super(contentChanges) + } + + checkChangeSource(): DocumentChangedSource { + if (this.contentChanges.length === 0) { + return DocumentChangedSource.Unknown + } + + // event.contentChanges.length will be 2 when user press Enter key multiple times + if (this.contentChanges.length > 2) { + return DocumentChangedSource.Reformatting + } + + // Case when event.contentChanges.length === 1 + const changedText = this.contentChanges[0].text + + if (this.isSingleLine(changedText)) { + if (changedText === '') { + return DocumentChangedSource.Deletion + } else if (this.isEnterKey(changedText)) { + return DocumentChangedSource.EnterKey + } else if (this.isTabKey(changedText)) { + return DocumentChangedSource.TabKey + } else if (this.isUserTypingSpecialChar(changedText)) { + return DocumentChangedSource.SpecialCharsKey + } else if (changedText.length === 1) { + return DocumentChangedSource.RegularKey + } else if (new RegExp('^[ ]+$').test(changedText)) { + // single line && single place reformat should consist of space chars only + return DocumentChangedSource.Reformatting + } else { + return DocumentChangedSource.Unknown + } + } + + // Won't trigger cwspr on multi-line changes + return DocumentChangedSource.Unknown + } +} + +export enum DocumentChangedSource { + SpecialCharsKey = 'SpecialCharsKey', + RegularKey = 'RegularKey', + TabKey = 'TabKey', + EnterKey = 'EnterKey', + Reformatting = 'Reformatting', + Deletion = 'Deletion', + Unknown = 'Unknown', +} diff --git a/packages/core/src/codewhisperer/service/recommendationHandler.ts b/packages/core/src/codewhisperer/service/recommendationHandler.ts new file mode 100644 index 00000000000..b354fb60a05 --- /dev/null +++ b/packages/core/src/codewhisperer/service/recommendationHandler.ts @@ -0,0 +1,731 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { extensionVersion } from '../../shared/vscode/env' +import { RecommendationsList, DefaultCodeWhispererClient, CognitoCredentialsError } from '../client/codewhisperer' +import * as EditorContext from '../util/editorContext' +import * as CodeWhispererConstants from '../models/constants' +import { ConfigurationEntry, GetRecommendationsResponse, vsCodeState } from '../models/model' +import { runtimeLanguageContext } from '../util/runtimeLanguageContext' +import { AWSError } from 'aws-sdk' +import { isAwsError } from '../../shared/errors' +import { TelemetryHelper } from '../util/telemetryHelper' +import { getLogger } from '../../shared/logger/logger' +import { hasVendedIamCredentials } from '../../auth/auth' +import { + asyncCallWithTimeout, + isInlineCompletionEnabled, + isVscHavingRegressionInlineCompletionApi, +} from '../util/commonUtil' +import { showTimedMessage } from '../../shared/utilities/messages' +import { + CodewhispererAutomatedTriggerType, + CodewhispererCompletionType, + CodewhispererGettingStartedTask, + CodewhispererTriggerType, + telemetry, +} from '../../shared/telemetry/telemetry' +import { CodeWhispererCodeCoverageTracker } from '../tracker/codewhispererCodeCoverageTracker' +import { invalidCustomizationMessage } from '../models/constants' +import { getSelectedCustomization, switchToBaseCustomizationAndNotify } from '../util/customizationUtil' +import { session } from '../util/codeWhispererSession' +import { Commands } from '../../shared/vscode/commands2' +import globals from '../../shared/extensionGlobals' +import { noSuggestions, updateInlineLockKey } from '../models/constants' +import AsyncLock from 'async-lock' +import { AuthUtil } from '../util/authUtil' +import { CWInlineCompletionItemProvider } from './inlineCompletionItemProvider' +import { application } from '../util/codeWhispererApplication' +import { openUrl } from '../../shared/utilities/vsCodeUtils' +import { indent } from '../../shared/utilities/textUtilities' +import path from 'path' +import { isIamConnection } from '../../auth/connection' +import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' +import { LanguageClient } from 'vscode-languageclient' + +/** + * This class is for getRecommendation/listRecommendation API calls and its states + * It does not contain UI/UX related logic + */ + +/** + * Commands as a level of indirection so that declare doesn't intercept any registrations for the + * language server implementation. + * + * Otherwise you'll get: + * "Unable to launch amazonq language server: Command "aws.amazonq.rejectCodeSuggestion" has already been declared by the Toolkit" + */ +function createCommands() { + // below commands override VS Code inline completion commands + const prevCommand = Commands.declare('editor.action.inlineSuggest.showPrevious', () => async () => { + await RecommendationHandler.instance.showRecommendation(-1) + }) + const nextCommand = Commands.declare('editor.action.inlineSuggest.showNext', () => async () => { + await RecommendationHandler.instance.showRecommendation(1) + }) + + const rejectCommand = Commands.declare('aws.amazonq.rejectCodeSuggestion', () => async () => { + telemetry.record({ + traceId: TelemetryHelper.instance.traceId, + }) + + await vscode.commands.executeCommand('editor.action.inlineSuggest.hide') + RecommendationHandler.instance.reportUserDecisions(-1) + await Commands.tryExecute('aws.amazonq.refreshAnnotation') + }) + + return { + prevCommand, + nextCommand, + rejectCommand, + } +} + +const lock = new AsyncLock({ maxPending: 1 }) + +export class RecommendationHandler { + public lastInvocationTime: number + // TODO: remove this requestId + public requestId: string + private nextToken: string + private cancellationToken: vscode.CancellationTokenSource + private _onDidReceiveRecommendation: vscode.EventEmitter = new vscode.EventEmitter() + public readonly onDidReceiveRecommendation: vscode.Event = this._onDidReceiveRecommendation.event + private inlineCompletionProvider?: CWInlineCompletionItemProvider + private inlineCompletionProviderDisposable?: vscode.Disposable + private reject: vscode.Disposable + private next: vscode.Disposable + private prev: vscode.Disposable + private _timer?: NodeJS.Timer + private languageClient?: LanguageClient + documentUri: vscode.Uri | undefined = undefined + + constructor() { + this.requestId = '' + this.nextToken = '' + this.lastInvocationTime = Date.now() - CodeWhispererConstants.invocationTimeIntervalThreshold * 1000 + this.cancellationToken = new vscode.CancellationTokenSource() + this.prev = new vscode.Disposable(() => {}) + this.next = new vscode.Disposable(() => {}) + this.reject = new vscode.Disposable(() => {}) + } + + static #instance: RecommendationHandler + + public static get instance() { + return (this.#instance ??= new this()) + } + + isValidResponse(): boolean { + return session.recommendations.some((r) => r.content.trim() !== '') + } + + setLanguageClient(languageClient: LanguageClient) { + this.languageClient = languageClient + } + + async getServerResponse( + triggerType: CodewhispererTriggerType, + isManualTriggerOn: boolean, + promise: Promise + ): Promise { + const timeoutMessage = hasVendedIamCredentials() + ? 'Generate recommendation timeout.' + : 'List recommendation timeout' + if (isManualTriggerOn && triggerType === 'OnDemand' && hasVendedIamCredentials()) { + return vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: CodeWhispererConstants.pendingResponse, + cancellable: false, + }, + async () => { + return await asyncCallWithTimeout( + promise, + timeoutMessage, + CodeWhispererConstants.promiseTimeoutLimit * 1000 + ) + } + ) + } + return await asyncCallWithTimeout(promise, timeoutMessage, CodeWhispererConstants.promiseTimeoutLimit * 1000) + } + + async getTaskTypeFromEditorFileName(filePath: string): Promise { + if (filePath.includes('CodeWhisperer_generate_suggestion')) { + return 'autoTrigger' + } else if (filePath.includes('CodeWhisperer_manual_invoke')) { + return 'manualTrigger' + } else if (filePath.includes('CodeWhisperer_use_comments')) { + return 'commentAsPrompt' + } else if (filePath.includes('CodeWhisperer_navigate_suggestions')) { + return 'navigation' + } else if (filePath.includes('Generate_unit_tests')) { + return 'unitTest' + } else { + return undefined + } + } + + async getRecommendations( + client: DefaultCodeWhispererClient, + editor: vscode.TextEditor, + triggerType: CodewhispererTriggerType, + config: ConfigurationEntry, + autoTriggerType?: CodewhispererAutomatedTriggerType, + pagination: boolean = true, + page: number = 0, + generate: boolean = isIamConnection(AuthUtil.instance.conn) + ): Promise { + let invocationResult: 'Succeeded' | 'Failed' = 'Failed' + let errorMessage: string | undefined = undefined + let errorCode: string | undefined = undefined + + if (!editor) { + return Promise.resolve({ + result: invocationResult, + errorMessage: errorMessage, + recommendationCount: 0, + }) + } + let recommendations: RecommendationsList = [] + let requestId = '' + let sessionId = '' + let reason = '' + let startTime = 0 + let latency = 0 + let nextToken = '' + let shouldRecordServiceInvocation = true + session.language = runtimeLanguageContext.getLanguageContext( + editor.document.languageId, + path.extname(editor.document.fileName) + ).language + session.taskType = await this.getTaskTypeFromEditorFileName(editor.document.fileName) + + if (pagination && !generate) { + if (page === 0) { + session.requestContext = await EditorContext.buildListRecommendationRequest( + editor as vscode.TextEditor, + this.nextToken, + config.isSuggestionsWithCodeReferencesEnabled, + this.languageClient + ) + } else { + session.requestContext = { + request: { + ...session.requestContext.request, + // Putting nextToken assignment in the end so it overwrites the existing nextToken + nextToken: this.nextToken, + }, + supplementalMetadata: session.requestContext.supplementalMetadata, + } + } + } else { + session.requestContext = await EditorContext.buildGenerateRecommendationRequest(editor as vscode.TextEditor) + } + const request = session.requestContext.request + // record preprocessing end time + TelemetryHelper.instance.setPreprocessEndTime() + + // set start pos for non pagination call or first pagination call + if (!pagination || (pagination && page === 0)) { + session.startPos = editor.selection.active + session.startCursorOffset = editor.document.offsetAt(session.startPos) + session.leftContextOfCurrentLine = EditorContext.getLeftContext(editor, session.startPos.line) + session.triggerType = triggerType + session.autoTriggerType = autoTriggerType + + /** + * Validate request + */ + if (!EditorContext.validateRequest(request)) { + getLogger().verbose('Invalid Request: %O', request) + const languageName = request.fileContext.programmingLanguage.languageName + if (!runtimeLanguageContext.isLanguageSupported(languageName)) { + errorMessage = `${languageName} is currently not supported by Amazon Q inline suggestions` + } + return Promise.resolve({ + result: invocationResult, + errorMessage: errorMessage, + recommendationCount: 0, + }) + } + } + + try { + startTime = Date.now() + this.lastInvocationTime = startTime + const mappedReq = runtimeLanguageContext.mapToRuntimeLanguage(request) + const codewhispererPromise = + pagination && !generate + ? client.listRecommendations(mappedReq) + : client.generateRecommendations(mappedReq) + const resp = await this.getServerResponse(triggerType, config.isManualTriggerEnabled, codewhispererPromise) + TelemetryHelper.instance.setSdkApiCallEndTime() + latency = startTime !== 0 ? Date.now() - startTime : 0 + if ('recommendations' in resp) { + recommendations = (resp && resp.recommendations) || [] + } else { + recommendations = (resp && resp.completions) || [] + } + invocationResult = 'Succeeded' + requestId = resp?.$response && resp?.$response?.requestId + nextToken = resp?.nextToken ? resp?.nextToken : '' + sessionId = resp?.$response?.httpResponse?.headers['x-amzn-sessionid'] + TelemetryHelper.instance.setFirstResponseRequestId(requestId) + if (page === 0) { + session.setTimeToFirstRecommendation(Date.now()) + } + if (nextToken === '') { + TelemetryHelper.instance.setAllPaginationEndTime() + } + } catch (error) { + if (error instanceof CognitoCredentialsError) { + shouldRecordServiceInvocation = false + } + if (latency === 0) { + latency = startTime !== 0 ? Date.now() - startTime : 0 + } + getLogger().error('amazonq inline-suggest: Invocation Exception : %s', (error as Error).message) + if (isAwsError(error)) { + errorMessage = error.message + requestId = error.requestId || '' + errorCode = error.code + reason = `CodeWhisperer Invocation Exception: ${error?.code ?? error?.name ?? 'unknown'}` + await this.onThrottlingException(error, triggerType) + + if (error?.code === 'AccessDeniedException' && errorMessage?.includes('no identity-based policy')) { + getLogger().error('amazonq inline-suggest: AccessDeniedException : %s', (error as Error).message) + void vscode.window + .showErrorMessage(`CodeWhisperer: ${error?.message}`, CodeWhispererConstants.settingsLearnMore) + .then(async (resp) => { + if (resp === CodeWhispererConstants.settingsLearnMore) { + void openUrl(vscode.Uri.parse(CodeWhispererConstants.learnMoreUri)) + } + }) + await vscode.commands.executeCommand('aws.amazonq.enableCodeSuggestions', false) + } + } else { + errorMessage = error instanceof Error ? error.message : String(error) + reason = error ? String(error) : 'unknown' + } + } finally { + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone + + let msg = indent( + `codewhisperer: request-id: ${requestId}, + timestamp(epoch): ${Date.now()}, + timezone: ${timezone}, + datetime: ${new Date().toLocaleString([], { timeZone: timezone })}, + vscode version: '${vscode.version}', + extension version: '${extensionVersion}', + filename: '${EditorContext.getFileName(editor)}', + left context of line: '${session.leftContextOfCurrentLine}', + line number: ${session.startPos.line}, + character location: ${session.startPos.character}, + latency: ${latency} ms. + Recommendations:`, + 4, + true + ).trimStart() + for (const [index, item] of recommendations.entries()) { + msg += `\n ${index.toString().padStart(2, '0')}: ${indent(item.content, 8, true).trim()}` + session.requestIdList.push(requestId) + } + getLogger().debug(msg) + if (invocationResult === 'Succeeded') { + CodeWhispererCodeCoverageTracker.getTracker(session.language)?.incrementServiceInvocationCount() + UserWrittenCodeTracker.instance.onQFeatureInvoked() + } else { + if ( + (errorMessage?.includes(invalidCustomizationMessage) && errorCode === 'AccessDeniedException') || + errorCode === 'ResourceNotFoundException' + ) { + getLogger() + .debug(`The selected customization is no longer available. Retrying with the default model. + Failed request id: ${requestId}`) + await switchToBaseCustomizationAndNotify() + await this.getRecommendations( + client, + editor, + triggerType, + config, + autoTriggerType, + pagination, + page, + true + ) + } + } + + if (shouldRecordServiceInvocation) { + TelemetryHelper.instance.recordServiceInvocationTelemetry( + requestId, + sessionId, + session.recommendations.length + recommendations.length - 1, + invocationResult, + latency, + session.language, + session.taskType, + reason, + session.requestContext.supplementalMetadata + ) + } + } + + if (this.isCancellationRequested()) { + return Promise.resolve({ + result: invocationResult, + errorMessage: errorMessage, + recommendationCount: session.recommendations.length, + }) + } + + const typedPrefix = editor.document + .getText(new vscode.Range(session.startPos, editor.selection.active)) + .replace('\r\n', '\n') + if (recommendations.length > 0) { + TelemetryHelper.instance.setTypeAheadLength(typedPrefix.length) + // mark suggestions that does not match typeahead when arrival as Discard + // these suggestions can be marked as Showed if typeahead can be removed with new inline API + for (const [i, r] of recommendations.entries()) { + const recommendationIndex = i + session.recommendations.length + if ( + !r.content.startsWith(typedPrefix) && + session.getSuggestionState(recommendationIndex) === undefined + ) { + session.setSuggestionState(recommendationIndex, 'Discard') + } + session.setCompletionType(recommendationIndex, r) + } + session.recommendations = pagination ? session.recommendations.concat(recommendations) : recommendations + if (isInlineCompletionEnabled() && this.hasAtLeastOneValidSuggestion(typedPrefix)) { + this._onDidReceiveRecommendation.fire() + } + } + + this.requestId = requestId + session.sessionId = sessionId + this.nextToken = nextToken + + // send Empty userDecision event if user receives no recommendations in this session at all. + if (invocationResult === 'Succeeded' && nextToken === '') { + // case 1: empty list of suggestion [] + if (session.recommendations.length === 0) { + session.requestIdList.push(requestId) + // Received an empty list of recommendations + TelemetryHelper.instance.recordUserDecisionTelemetryForEmptyList( + session.requestIdList, + sessionId, + page, + runtimeLanguageContext.getLanguageContext( + editor.document.languageId, + path.extname(editor.document.fileName) + ).language, + session.requestContext.supplementalMetadata + ) + } + // case 2: non empty list of suggestion but with (a) empty content or (b) non-matching typeahead + else if (!this.hasAtLeastOneValidSuggestion(typedPrefix)) { + this.reportUserDecisions(-1) + } + } + return Promise.resolve({ + result: invocationResult, + errorMessage: errorMessage, + recommendationCount: session.recommendations.length, + }) + } + + hasAtLeastOneValidSuggestion(typedPrefix: string): boolean { + return session.recommendations.some((r) => r.content.trim() !== '' && r.content.startsWith(typedPrefix)) + } + + cancelPaginatedRequest() { + this.nextToken = '' + this.cancellationToken.cancel() + } + + isCancellationRequested() { + return this.cancellationToken.token.isCancellationRequested + } + + checkAndResetCancellationTokens() { + if (this.isCancellationRequested()) { + this.cancellationToken.dispose() + this.cancellationToken = new vscode.CancellationTokenSource() + this.nextToken = '' + return true + } + return false + } + /** + * Clear recommendation state + */ + clearRecommendations() { + session.requestIdList = [] + session.recommendations = [] + session.suggestionStates = new Map() + session.completionTypes = new Map() + this.requestId = '' + session.sessionId = '' + this.nextToken = '' + session.requestContext.supplementalMetadata = undefined + } + + async clearInlineCompletionStates() { + try { + vsCodeState.isCodeWhispererEditing = false + application()._clearCodeWhispererUIListener.fire() + this.cancelPaginatedRequest() + this.clearRecommendations() + this.disposeInlineCompletion() + await vscode.commands.executeCommand('aws.amazonq.refreshStatusBar') + // fix a regression that requires user to hit Esc twice to clear inline ghost text + // because disposing a provider does not clear the UX + if (isVscHavingRegressionInlineCompletionApi()) { + await vscode.commands.executeCommand('editor.action.inlineSuggest.hide') + } + } finally { + this.clearRejectionTimer() + } + } + + reportDiscardedUserDecisions() { + for (const [i, _] of session.recommendations.entries()) { + session.setSuggestionState(i, 'Discard') + } + this.reportUserDecisions(-1) + } + + /** + * Emits telemetry reflecting user decision for current recommendation. + */ + reportUserDecisions(acceptIndex: number) { + if (session.sessionId === '' || this.requestId === '') { + return + } + TelemetryHelper.instance.recordUserDecisionTelemetry( + session.requestIdList, + session.sessionId, + session.recommendations, + acceptIndex, + session.recommendations.length, + session.completionTypes, + session.suggestionStates, + session.requestContext.supplementalMetadata + ) + if (isInlineCompletionEnabled()) { + this.clearInlineCompletionStates().catch((e) => { + getLogger().error('clearInlineCompletionStates failed: %s', (e as Error).message) + }) + } + } + + hasNextToken(): boolean { + return this.nextToken !== '' + } + + canShowRecommendationInIntelliSense( + editor: vscode.TextEditor, + showPrompt: boolean = false, + response: GetRecommendationsResponse + ): boolean { + const reject = () => { + this.reportUserDecisions(-1) + } + if (!this.isValidResponse()) { + if (showPrompt) { + void showTimedMessage(response.errorMessage ? response.errorMessage : noSuggestions, 3000) + } + reject() + return false + } + // do not show recommendation if cursor is before invocation position + // also mark as Discard + if (editor.selection.active.isBefore(session.startPos)) { + for (const [i, _] of session.recommendations.entries()) { + session.setSuggestionState(i, 'Discard') + } + reject() + return false + } + + // do not show recommendation if typeahead does not match + // also mark as Discard + const typedPrefix = editor.document.getText( + new vscode.Range( + session.startPos.line, + session.startPos.character, + editor.selection.active.line, + editor.selection.active.character + ) + ) + if (!session.recommendations[0].content.startsWith(typedPrefix.trimStart())) { + for (const [i, _] of session.recommendations.entries()) { + session.setSuggestionState(i, 'Discard') + } + reject() + return false + } + return true + } + + async onThrottlingException(awsError: AWSError, triggerType: CodewhispererTriggerType) { + if ( + awsError.code === 'ThrottlingException' && + awsError.message.includes(CodeWhispererConstants.throttlingMessage) + ) { + if (triggerType === 'OnDemand') { + void vscode.window.showErrorMessage(CodeWhispererConstants.freeTierLimitReached) + } + vsCodeState.isFreeTierLimitReached = true + } + } + + public disposeInlineCompletion() { + this.inlineCompletionProviderDisposable?.dispose() + this.inlineCompletionProvider = undefined + } + + private disposeCommandOverrides() { + this.prev.dispose() + this.reject.dispose() + this.next.dispose() + } + + // These commands override the vs code inline completion commands + // They are subscribed when suggestion starts and disposed when suggestion is accepted/rejected + // to avoid impacting other plugins or user who uses this API + private registerCommandOverrides() { + const { prevCommand, nextCommand, rejectCommand } = createCommands() + this.prev = prevCommand.register() + this.next = nextCommand.register() + this.reject = rejectCommand.register() + } + + subscribeSuggestionCommands() { + this.disposeCommandOverrides() + this.registerCommandOverrides() + globals.context.subscriptions.push(this.prev) + globals.context.subscriptions.push(this.next) + globals.context.subscriptions.push(this.reject) + } + + async showRecommendation(indexShift: number, noSuggestionVisible: boolean = false) { + await lock.acquire(updateInlineLockKey, async () => { + if (!vscode.window.state.focused) { + this.reportDiscardedUserDecisions() + return + } + const inlineCompletionProvider = new CWInlineCompletionItemProvider( + this.inlineCompletionProvider?.getActiveItemIndex, + indexShift, + session.recommendations, + this.requestId, + session.startPos, + this.nextToken + ) + this.inlineCompletionProviderDisposable?.dispose() + // when suggestion is active, registering a new provider will let VS Code invoke inline API automatically + this.inlineCompletionProviderDisposable = vscode.languages.registerInlineCompletionItemProvider( + Object.assign([], CodeWhispererConstants.platformLanguageIds), + inlineCompletionProvider + ) + this.inlineCompletionProvider = inlineCompletionProvider + + if (isVscHavingRegressionInlineCompletionApi() && !noSuggestionVisible) { + // fix a regression in new VS Code when disposing and re-registering + // a new provider does not auto refresh the inline suggestion widget + // by manually refresh it + await vscode.commands.executeCommand('editor.action.inlineSuggest.hide') + await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger') + } + if (noSuggestionVisible) { + await vscode.commands.executeCommand(`editor.action.inlineSuggest.trigger`) + this.sendPerceivedLatencyTelemetry() + } + }) + } + + async onEditorChange() { + this.reportUserDecisions(-1) + } + + async onFocusChange() { + this.reportUserDecisions(-1) + } + + async onCursorChange(e: vscode.TextEditorSelectionChangeEvent) { + // we do not want to reset the states for keyboard events because they can be typeahead + if ( + e.kind !== vscode.TextEditorSelectionChangeKind.Keyboard && + vscode.window.activeTextEditor === e.textEditor + ) { + application()._clearCodeWhispererUIListener.fire() + // when cursor change due to mouse movement we need to reset the active item index for inline + if (e.kind === vscode.TextEditorSelectionChangeKind.Mouse) { + this.inlineCompletionProvider?.clearActiveItemIndex() + } + } + } + + isSuggestionVisible(): boolean { + return this.inlineCompletionProvider?.getActiveItemIndex !== undefined + } + + async tryShowRecommendation() { + const editor = vscode.window.activeTextEditor + if (editor === undefined) { + return + } + if (this.isSuggestionVisible()) { + // do not force refresh the tooltip to avoid suggestion "flashing" + return + } + if ( + editor.selection.active.isBefore(session.startPos) || + editor.document.uri.fsPath !== this.documentUri?.fsPath + ) { + for (const [i, _] of session.recommendations.entries()) { + session.setSuggestionState(i, 'Discard') + } + this.reportUserDecisions(-1) + } else if (session.recommendations.length > 0) { + await this.showRecommendation(0, true) + } + } + + private clearRejectionTimer() { + if (this._timer !== undefined) { + clearInterval(this._timer) + this._timer = undefined + } + } + + private sendPerceivedLatencyTelemetry() { + if (vscode.window.activeTextEditor) { + const languageContext = runtimeLanguageContext.getLanguageContext( + vscode.window.activeTextEditor.document.languageId, + vscode.window.activeTextEditor.document.fileName.substring( + vscode.window.activeTextEditor.document.fileName.lastIndexOf('.') + 1 + ) + ) + telemetry.codewhisperer_perceivedLatency.emit({ + codewhispererRequestId: this.requestId, + codewhispererSessionId: session.sessionId, + codewhispererTriggerType: session.triggerType, + codewhispererCompletionType: session.getCompletionType(0), + codewhispererCustomizationArn: getSelectedCustomization().arn, + codewhispererLanguage: languageContext.language, + duration: Date.now() - this.lastInvocationTime, + passive: true, + credentialStartUrl: AuthUtil.instance.startUrl, + result: 'Succeeded', + }) + } + } +} diff --git a/packages/core/src/codewhisperer/service/recommendationService.ts b/packages/core/src/codewhisperer/service/recommendationService.ts new file mode 100644 index 00000000000..de78b435913 --- /dev/null +++ b/packages/core/src/codewhisperer/service/recommendationService.ts @@ -0,0 +1,122 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as vscode from 'vscode' +import { ConfigurationEntry, GetRecommendationsResponse } from '../models/model' +import { isInlineCompletionEnabled } from '../util/commonUtil' +import { + CodewhispererAutomatedTriggerType, + CodewhispererTriggerType, + telemetry, +} from '../../shared/telemetry/telemetry' +import { InlineCompletionService } from '../service/inlineCompletionService' +import { ClassifierTrigger } from './classifierTrigger' +import { DefaultCodeWhispererClient } from '../client/codewhisperer' +import { randomUUID } from '../../shared/crypto' +import { TelemetryHelper } from '../util/telemetryHelper' +import { AuthUtil } from '../util/authUtil' + +export interface SuggestionActionEvent { + readonly editor: vscode.TextEditor | undefined + readonly isRunning: boolean + readonly triggerType: CodewhispererTriggerType + readonly response: GetRecommendationsResponse | undefined +} + +export class RecommendationService { + static #instance: RecommendationService + + private _isRunning: boolean = false + get isRunning() { + return this._isRunning + } + + private _onSuggestionActionEvent = new vscode.EventEmitter() + get suggestionActionEvent(): vscode.Event { + return this._onSuggestionActionEvent.event + } + + private _acceptedSuggestionCount: number = 0 + get acceptedSuggestionCount() { + return this._acceptedSuggestionCount + } + + private _totalValidTriggerCount: number = 0 + get totalValidTriggerCount() { + return this._totalValidTriggerCount + } + + public static get instance() { + return (this.#instance ??= new RecommendationService()) + } + + incrementAcceptedCount() { + this._acceptedSuggestionCount++ + } + + incrementValidTriggerCount() { + this._totalValidTriggerCount++ + } + + async generateRecommendation( + client: DefaultCodeWhispererClient, + editor: vscode.TextEditor, + triggerType: CodewhispererTriggerType, + config: ConfigurationEntry, + autoTriggerType?: CodewhispererAutomatedTriggerType, + event?: vscode.TextDocumentChangeEvent + ) { + // TODO: should move all downstream auth check(inlineCompletionService, recommendationHandler etc) to here(upstream) instead of spreading everywhere + if (AuthUtil.instance.isConnected() && AuthUtil.instance.requireProfileSelection()) { + return + } + + if (this._isRunning) { + return + } + + /** + * Use an existing trace ID if invoked through a command (e.g., manual invocation), + * otherwise generate a new trace ID + */ + const traceId = telemetry.attributes?.traceId ?? randomUUID() + TelemetryHelper.instance.setTraceId(traceId) + await telemetry.withTraceId(async () => { + if (isInlineCompletionEnabled()) { + if (triggerType === 'OnDemand') { + ClassifierTrigger.instance.recordClassifierResultForManualTrigger(editor) + } + + this._isRunning = true + let response: GetRecommendationsResponse | undefined = undefined + + try { + this._onSuggestionActionEvent.fire({ + editor: editor, + isRunning: true, + triggerType: triggerType, + response: undefined, + }) + + response = await InlineCompletionService.instance.getPaginatedRecommendation( + client, + editor, + triggerType, + config, + autoTriggerType, + event + ) + } finally { + this._isRunning = false + this._onSuggestionActionEvent.fire({ + editor: editor, + isRunning: false, + triggerType: triggerType, + response: response, + }) + } + } + }, traceId) + } +} diff --git a/packages/core/src/codewhisperer/service/referenceInlineProvider.ts b/packages/core/src/codewhisperer/service/referenceInlineProvider.ts index a90565797fb..6fe0cf122f2 100644 --- a/packages/core/src/codewhisperer/service/referenceInlineProvider.ts +++ b/packages/core/src/codewhisperer/service/referenceInlineProvider.ts @@ -35,7 +35,7 @@ export class ReferenceInlineProvider implements vscode.CodeLensProvider { } public setInlineReference(line: number, suggestion: string, references: References | undefined) { - const startTime = performance.now() + const startTime = Date.now() this.ranges = [] this.refs = [] if ( @@ -53,7 +53,7 @@ export class ReferenceInlineProvider implements vscode.CodeLensProvider { const licenses = [...n].join(', ') this.ranges.push(new vscode.Range(line, 0, line, 1)) this.refs.push(CodeWhispererConstants.suggestionDetailReferenceText(licenses)) - const duration = performance.now() - startTime + const duration = Date.now() - startTime if (duration > 100) { getLogger().warn(`setInlineReference takes ${duration}ms`) } @@ -70,7 +70,7 @@ export class ReferenceInlineProvider implements vscode.CodeLensProvider { document: vscode.TextDocument, token: vscode.CancellationToken ): vscode.CodeLens[] | Thenable { - const startTime = performance.now() + const startTime = Date.now() const codeLenses: vscode.CodeLens[] = [] for (let i = 0; i < this.ranges.length; i++) { const codeLens = new vscode.CodeLens(this.ranges[i]) @@ -82,7 +82,7 @@ export class ReferenceInlineProvider implements vscode.CodeLensProvider { } codeLenses.push(codeLens) } - const duration = performance.now() - startTime + const duration = Date.now() - startTime if (duration > 100) { getLogger().warn(`setInlineReference takes ${duration}ms`) } diff --git a/packages/core/src/codewhisperer/service/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/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 20ef306f7ab..00dc16398da 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts @@ -743,7 +743,7 @@ export async function pollTransformationJob(jobId: string, validStates: string[] } await sleep(CodeWhispererConstants.transformationJobPollingIntervalSeconds * 1000) } catch (e: any) { - getLogger().error(`CodeTransformation: GetTransformation error = %O`, e) + getLogger().error(`CodeTransformation: error = %O`, e) throw e } } @@ -827,11 +827,11 @@ async function processClientInstructions(jobId: string, clientInstructionsPath: getLogger().info(`CodeTransformation: copied project to ${destinationPath}`) const diffContents = await fs.readFileText(clientInstructionsPath) if (diffContents.trim()) { - const diffModel = new DiffModel() - diffModel.parseDiff(clientInstructionsPath, path.join(destinationPath, 'sources'), true) // show user the diff.patch const doc = await vscode.workspace.openTextDocument(clientInstructionsPath) await vscode.window.showTextDocument(doc, { viewColumn: vscode.ViewColumn.One }) + const diffModel = new DiffModel() + diffModel.parseDiff(clientInstructionsPath, path.join(destinationPath, 'sources'), true) } else { // still need to set the project copy so that we can use it below transformByQState.setProjectCopyFilePath(path.join(destinationPath, 'sources')) diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts index 88f34a799d1..91ee8f6639c 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts @@ -6,17 +6,20 @@ 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, transformByQState } from '../../models/model' -import { IManifestFile } from '../../../amazonqFeatureDev/models' 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 async function getDependenciesFolderInfo(): Promise { const dependencyFolderName = `${CodeWhispererConstants.dependencyFolderName}${globals.clock.Date.now()}` @@ -117,15 +120,53 @@ 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 dependenciesAndPlugins) { + const errorMessage = validateItem(item) + 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, validOriginTypes: string[] = ['FIRST_PARTY', 'THIRD_PARTY']) { + if (!/^[^\s:]+:[^\s:]+$/.test(item.identifier)) { + getLogger().info(`CodeTransformation: Invalid identifier format: ${item.identifier}`) + return `Invalid identifier format: \`${item.identifier}\`. Must be in format \`groupId:artifactId\` without spaces` + } + 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) { @@ -347,3 +388,40 @@ export async function parseVersionsListFromPomFile(xmlString: string): 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], + }) + } + } + 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 +) { + const historyLogFilePath = path.join(os.homedir(), '.aws', 'transform', 'transformation_history.tsv') + // create transform folder if necessary + if (!(await fs.existsFile(historyLogFilePath))) { + await fs.mkdir(path.dirname(historyLogFilePath)) + // create headers of new transformation history file + await fs.writeFile(historyLogFilePath, 'date\tproject_name\tstatus\tduration\tdiff_patch\tsummary\tjob_id\n') + } + 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, + ] + + 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') + // startTime: jobInfo[0], projectName: jobInfo[1], status: jobInfo[2], duration: jobInfo[3], diffPath: jobInfo[4], summaryPath: jobInfo[5], jobId: jobInfo[6] + 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\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..35e8319ab46 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,19 @@ 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(), + }) + } return ` @@ -99,18 +133,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 +204,48 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider - + + + + - - - - - - - + ${history + .map( + (job) => ` + + + + + + + + + + + ` + ) + .join('')}
Project Status DurationIdDiff PatchSummary FileJob IdRefresh Job
${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} + +
` diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts b/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts index 0b678f8120d..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,6 +166,8 @@ export class DiffModel { throw new Error(CodeWhispererConstants.noChangesMadeMessage) } + getLogger().info(`CodeTransformation: parsing patch file at ${pathToDiff}`) + let changedFiles = parsePatch(diffContents) // exclude dependency_upgrade.yml from patch application changedFiles = changedFiles.filter((file) => !file.oldFileName?.includes('dependency_upgrade')) @@ -426,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) @@ -441,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/codewhispererCodeCoverageTracker.ts b/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts new file mode 100644 index 00000000000..0989f022245 --- /dev/null +++ b/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts @@ -0,0 +1,319 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getLogger } from '../../shared/logger/logger' +import * as CodeWhispererConstants from '../models/constants' +import globals from '../../shared/extensionGlobals' +import { vsCodeState } from '../models/model' +import { CodewhispererLanguage, telemetry } from '../../shared/telemetry/telemetry' +import { runtimeLanguageContext } from '../util/runtimeLanguageContext' +import { TelemetryHelper } from '../util/telemetryHelper' +import { AuthUtil } from '../util/authUtil' +import { getSelectedCustomization } from '../util/customizationUtil' +import { codeWhispererClient as client } from '../client/codewhisperer' +import { isAwsError } from '../../shared/errors' +import { getUnmodifiedAcceptedTokens } from '../util/commonUtil' + +interface CodeWhispererToken { + range: vscode.Range + text: string + accepted: number +} + +const autoClosingKeystrokeInputs = ['[]', '{}', '()', '""', "''"] + +/** + * This singleton class is mainly used for calculating the code written by codeWhisperer + * TODO: Remove this tracker, uses user written code tracker instead. + * This is kept in codebase for server side backward compatibility until service fully switch to user written code + */ +export class CodeWhispererCodeCoverageTracker { + private _acceptedTokens: { [key: string]: CodeWhispererToken[] } + private _totalTokens: { [key: string]: number } + private _timer?: NodeJS.Timer + private _startTime: number + private _language: CodewhispererLanguage + private _serviceInvocationCount: number + + private constructor(language: CodewhispererLanguage) { + this._acceptedTokens = {} + this._totalTokens = {} + this._startTime = 0 + this._language = language + this._serviceInvocationCount = 0 + } + + public get serviceInvocationCount(): number { + return this._serviceInvocationCount + } + + public get acceptedTokens(): { [key: string]: CodeWhispererToken[] } { + return this._acceptedTokens + } + + public get totalTokens(): { [key: string]: number } { + return this._totalTokens + } + + public isActive(): boolean { + return TelemetryHelper.instance.isTelemetryEnabled() && AuthUtil.instance.isConnected() + } + + public incrementServiceInvocationCount() { + this._serviceInvocationCount += 1 + } + + public flush() { + if (!this.isActive()) { + this._totalTokens = {} + this._acceptedTokens = {} + this.closeTimer() + return + } + try { + this.emitCodeWhispererCodeContribution() + } catch (error) { + getLogger().error(`Encountered ${error} when emitting code contribution metric`) + } + } + + // TODO: Improve the range tracking of the accepted recommendation + // TODO: use the editor of the filename, not the current editor + public updateAcceptedTokensCount(editor: vscode.TextEditor) { + const filename = editor.document.fileName + if (filename in this._acceptedTokens) { + for (let i = 0; i < this._acceptedTokens[filename].length; i++) { + const oldText = this._acceptedTokens[filename][i].text + const newText = editor.document.getText(this._acceptedTokens[filename][i].range) + this._acceptedTokens[filename][i].accepted = getUnmodifiedAcceptedTokens(oldText, newText) + } + } + } + + public emitCodeWhispererCodeContribution() { + let totalTokens = 0 + for (const filename in this._totalTokens) { + totalTokens += this._totalTokens[filename] + } + if (vscode.window.activeTextEditor) { + this.updateAcceptedTokensCount(vscode.window.activeTextEditor) + } + // the accepted characters without counting user modification + let acceptedTokens = 0 + // the accepted characters after calculating user modification + let unmodifiedAcceptedTokens = 0 + for (const filename in this._acceptedTokens) { + for (const v of this._acceptedTokens[filename]) { + if (filename in this._totalTokens && this._totalTokens[filename] >= v.accepted) { + unmodifiedAcceptedTokens += v.accepted + acceptedTokens += v.text.length + } + } + } + const percentCount = ((acceptedTokens / totalTokens) * 100).toFixed(2) + const percentage = Math.round(parseInt(percentCount)) + const selectedCustomization = getSelectedCustomization() + if (this._serviceInvocationCount <= 0) { + getLogger().debug(`Skip emiting code contribution metric`) + return + } + telemetry.codewhisperer_codePercentage.emit({ + codewhispererTotalTokens: totalTokens, + codewhispererLanguage: this._language, + codewhispererAcceptedTokens: unmodifiedAcceptedTokens, + codewhispererSuggestedTokens: acceptedTokens, + codewhispererPercentage: percentage ? percentage : 0, + successCount: this._serviceInvocationCount, + codewhispererCustomizationArn: selectedCustomization.arn === '' ? undefined : selectedCustomization.arn, + credentialStartUrl: AuthUtil.instance.startUrl, + }) + + client + .sendTelemetryEvent({ + telemetryEvent: { + codeCoverageEvent: { + customizationArn: selectedCustomization.arn === '' ? undefined : selectedCustomization.arn, + programmingLanguage: { + languageName: runtimeLanguageContext.toRuntimeLanguage(this._language), + }, + acceptedCharacterCount: acceptedTokens, + unmodifiedAcceptedCharacterCount: unmodifiedAcceptedTokens, + totalCharacterCount: totalTokens, + timestamp: new Date(Date.now()), + }, + }, + profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, + }) + .then() + .catch((error) => { + let requestId: string | undefined + if (isAwsError(error)) { + requestId = error.requestId + } + + getLogger().debug( + `Failed to sendTelemetryEvent to CodeWhisperer, requestId: ${requestId ?? ''}, message: ${ + error.message + }` + ) + }) + } + + private tryStartTimer() { + if (this._timer !== undefined) { + return + } + const currentDate = new globals.clock.Date() + this._startTime = currentDate.getTime() + this._timer = setTimeout(() => { + try { + const currentTime = new globals.clock.Date().getTime() + const delay: number = CodeWhispererConstants.defaultCheckPeriodMillis + const diffTime: number = this._startTime + delay + if (diffTime <= currentTime) { + let totalTokens = 0 + for (const filename in this._totalTokens) { + totalTokens += this._totalTokens[filename] + } + if (totalTokens > 0) { + this.flush() + } else { + getLogger().debug( + `CodeWhispererCodeCoverageTracker: skipped telemetry due to empty tokens array` + ) + } + } + } catch (e) { + getLogger().verbose(`Exception Thrown from CodeWhispererCodeCoverageTracker: ${e}`) + } finally { + this.resetTracker() + this.closeTimer() + } + }, CodeWhispererConstants.defaultCheckPeriodMillis) + } + + private resetTracker() { + this._totalTokens = {} + this._acceptedTokens = {} + this._startTime = 0 + this._serviceInvocationCount = 0 + } + + private closeTimer() { + if (this._timer !== undefined) { + clearTimeout(this._timer) + this._timer = undefined + } + } + + public addAcceptedTokens(filename: string, token: CodeWhispererToken) { + if (!(filename in this._acceptedTokens)) { + this._acceptedTokens[filename] = [] + } + this._acceptedTokens[filename].push(token) + } + + public addTotalTokens(filename: string, count: number) { + if (!(filename in this._totalTokens)) { + this._totalTokens[filename] = 0 + } + this._totalTokens[filename] += count + if (this._totalTokens[filename] < 0) { + this._totalTokens[filename] = 0 + } + } + + public countAcceptedTokens(range: vscode.Range, text: string, filename: string) { + if (!this.isActive()) { + return + } + // generate accepted recommendation token and stored in collection + this.addAcceptedTokens(filename, { range: range, text: text, accepted: text.length }) + this.addTotalTokens(filename, text.length) + } + + // For below 2 edge cases + // 1. newline character with indentation + // 2. 2 character insertion of closing brackets + public getCharacterCountFromComplexEvent(e: vscode.TextDocumentChangeEvent) { + function countChanges(cond: boolean, text: string): number { + if (!cond) { + return 0 + } + if ((text.startsWith('\n') || text.startsWith('\r\n')) && text.trim().length === 0) { + return 1 + } + if (autoClosingKeystrokeInputs.includes(text)) { + return 2 + } + return 0 + } + if (e.contentChanges.length === 2) { + const text1 = e.contentChanges[0].text + const text2 = e.contentChanges[1].text + const text2Count = countChanges(text1.length === 0, text2) + const text1Count = countChanges(text2.length === 0, text1) + return text2Count > 0 ? text2Count : text1Count + } else if (e.contentChanges.length === 1) { + return countChanges(true, e.contentChanges[0].text) + } + return 0 + } + + public isFromUserKeystroke(e: vscode.TextDocumentChangeEvent) { + return e.contentChanges.length === 1 && e.contentChanges[0].text.length === 1 + } + + public countTotalTokens(e: vscode.TextDocumentChangeEvent) { + // ignore no contentChanges. ignore contentChanges from other plugins (formatters) + // only include contentChanges from user keystroke input(one character input). + // Also ignore deletion events due to a known issue of tracking deleted CodeWhiperer tokens. + if (!runtimeLanguageContext.isLanguageSupported(e.document.languageId) || vsCodeState.isCodeWhispererEditing) { + return + } + // a user keystroke input can be + // 1. content change with 1 character insertion + // 2. newline character with indentation + // 3. 2 character insertion of closing brackets + if (this.isFromUserKeystroke(e)) { + this.tryStartTimer() + this.addTotalTokens(e.document.fileName, 1) + } else if (this.getCharacterCountFromComplexEvent(e) !== 0) { + this.tryStartTimer() + const characterIncrease = this.getCharacterCountFromComplexEvent(e) + this.addTotalTokens(e.document.fileName, characterIncrease) + } + // also include multi character input within 50 characters (not from CWSPR) + else if ( + e.contentChanges.length === 1 && + e.contentChanges[0].text.length > 1 && + TelemetryHelper.instance.lastSuggestionInDisplay !== e.contentChanges[0].text + ) { + const multiCharInputSize = e.contentChanges[0].text.length + + // select 50 as the cut-off threshold for counting user input. + // ignore all white space multi char input, this usually comes from reformat. + if (multiCharInputSize < 50 && e.contentChanges[0].text.trim().length > 0) { + this.addTotalTokens(e.document.fileName, multiCharInputSize) + } + } + } + + public static readonly instances = new Map() + + public static getTracker(language: string): CodeWhispererCodeCoverageTracker | undefined { + if (!runtimeLanguageContext.isLanguageSupported(language)) { + return undefined + } + const cwsprLanguage = runtimeLanguageContext.normalizeLanguage(language) + if (!cwsprLanguage) { + return undefined + } + const instance = this.instances.get(cwsprLanguage) ?? new this(cwsprLanguage) + this.instances.set(cwsprLanguage, instance) + return instance + } +} diff --git a/packages/core/src/codewhisperer/tracker/userWrittenCodeTracker.ts b/packages/core/src/codewhisperer/tracker/userWrittenCodeTracker.ts index 32de471878d..7dfb14b5745 100644 --- a/packages/core/src/codewhisperer/tracker/userWrittenCodeTracker.ts +++ b/packages/core/src/codewhisperer/tracker/userWrittenCodeTracker.ts @@ -53,7 +53,7 @@ export class UserWrittenCodeTracker { // for all Q features public onQFeatureInvoked() { this._qUsageCount += 1 - this._lastQInvocationTime = performance.now() + this._lastQInvocationTime = Date.now() } public onQStartsMakingEdits() { @@ -129,10 +129,10 @@ export class UserWrittenCodeTracker { this.reset() return } - const startTime = performance.now() + const startTime = Date.now() this._timer = setTimeout(() => { try { - const currentTime = performance.now() + const currentTime = Date.now() const delay: number = UserWrittenCodeTracker.defaultCheckPeriodMillis const diffTime: number = startTime + delay if (diffTime <= currentTime) { @@ -169,7 +169,7 @@ export class UserWrittenCodeTracker { // due to unhandled edge cases or early terminated code paths // reset it back to false after a reasonable period of time if (this._qIsMakingEdits) { - if (performance.now() - this._lastQInvocationTime > UserWrittenCodeTracker.resetQIsEditingTimeoutMs) { + if (Date.now() - this._lastQInvocationTime > UserWrittenCodeTracker.resetQIsEditingTimeoutMs) { getLogger().warn(`Reset Q is editing state to false.`) this._qIsMakingEdits = false } diff --git a/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts b/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts index 28ed3952494..c1934ec6a73 100644 --- a/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts +++ b/packages/core/src/codewhisperer/ui/codeWhispererNodes.ts @@ -21,7 +21,7 @@ import { selectRegionProfileCommand, } from '../commands/basicCommands' import { CodeWhispererCommandDeclarations } from '../commands/gettingStartedPageCommands' -import { CodeScansState, codeScanState, RegionProfile } from '../models/model' +import { CodeScansState, RegionProfile } from '../models/model' import { getNewCustomizationsAvailable, getSelectedCustomization } from '../util/customizationUtil' import { cwQuickPickSource } from '../commands/types' import { AuthUtil } from '../util/authUtil' @@ -70,25 +70,6 @@ export function createOpenReferenceLog(): DataQuickPickItem<'openReferenceLog'> } as DataQuickPickItem<'openReferenceLog'> } -export function createSecurityScan(): DataQuickPickItem<'securityScan'> { - const label = `Full project scan is now /review!` - const icon = codeScanState.getIconForButton() - const description = 'Open in Chat Panel' - - return { - data: 'securityScan', - label: codicon`${icon} ${label}`, - description: description, - onClick: () => - vscode.commands.executeCommand( - 'aws.amazonq.security.scan-statusbar', - placeholder, - 'cwQuickPickSource', - true - ), - } as DataQuickPickItem<'securityScan'> -} - export function createReconnect(): DataQuickPickItem<'reconnect'> { const label = localize('aws.amazonq.reconnectNode.label', 'Re-authenticate to connect') const icon = addColor(getIcon('vscode-debug-disconnect'), 'notificationsErrorIcon.foreground') diff --git a/packages/core/src/codewhisperer/ui/statusBarMenu.ts b/packages/core/src/codewhisperer/ui/statusBarMenu.ts index 46f47e35a2c..345ae641a78 100644 --- a/packages/core/src/codewhisperer/ui/statusBarMenu.ts +++ b/packages/core/src/codewhisperer/ui/statusBarMenu.ts @@ -21,7 +21,6 @@ import { createAutoScans, createSignIn, switchToAmazonQNode, - createSecurityScan, createSelectRegionProfileNode, } from './codeWhispererNodes' import { hasVendedIamCredentials, hasVendedCredentialsFromMetadata } from '../../auth/auth' @@ -52,12 +51,7 @@ function getAmazonQCodeWhispererNodes() { if (hasVendedIamCredentials()) { return [createFreeTierLimitMet(), createOpenReferenceLog()] } - return [ - createFreeTierLimitMet(), - createOpenReferenceLog(), - createSeparator('Other Features'), - createSecurityScan(), - ] + return [createFreeTierLimitMet(), createOpenReferenceLog(), createSeparator('Other Features')] } if (hasVendedIamCredentials()) { @@ -74,7 +68,6 @@ function getAmazonQCodeWhispererNodes() { // Security scans createSeparator('Code Reviews'), ...(AuthUtil.instance.isBuilderIdInUse() ? [] : [createAutoScans(autoScansEnabled)]), - createSecurityScan(), // Amazon Q + others createSeparator('Other Features'), diff --git a/packages/core/src/codewhisperer/util/closingBracketUtil.ts b/packages/core/src/codewhisperer/util/closingBracketUtil.ts new file mode 100644 index 00000000000..4892c5694b4 --- /dev/null +++ b/packages/core/src/codewhisperer/util/closingBracketUtil.ts @@ -0,0 +1,263 @@ +/*! + * 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' +import * as CodeWhispererConstants from '../models/constants' + +interface bracketMapType { + [k: string]: string +} + +const quotes = ["'", '"', '`'] +const parenthesis = ['(', '[', '{', ')', ']', '}', '<', '>'] + +const closeToOpen: bracketMapType = { + ')': '(', + ']': '[', + '}': '{', + '>': '<', +} + +const openToClose: bracketMapType = { + '(': ')', + '[': ']', + '{': '}', + '<': '>', +} + +/** + * LeftContext | Recommendation | RightContext + * This function aims to resolve symbols which are redundant and need to be removed + * The high level logic is as followed + * 1. Pair non-paired closing symbols(parenthesis, brackets, quotes) existing in the "recommendation" with non-paired symbols existing in the "leftContext" + * 2. Remove non-paired closing symbols existing in the "rightContext" + * @param endPosition: end position of the effective recommendation written by CodeWhisperer + * @param startPosition: start position of the effective recommendation by CodeWhisperer + * + * for example given file context ('|' is where we trigger the service): + * anArray.pu| + * recommendation returned: "sh(element);" + * typeahead: "sh(" + * the effective recommendation written by CodeWhisperer: "element);" + */ +export async function handleExtraBrackets( + editor: vscode.TextEditor, + endPosition: vscode.Position, + startPosition: vscode.Position +) { + const recommendation = editor.document.getText(new vscode.Range(startPosition, endPosition)) + const endOffset = editor.document.offsetAt(endPosition) + const startOffset = editor.document.offsetAt(startPosition) + const leftContext = editor.document.getText( + new vscode.Range( + startPosition, + editor.document.positionAt(Math.max(startOffset - CodeWhispererConstants.charactersLimit, 0)) + ) + ) + + const rightContext = editor.document.getText( + new vscode.Range( + editor.document.positionAt(endOffset), + editor.document.positionAt(endOffset + CodeWhispererConstants.charactersLimit) + ) + ) + const bracketsToRemove = getBracketsToRemove( + editor, + recommendation, + leftContext, + rightContext, + endPosition, + startPosition + ) + + const quotesToRemove = getQuotesToRemove( + editor, + recommendation, + leftContext, + rightContext, + endPosition, + startPosition + ) + + const symbolsToRemove = [...bracketsToRemove, ...quotesToRemove] + + if (symbolsToRemove.length) { + await removeBracketsFromRightContext(editor, symbolsToRemove, endPosition) + } +} + +const removeBracketsFromRightContext = async ( + editor: vscode.TextEditor, + idxToRemove: number[], + endPosition: vscode.Position +) => { + const offset = editor.document.offsetAt(endPosition) + + await editor.edit( + (editBuilder) => { + for (const idx of idxToRemove) { + const range = new vscode.Range( + editor.document.positionAt(offset + idx), + editor.document.positionAt(offset + idx + 1) + ) + editBuilder.delete(range) + } + }, + { undoStopAfter: false, undoStopBefore: false } + ) +} + +function getBracketsToRemove( + editor: vscode.TextEditor, + recommendation: string, + leftContext: string, + rightContext: string, + end: vscode.Position, + start: vscode.Position +) { + const unpairedClosingsInReco = nonClosedClosingParen(recommendation) + const unpairedOpeningsInLeftContext = nonClosedOpneingParen(leftContext, unpairedClosingsInReco.length) + const unpairedClosingsInRightContext = nonClosedClosingParen(rightContext) + + const toRemove: number[] = [] + + let i = 0 + let j = 0 + let k = 0 + while (i < unpairedOpeningsInLeftContext.length && j < unpairedClosingsInReco.length) { + const opening = unpairedOpeningsInLeftContext[i] + const closing = unpairedClosingsInReco[j] + + const isPaired = closeToOpen[closing.char] === opening.char + const rightContextCharToDelete = unpairedClosingsInRightContext[k] + + if (isPaired) { + if (rightContextCharToDelete && rightContextCharToDelete.char === closing.char) { + const rightContextStart = editor.document.offsetAt(end) + 1 + const symbolPosition = editor.document.positionAt( + rightContextStart + rightContextCharToDelete.strOffset + ) + const lineCnt = recommendation.split('\n').length - 1 + const isSameline = symbolPosition.line - lineCnt === start.line + + if (isSameline) { + toRemove.push(rightContextCharToDelete.strOffset) + } + + k++ + } + } + + i++ + j++ + } + + return toRemove +} + +function getQuotesToRemove( + editor: vscode.TextEditor, + recommendation: string, + leftContext: string, + rightContext: string, + endPosition: vscode.Position, + startPosition: vscode.Position +) { + let leftQuote: string | undefined = undefined + let leftIndex: number | undefined = undefined + for (let i = leftContext.length - 1; i >= 0; i--) { + const char = leftContext[i] + if (quotes.includes(char)) { + leftQuote = char + leftIndex = leftContext.length - i + break + } + } + + let rightQuote: string | undefined = undefined + let rightIndex: number | undefined = undefined + for (let i = 0; i < rightContext.length; i++) { + const char = rightContext[i] + if (quotes.includes(char)) { + rightQuote = char + rightIndex = i + break + } + } + + let quoteCountInReco = 0 + if (leftQuote && rightQuote && leftQuote === rightQuote) { + for (const char of recommendation) { + if (quotes.includes(char) && char === leftQuote) { + quoteCountInReco++ + } + } + } + + if (leftIndex !== undefined && rightIndex !== undefined && quoteCountInReco % 2 !== 0) { + const p = editor.document.positionAt(editor.document.offsetAt(endPosition) + rightIndex) + + if (endPosition.line === startPosition.line && endPosition.line === p.line) { + return [rightIndex] + } + } + + return [] +} + +function nonClosedOpneingParen(str: string, cnt?: number): { char: string; strOffset: number }[] { + const resultSet: { char: string; strOffset: number }[] = [] + const stack: string[] = [] + + for (let i = str.length - 1; i >= 0; i--) { + const char = str[i] + if (char! in parenthesis) { + continue + } + + if (char in closeToOpen) { + stack.push(char) + if (cnt && cnt === resultSet.length) { + return resultSet + } + } else if (char in openToClose) { + if (stack.length !== 0 && stack[stack.length - 1] === openToClose[char]) { + stack.pop() + } else { + resultSet.push({ char: char, strOffset: i }) + } + } + } + + return resultSet +} + +function nonClosedClosingParen(str: string, cnt?: number): { char: string; strOffset: number }[] { + const resultSet: { char: string; strOffset: number }[] = [] + const stack: string[] = [] + + for (let i = 0; i < str.length; i++) { + const char = str[i] + if (char! in parenthesis) { + continue + } + + if (char in openToClose) { + stack.push(char) + if (cnt && cnt === resultSet.length) { + return resultSet + } + } else if (char in closeToOpen) { + if (stack.length !== 0 && stack[stack.length - 1] === closeToOpen[char]) { + stack.pop() + } else { + resultSet.push({ char: char, strOffset: i }) + } + } + } + + return resultSet +} diff --git a/packages/core/src/codewhisperer/util/codeWhispererSession.ts b/packages/core/src/codewhisperer/util/codeWhispererSession.ts index 17d9c998112..4a529941004 100644 --- a/packages/core/src/codewhisperer/util/codeWhispererSession.ts +++ b/packages/core/src/codewhisperer/util/codeWhispererSession.ts @@ -53,13 +53,13 @@ class CodeWhispererSession { setFetchCredentialStart() { if (this.fetchCredentialStartTime === 0 && this.invokeSuggestionStartTime !== 0) { - this.fetchCredentialStartTime = performance.now() + this.fetchCredentialStartTime = Date.now() } } setSdkApiCallStart() { if (this.sdkApiCallStartTime === 0 && this.fetchCredentialStartTime !== 0) { - this.sdkApiCallStartTime = performance.now() + this.sdkApiCallStartTime = Date.now() } } diff --git a/packages/core/src/codewhisperer/util/commonUtil.ts b/packages/core/src/codewhisperer/util/commonUtil.ts index 729d3b7ed12..d2df78f1369 100644 --- a/packages/core/src/codewhisperer/util/commonUtil.ts +++ b/packages/core/src/codewhisperer/util/commonUtil.ts @@ -3,18 +3,80 @@ * SPDX-License-Identifier: Apache-2.0 */ +import * as vscode from 'vscode' +import * as semver from 'semver' import { distance } from 'fastest-levenshtein' import { getInlineSuggestEnabled } from '../../shared/utilities/editorUtilities' +import { + AWSTemplateCaseInsensitiveKeyWords, + AWSTemplateKeyWords, + JsonConfigFileNamingConvention, +} from '../models/constants' export function getLocalDatetime() { const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone return new Date().toLocaleString([], { timeZone: timezone }) } +export function asyncCallWithTimeout(asyncPromise: Promise, message: string, timeLimit: number): Promise { + let timeoutHandle: NodeJS.Timeout + const timeoutPromise = new Promise((_resolve, reject) => { + timeoutHandle = setTimeout(() => reject(new Error(message)), timeLimit) + }) + return Promise.race([asyncPromise, timeoutPromise]).then((result) => { + clearTimeout(timeoutHandle) + return result as T + }) +} + export function isInlineCompletionEnabled() { return getInlineSuggestEnabled() } +// This is the VS Code version that started to have regressions in inline completion API +export function isVscHavingRegressionInlineCompletionApi() { + return semver.gte(vscode.version, '1.78.0') && getInlineSuggestEnabled() +} + +export function getFileExt(languageId: string) { + switch (languageId) { + case 'java': + return '.java' + case 'python': + return '.py' + default: + break + } + return undefined +} + +/** + * Returns the longest overlap between the Suffix of firstString and Prefix of second string + * getPrefixSuffixOverlap("adwg31", "31ggrs") = "31" + */ +export function getPrefixSuffixOverlap(firstString: string, secondString: string) { + let i = Math.min(firstString.length, secondString.length) + while (i > 0) { + if (secondString.slice(0, i) === firstString.slice(-i)) { + break + } + i-- + } + return secondString.slice(0, i) +} + +export function checkLeftContextKeywordsForJson(fileName: string, leftFileContent: string, language: string): boolean { + if ( + language === 'json' && + !AWSTemplateKeyWords.some((substring) => leftFileContent.includes(substring)) && + !AWSTemplateCaseInsensitiveKeyWords.some((substring) => leftFileContent.toLowerCase().includes(substring)) && + !JsonConfigFileNamingConvention.has(fileName.toLowerCase()) + ) { + return true + } + return false +} + // With edit distance, complicate usermodification can be considered as simple edit(add, delete, replace), // and thus the unmodified part of recommendation length can be deducted/approximated // ex. (modified > original): originalRecom: foo -> modifiedRecom: fobarbarbaro, distance = 9, delta = 12 - 9 = 3 diff --git a/packages/core/src/codewhisperer/util/editorContext.ts b/packages/core/src/codewhisperer/util/editorContext.ts new file mode 100644 index 00000000000..dacf3b326a1 --- /dev/null +++ b/packages/core/src/codewhisperer/util/editorContext.ts @@ -0,0 +1,427 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as codewhispererClient from '../client/codewhisperer' +import * as path from 'path' +import * as CodeWhispererConstants from '../models/constants' +import { getTabSizeSetting } from '../../shared/utilities/editorUtilities' +import { truncate } from '../../shared/utilities/textUtilities' +import { getLogger } from '../../shared/logger/logger' +import { runtimeLanguageContext } from './runtimeLanguageContext' +import { fetchSupplementalContext } from './supplementalContext/supplementalContextUtil' +import { editorStateMaxLength, supplementalContextTimeoutInMs } from '../models/constants' +import { getSelectedCustomization } from './customizationUtil' +import { selectFrom } from '../../shared/utilities/tsUtils' +import { checkLeftContextKeywordsForJson } from './commonUtil' +import { CodeWhispererSupplementalContext } from '../models/model' +import { getOptOutPreference } from '../../shared/telemetry/util' +import { indent } from '../../shared/utilities/textUtilities' +import { isInDirectory } from '../../shared/filesystemUtilities' +import { AuthUtil } from './authUtil' +import { predictionTracker } from '../nextEditPrediction/activation' +import { LanguageClient } from 'vscode-languageclient' + +let tabSize: number = getTabSizeSetting() + +function getEnclosingNotebook(editor: vscode.TextEditor): vscode.NotebookDocument | undefined { + // For notebook cells, find the existing notebook with a cell that matches the current editor. + return vscode.workspace.notebookDocuments.find( + (nb) => + nb.notebookType === 'jupyter-notebook' && nb.getCells().some((cell) => cell.document === editor.document) + ) +} + +export function getNotebookContext( + notebook: vscode.NotebookDocument, + editor: vscode.TextEditor, + languageName: string, + caretLeftFileContext: string, + caretRightFileContext: string +) { + // Expand the context for a cell inside of a noteboo with whatever text fits from the preceding and subsequent cells + const allCells = notebook.getCells() + const cellIndex = allCells.findIndex((cell) => cell.document === editor.document) + // Extract text from prior cells if there is enough room in left file context + if (caretLeftFileContext.length < CodeWhispererConstants.charactersLimit - 1) { + const leftCellsText = getNotebookCellsSliceContext( + allCells.slice(0, cellIndex), + CodeWhispererConstants.charactersLimit - (caretLeftFileContext.length + 1), + languageName, + true + ) + if (leftCellsText.length > 0) { + caretLeftFileContext = addNewlineIfMissing(leftCellsText) + caretLeftFileContext + } + } + // Extract text from subsequent cells if there is enough room in right file context + if (caretRightFileContext.length < CodeWhispererConstants.charactersLimit - 1) { + const rightCellsText = getNotebookCellsSliceContext( + allCells.slice(cellIndex + 1), + CodeWhispererConstants.charactersLimit - (caretRightFileContext.length + 1), + languageName, + false + ) + if (rightCellsText.length > 0) { + caretRightFileContext = addNewlineIfMissing(caretRightFileContext) + rightCellsText + } + } + return { caretLeftFileContext, caretRightFileContext } +} + +export function getNotebookCellContext(cell: vscode.NotebookCell, referenceLanguage?: string): string { + // Extract the text verbatim if the cell is code and the cell has the same language. + // Otherwise, add the correct comment string for the reference language + const cellText = cell.document.getText() + if ( + cell.kind === vscode.NotebookCellKind.Markup || + (runtimeLanguageContext.normalizeLanguage(cell.document.languageId) ?? cell.document.languageId) !== + referenceLanguage + ) { + const commentPrefix = runtimeLanguageContext.getSingleLineCommentPrefix(referenceLanguage) + if (commentPrefix === '') { + return cellText + } + return cell.document + .getText() + .split('\n') + .map((line) => `${commentPrefix}${line}`) + .join('\n') + } + return cellText +} + +export function getNotebookCellsSliceContext( + cells: vscode.NotebookCell[], + maxLength: number, + referenceLanguage: string, + fromStart: boolean +): string { + // Extract context from array of notebook cells that fits inside `maxLength` characters, + // from either the start or the end of the array. + let output: string[] = [] + if (!fromStart) { + cells = cells.reverse() + } + cells.some((cell) => { + const cellText = addNewlineIfMissing(getNotebookCellContext(cell, referenceLanguage)) + if (cellText.length > 0) { + if (cellText.length >= maxLength) { + if (fromStart) { + output.push(cellText.substring(0, maxLength)) + } else { + output.push(cellText.substring(cellText.length - maxLength)) + } + return true + } + output.push(cellText) + maxLength -= cellText.length + } + }) + if (!fromStart) { + output = output.reverse() + } + return output.join('') +} + +export function addNewlineIfMissing(text: string): string { + if (text.length > 0 && !text.endsWith('\n')) { + text += '\n' + } + return text +} + +export function extractContextForCodeWhisperer(editor: vscode.TextEditor): codewhispererClient.FileContext { + const document = editor.document + const curPos = editor.selection.active + const offset = document.offsetAt(curPos) + + let caretLeftFileContext = editor.document.getText( + new vscode.Range( + document.positionAt(offset - CodeWhispererConstants.charactersLimit), + document.positionAt(offset) + ) + ) + let caretRightFileContext = editor.document.getText( + new vscode.Range( + document.positionAt(offset), + document.positionAt(offset + CodeWhispererConstants.charactersLimit) + ) + ) + let languageName = 'plaintext' + if (!checkLeftContextKeywordsForJson(document.fileName, caretLeftFileContext, editor.document.languageId)) { + languageName = runtimeLanguageContext.resolveLang(editor.document) + } + if (editor.document.uri.scheme === 'vscode-notebook-cell') { + const notebook = getEnclosingNotebook(editor) + if (notebook) { + ;({ caretLeftFileContext, caretRightFileContext } = getNotebookContext( + notebook, + editor, + languageName, + caretLeftFileContext, + caretRightFileContext + )) + } + } + + return { + fileUri: editor.document.uri.toString().substring(0, CodeWhispererConstants.filenameCharsLimit), + filename: getFileRelativePath(editor), + programmingLanguage: { + languageName: languageName, + }, + leftFileContent: caretLeftFileContext, + rightFileContent: caretRightFileContext, + } as codewhispererClient.FileContext +} + +export function getFileName(editor: vscode.TextEditor): string { + const fileName = path.basename(editor.document.fileName) + return fileName.substring(0, CodeWhispererConstants.filenameCharsLimit) +} + +export function getFileRelativePath(editor: vscode.TextEditor): string { + const fileName = path.basename(editor.document.fileName) + let relativePath = '' + const workspaceFolder = vscode.workspace.getWorkspaceFolder(editor.document.uri) + if (!workspaceFolder) { + relativePath = fileName + } else { + const workspacePath = workspaceFolder.uri.fsPath + const filePath = editor.document.uri.fsPath + relativePath = path.relative(workspacePath, filePath) + } + // For notebook files, we want to use the programming language for each cell for the code suggestions, so change + // the filename sent in the request to reflect that language + if (relativePath.endsWith('.ipynb')) { + const fileExtension = runtimeLanguageContext.getLanguageExtensionForNotebook(editor.document.languageId) + if (fileExtension !== undefined) { + const filenameWithNewExtension = relativePath.substring(0, relativePath.length - 5) + fileExtension + return filenameWithNewExtension.substring(0, CodeWhispererConstants.filenameCharsLimit) + } + } + return relativePath.substring(0, CodeWhispererConstants.filenameCharsLimit) +} + +async function getWorkspaceId(editor: vscode.TextEditor): Promise { + try { + const workspaceIds: { workspaces: { workspaceRoot: string; workspaceId: string }[] } = + await vscode.commands.executeCommand('aws.amazonq.getWorkspaceId') + for (const item of workspaceIds.workspaces) { + const path = vscode.Uri.parse(item.workspaceRoot).fsPath + if (isInDirectory(path, editor.document.uri.fsPath)) { + return item.workspaceId + } + } + } catch (err) { + getLogger().warn(`No workspace id found ${err}`) + } + return undefined +} + +export async function buildListRecommendationRequest( + editor: vscode.TextEditor, + nextToken: string, + allowCodeWithReference: boolean, + languageClient?: LanguageClient +): Promise<{ + request: codewhispererClient.ListRecommendationsRequest + supplementalMetadata: CodeWhispererSupplementalContext | undefined +}> { + const fileContext = extractContextForCodeWhisperer(editor) + + const tokenSource = new vscode.CancellationTokenSource() + setTimeout(() => { + tokenSource.cancel() + }, supplementalContextTimeoutInMs) + + const supplementalContexts = await fetchSupplementalContext(editor, tokenSource.token, languageClient) + + logSupplementalContext(supplementalContexts) + + // Get predictionSupplementalContext from PredictionTracker + let predictionSupplementalContext: codewhispererClient.SupplementalContext[] = [] + if (predictionTracker) { + predictionSupplementalContext = await predictionTracker.generatePredictionSupplementalContext() + } + + const selectedCustomization = getSelectedCustomization() + const completionSupplementalContext: codewhispererClient.SupplementalContext[] = supplementalContexts + ? supplementalContexts.supplementalContextItems.map((v) => { + return selectFrom(v, 'content', 'filePath') + }) + : [] + + const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile + + const editorState = getEditorState(editor, fileContext) + + // Combine inline and prediction supplemental contexts + const finalSupplementalContext = completionSupplementalContext.concat(predictionSupplementalContext) + return { + request: { + fileContext: fileContext, + nextToken: nextToken, + referenceTrackerConfiguration: { + recommendationsWithReferences: allowCodeWithReference ? 'ALLOW' : 'BLOCK', + }, + supplementalContexts: finalSupplementalContext, + editorState: editorState, + maxResults: CodeWhispererConstants.maxRecommendations, + customizationArn: selectedCustomization.arn === '' ? undefined : selectedCustomization.arn, + optOutPreference: getOptOutPreference(), + workspaceId: await getWorkspaceId(editor), + profileArn: profile?.arn, + }, + supplementalMetadata: supplementalContexts, + } +} + +export async function buildGenerateRecommendationRequest(editor: vscode.TextEditor): Promise<{ + request: codewhispererClient.GenerateRecommendationsRequest + supplementalMetadata: CodeWhispererSupplementalContext | undefined +}> { + const fileContext = extractContextForCodeWhisperer(editor) + + const tokenSource = new vscode.CancellationTokenSource() + // the supplement context fetch mechanisms each has a timeout of supplementalContextTimeoutInMs + // adding 10 ms for overall timeout as buffer + setTimeout(() => { + tokenSource.cancel() + }, supplementalContextTimeoutInMs + 10) + const supplementalContexts = await fetchSupplementalContext(editor, tokenSource.token) + + logSupplementalContext(supplementalContexts) + + return { + request: { + fileContext: fileContext, + maxResults: CodeWhispererConstants.maxRecommendations, + supplementalContexts: supplementalContexts?.supplementalContextItems ?? [], + }, + supplementalMetadata: supplementalContexts, + } +} + +export function validateRequest( + req: codewhispererClient.ListRecommendationsRequest | codewhispererClient.GenerateRecommendationsRequest +): boolean { + const isLanguageNameValid = + req.fileContext.programmingLanguage.languageName !== undefined && + req.fileContext.programmingLanguage.languageName.length >= 1 && + req.fileContext.programmingLanguage.languageName.length <= 128 && + (runtimeLanguageContext.isLanguageSupported(req.fileContext.programmingLanguage.languageName) || + runtimeLanguageContext.isFileFormatSupported( + req.fileContext.filename.substring(req.fileContext.filename.lastIndexOf('.') + 1) + )) + const isFileNameValid = !(req.fileContext.filename === undefined || req.fileContext.filename.length < 1) + const isFileContextValid = !( + req.fileContext.leftFileContent.length > CodeWhispererConstants.charactersLimit || + req.fileContext.rightFileContent.length > CodeWhispererConstants.charactersLimit + ) + if (isFileNameValid && isLanguageNameValid && isFileContextValid) { + return true + } + return false +} + +export function updateTabSize(val: number): void { + tabSize = val +} + +export function getTabSize(): number { + return tabSize +} + +export function getEditorState(editor: vscode.TextEditor, fileContext: codewhispererClient.FileContext): any { + try { + const cursorPosition = editor.selection.active + const cursorOffset = editor.document.offsetAt(cursorPosition) + const documentText = editor.document.getText() + + // Truncate if document content is too large (defined in constants.ts) + let fileText = documentText + if (documentText.length > editorStateMaxLength) { + const halfLength = Math.floor(editorStateMaxLength / 2) + + // Use truncate function to get the text around the cursor position + const leftPart = truncate(documentText.substring(0, cursorOffset), -halfLength, '') + const rightPart = truncate(documentText.substring(cursorOffset), halfLength, '') + + fileText = leftPart + rightPart + } + + return { + document: { + programmingLanguage: { + languageName: fileContext.programmingLanguage.languageName, + }, + relativeFilePath: fileContext.filename, + text: fileText, + }, + cursorState: { + position: { + line: editor.selection.active.line, + character: editor.selection.active.character, + }, + }, + } + } catch (error) { + getLogger().error(`Error generating editor state: ${error}`) + return undefined + } +} + +export function getLeftContext(editor: vscode.TextEditor, line: number): string { + let lineText = '' + try { + if (editor && editor.document.lineAt(line)) { + lineText = editor.document.lineAt(line).text + if (lineText.length > CodeWhispererConstants.contextPreviewLen) { + lineText = + '...' + + lineText.substring( + lineText.length - CodeWhispererConstants.contextPreviewLen - 1, + lineText.length - 1 + ) + } + } + } catch (error) { + getLogger().error(`Error when getting left context ${error}`) + } + + return lineText +} + +function logSupplementalContext(supplementalContext: CodeWhispererSupplementalContext | undefined) { + if (!supplementalContext) { + return + } + + let logString = indent( + `CodeWhispererSupplementalContext: + isUtg: ${supplementalContext.isUtg}, + isProcessTimeout: ${supplementalContext.isProcessTimeout}, + contentsLength: ${supplementalContext.contentsLength}, + latency: ${supplementalContext.latency} + strategy: ${supplementalContext.strategy}`, + 4, + true + ).trimStart() + + for (const [index, context] of supplementalContext.supplementalContextItems.entries()) { + logString += indent(`\nChunk ${index}:\n`, 4, true) + logString += indent( + `Path: ${context.filePath} + Length: ${context.content.length} + Score: ${context.score}`, + 8, + true + ) + } + + getLogger().debug(logString) +} diff --git a/packages/core/src/codewhisperer/util/globalStateUtil.ts b/packages/core/src/codewhisperer/util/globalStateUtil.ts new file mode 100644 index 00000000000..55376a83546 --- /dev/null +++ b/packages/core/src/codewhisperer/util/globalStateUtil.ts @@ -0,0 +1,23 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { vsCodeState } from '../models/model' + +export function resetIntelliSenseState( + isManualTriggerEnabled: boolean, + isAutomatedTriggerEnabled: boolean, + hasResponse: boolean +) { + /** + * Skip when CodeWhisperer service is turned off + */ + if (!isManualTriggerEnabled && !isAutomatedTriggerEnabled) { + return + } + + if (vsCodeState.isIntelliSenseActive && hasResponse) { + vsCodeState.isIntelliSenseActive = false + } +} diff --git a/packages/core/src/codewhisperer/util/supplementalContext/codeParsingUtil.ts b/packages/core/src/codewhisperer/util/supplementalContext/codeParsingUtil.ts new file mode 100644 index 00000000000..c73a2eebaa4 --- /dev/null +++ b/packages/core/src/codewhisperer/util/supplementalContext/codeParsingUtil.ts @@ -0,0 +1,130 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import path = require('path') +import { normalize } from '../../../shared/utilities/pathUtils' + +// TODO: functionExtractionPattern, classExtractionPattern, imposrtStatementRegex are not scalable and we will deprecate and remove the usage in the near future +export interface utgLanguageConfig { + extension: string + testFilenamePattern: RegExp[] + functionExtractionPattern?: RegExp + classExtractionPattern?: RegExp + importStatementRegExp?: RegExp +} + +export const utgLanguageConfigs: Record = { + // Java regexes are not working efficiently for class or function extraction + java: { + extension: '.java', + testFilenamePattern: [/^(.+)Test(\.java)$/, /(.+)Tests(\.java)$/, /Test(.+)(\.java)$/], + functionExtractionPattern: + /(?:(?:public|private|protected)\s+)(?:static\s+)?(?:[\w<>]+\s+)?(\w+)\s*\([^)]*\)\s*(?:(?:throws\s+\w+)?\s*)[{;]/gm, // TODO: Doesn't work for generice T functions. + classExtractionPattern: /(?<=^|\n)\s*public\s+class\s+(\w+)/gm, // TODO: Verify these. + importStatementRegExp: /import .*\.([a-zA-Z0-9]+);/, + }, + python: { + extension: '.py', + testFilenamePattern: [/^test_(.+)(\.py)$/, /^(.+)_test(\.py)$/], + functionExtractionPattern: /def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g, // Worked fine + classExtractionPattern: /^class\s+(\w+)\s*:/gm, + importStatementRegExp: /from (.*) import.*/, + }, + typescript: { + extension: '.ts', + testFilenamePattern: [/^(.+)\.test(\.ts|\.tsx)$/, /^(.+)\.spec(\.ts|\.tsx)$/], + }, + javascript: { + extension: '.js', + testFilenamePattern: [/^(.+)\.test(\.js|\.jsx)$/, /^(.+)\.spec(\.js|\.jsx)$/], + }, + typescriptreact: { + extension: '.tsx', + testFilenamePattern: [/^(.+)\.test(\.ts|\.tsx)$/, /^(.+)\.spec(\.ts|\.tsx)$/], + }, + javascriptreact: { + extension: '.jsx', + testFilenamePattern: [/^(.+)\.test(\.js|\.jsx)$/, /^(.+)\.spec(\.js|\.jsx)$/], + }, +} + +export function extractFunctions(fileContent: string, regex?: RegExp) { + if (!regex) { + return [] + } + const functionNames: string[] = [] + let match: RegExpExecArray | null + + while ((match = regex.exec(fileContent)) !== null) { + functionNames.push(match[1]) + } + return functionNames +} + +export function extractClasses(fileContent: string, regex?: RegExp) { + if (!regex) { + return [] + } + const classNames: string[] = [] + let match: RegExpExecArray | null + + while ((match = regex.exec(fileContent)) !== null) { + classNames.push(match[1]) + } + return classNames +} + +export function countSubstringMatches(arr1: string[], arr2: string[]): number { + let count = 0 + for (const str1 of arr1) { + for (const str2 of arr2) { + if (str2.toLowerCase().includes(str1.toLowerCase())) { + count++ + } + } + } + return count +} + +export async function isTestFile( + filePath: string, + languageConfig: { + languageId: vscode.TextDocument['languageId'] + fileContent?: string + } +): Promise { + const normalizedFilePath = normalize(filePath) + const pathContainsTest = + normalizedFilePath.includes('tests/') || + normalizedFilePath.includes('test/') || + normalizedFilePath.includes('tst/') + const fileNameMatchTestPatterns = isTestFileByName(normalizedFilePath, languageConfig.languageId) + + if (pathContainsTest || fileNameMatchTestPatterns) { + return true + } + + return false +} + +function isTestFileByName(filePath: string, language: vscode.TextDocument['languageId']): boolean { + const languageConfig = utgLanguageConfigs[language] + if (!languageConfig) { + // We have enabled the support only for python and Java for this check + // as we depend on Regex for this validation. + return false + } + const testFilenamePattern = languageConfig.testFilenamePattern + + const filename = path.basename(filePath) + for (const pattern of testFilenamePattern) { + if (pattern.test(filename)) { + return true + } + } + + return false +} diff --git a/packages/core/src/codewhisperer/util/supplementalContext/crossFileContextUtil.ts b/packages/core/src/codewhisperer/util/supplementalContext/crossFileContextUtil.ts new file mode 100644 index 00000000000..17dc594cde9 --- /dev/null +++ b/packages/core/src/codewhisperer/util/supplementalContext/crossFileContextUtil.ts @@ -0,0 +1,407 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import path = require('path') +import { BM25Document, BM25Okapi } from './rankBm25' +import { + crossFileContextConfig, + supplementalContextTimeoutInMs, + supplementalContextMaxTotalLength, +} from '../../models/constants' +import { isTestFile } from './codeParsingUtil' +import { getFileDistance } from '../../../shared/filesystemUtilities' +import { getOpenFilesInWindow } from '../../../shared/utilities/editorUtilities' +import { getLogger } from '../../../shared/logger/logger' +import { + CodeWhispererSupplementalContext, + CodeWhispererSupplementalContextItem, + SupplementalContextStrategy, +} from '../../models/model' +import { waitUntil } from '../../../shared/utilities/timeoutUtils' +import { FeatureConfigProvider } from '../../../shared/featureConfig' +import fs from '../../../shared/fs/fs' +import { LanguageClient } from 'vscode-languageclient' + +import { + GetSupplementalContextParams, + getSupplementalContextRequestType, + SupplementalContextItem, +} from '@aws/language-server-runtimes/protocol' +type CrossFileSupportedLanguage = + | 'java' + | 'python' + | 'javascript' + | 'typescript' + | 'javascriptreact' + | 'typescriptreact' + +// TODO: ugly, can we make it prettier? like we have to manually type 'java', 'javascriptreact' which is error prone +// TODO: Move to another config file or constants file +// Supported language to its corresponding file ext +const supportedLanguageToDialects: Readonly>> = { + java: new Set(['.java']), + python: new Set(['.py']), + javascript: new Set(['.js', '.jsx']), + javascriptreact: new Set(['.js', '.jsx']), + typescript: new Set(['.ts', '.tsx']), + typescriptreact: new Set(['.ts', '.tsx']), +} + +function isCrossFileSupported(languageId: string): languageId is CrossFileSupportedLanguage { + return Object.keys(supportedLanguageToDialects).includes(languageId) +} + +interface Chunk { + fileName: string + content: string + nextContent: string + score?: number +} + +/** + * `none`: supplementalContext is not supported + * `opentabs`: opentabs_BM25 + * `codemap`: repomap + opentabs BM25 + * `bm25`: global_BM25 + * `default`: repomap + global_BM25 + */ +type SupplementalContextConfig = 'none' | 'opentabs' | 'codemap' | 'bm25' | 'default' + +export async function fetchSupplementalContextForSrc( + editor: vscode.TextEditor, + cancellationToken: vscode.CancellationToken, + languageClient?: LanguageClient +): Promise | undefined> { + const supplementalContextConfig = getSupplementalContextConfig(editor.document.languageId) + + // not supported case + if (supplementalContextConfig === 'none') { + return undefined + } + + // fallback to opentabs if projectContext timeout + const opentabsContextPromise = waitUntil( + async function () { + return await fetchOpentabsContext(editor, cancellationToken) + }, + { timeout: supplementalContextTimeoutInMs, interval: 5, truthy: false } + ) + + // opentabs context will use bm25 and users' open tabs to fetch supplemental context + if (supplementalContextConfig === 'opentabs') { + const supContext = (await opentabsContextPromise) ?? [] + return { + supplementalContextItems: supContext, + strategy: supContext.length === 0 ? 'empty' : 'opentabs', + } + } + + // codemap will use opentabs context plus repomap if it's present + if (supplementalContextConfig === 'codemap') { + let strategy: SupplementalContextStrategy = 'empty' + let hasCodemap: boolean = false + let hasOpentabs: boolean = false + const opentabsContextAndCodemap = await waitUntil( + async function () { + const result: CodeWhispererSupplementalContextItem[] = [] + const opentabsContext = await fetchOpentabsContext(editor, cancellationToken) + const codemap = await fetchProjectContext(editor, 'codemap', languageClient) + + function addToResult(items: CodeWhispererSupplementalContextItem[]) { + for (const item of items) { + const curLen = result.reduce((acc, i) => acc + i.content.length, 0) + if (curLen + item.content.length < supplementalContextMaxTotalLength) { + result.push(item) + } + } + } + + if (codemap && codemap.length > 0) { + addToResult(codemap) + hasCodemap = true + } + + if (opentabsContext && opentabsContext.length > 0) { + addToResult(opentabsContext) + hasOpentabs = true + } + + return result + }, + { timeout: supplementalContextTimeoutInMs, interval: 5, truthy: false } + ) + + if (hasCodemap) { + strategy = 'codemap' + } else if (hasOpentabs) { + strategy = 'opentabs' + } else { + strategy = 'empty' + } + + return { + supplementalContextItems: opentabsContextAndCodemap ?? [], + strategy: strategy, + } + } + + // global bm25 without repomap + if (supplementalContextConfig === 'bm25') { + const projectBM25Promise = waitUntil( + async function () { + return await fetchProjectContext(editor, 'bm25', languageClient) + }, + { timeout: supplementalContextTimeoutInMs, interval: 5, truthy: false } + ) + + const [projectContext, opentabsContext] = await Promise.all([projectBM25Promise, opentabsContextPromise]) + if (projectContext && projectContext.length > 0) { + return { + supplementalContextItems: projectContext, + strategy: 'bm25', + } + } + + const supContext = opentabsContext ?? [] + return { + supplementalContextItems: supContext, + strategy: supContext.length === 0 ? 'empty' : 'opentabs', + } + } + + // global bm25 with repomap + const projectContextAndCodemapPromise = waitUntil( + async function () { + return await fetchProjectContext(editor, 'default', languageClient) + }, + { timeout: supplementalContextTimeoutInMs, interval: 5, truthy: false } + ) + + const [projectContext, opentabsContext] = await Promise.all([ + projectContextAndCodemapPromise, + opentabsContextPromise, + ]) + if (projectContext && projectContext.length > 0) { + return { + supplementalContextItems: projectContext, + strategy: 'default', + } + } + + return { + supplementalContextItems: opentabsContext ?? [], + strategy: 'opentabs', + } +} + +export async function fetchProjectContext( + editor: vscode.TextEditor, + target: 'default' | 'codemap' | 'bm25', + languageclient?: LanguageClient +): Promise { + try { + if (languageclient) { + const request: GetSupplementalContextParams = { + filePath: editor.document.uri.fsPath, + } + const response = await languageclient.sendRequest( + getSupplementalContextRequestType.method, + request + ) + return response as CodeWhispererSupplementalContextItem[] + } + } catch (error) { + return [] + } + return [] +} + +export async function fetchOpentabsContext( + editor: vscode.TextEditor, + cancellationToken: vscode.CancellationToken +): Promise { + const codeChunksCalculated = crossFileContextConfig.numberOfChunkToFetch + + // Step 1: Get relevant cross files to refer + const relevantCrossFilePaths = await getCrossFileCandidates(editor) + + // Step 2: Split files to chunks with upper bound on chunkCount + // We restrict the total number of chunks to improve on latency. + // Chunk linking is required as we want to pass the next chunk value for matched chunk. + let chunkList: Chunk[] = [] + for (const relevantFile of relevantCrossFilePaths) { + const chunks: Chunk[] = await splitFileToChunks(relevantFile, crossFileContextConfig.numberOfLinesEachChunk) + const linkedChunks = linkChunks(chunks) + chunkList.push(...linkedChunks) + if (chunkList.length >= codeChunksCalculated) { + break + } + } + + // it's required since chunkList.push(...) is likely giving us a list of size > 60 + chunkList = chunkList.slice(0, codeChunksCalculated) + + // Step 3: Generate Input chunk (10 lines left of cursor position) + // and Find Best K chunks w.r.t input chunk using BM25 + const inputChunk: Chunk = getInputChunk(editor) + const bestChunks: Chunk[] = findBestKChunkMatches(inputChunk, chunkList, crossFileContextConfig.topK) + + // Step 4: Transform best chunks to supplemental contexts + const supplementalContexts: CodeWhispererSupplementalContextItem[] = [] + let totalLength = 0 + for (const chunk of bestChunks) { + totalLength += chunk.nextContent.length + + if (totalLength > crossFileContextConfig.maximumTotalLength) { + break + } + + supplementalContexts.push({ + filePath: chunk.fileName, + content: chunk.nextContent, + score: chunk.score, + }) + } + + // DO NOT send code chunk with empty content + getLogger().debug(`CodeWhisperer finished fetching crossfile context out of ${relevantCrossFilePaths.length} files`) + return supplementalContexts +} + +function findBestKChunkMatches(chunkInput: Chunk, chunkReferences: Chunk[], k: number): Chunk[] { + const chunkContentList = chunkReferences.map((chunk) => chunk.content) + + // performBM25Scoring returns the output in a sorted order (descending of scores) + const top3: BM25Document[] = new BM25Okapi(chunkContentList).topN(chunkInput.content, crossFileContextConfig.topK) + + return top3.map((doc) => { + // reference to the original metadata since BM25.top3 will sort the result + const chunkIndex = doc.index + const chunkReference = chunkReferences[chunkIndex] + return { + content: chunkReference.content, + fileName: chunkReference.fileName, + nextContent: chunkReference.nextContent, + score: doc.score, + } + }) +} + +/* This extract 10 lines to the left of the cursor from trigger file. + * This will be the inputquery to bm25 matching against list of cross-file chunks + */ +function getInputChunk(editor: vscode.TextEditor) { + const chunkSize = crossFileContextConfig.numberOfLinesEachChunk + const cursorPosition = editor.selection.active + const startLine = Math.max(cursorPosition.line - chunkSize, 0) + const endLine = Math.max(cursorPosition.line - 1, 0) + const inputChunkContent = editor.document.getText( + new vscode.Range(startLine, 0, endLine, editor.document.lineAt(endLine).text.length) + ) + const inputChunk: Chunk = { fileName: editor.document.fileName, content: inputChunkContent, nextContent: '' } + return inputChunk +} + +/** + * Util to decide if we need to fetch crossfile context since CodeWhisperer CrossFile Context feature is gated by userGroup and language level + * @param languageId: VSCode language Identifier + * @returns specifically returning undefined if the langueage is not supported, + * otherwise true/false depending on if the language is fully supported or not belonging to the user group + */ +function getSupplementalContextConfig(languageId: vscode.TextDocument['languageId']): SupplementalContextConfig { + if (!isCrossFileSupported(languageId)) { + return 'none' + } + + const group = FeatureConfigProvider.instance.getProjectContextGroup() + switch (group) { + default: + return 'codemap' + } +} + +/** + * This linking is required from science experimentations to pass the next contnet chunk + * when a given chunk context passes the match in BM25. + * Special handling is needed for last(its next points to its own) and first chunk + */ +export function linkChunks(chunks: Chunk[]) { + const updatedChunks: Chunk[] = [] + + // This additional chunk is needed to create a next pointer to chunk 0. + const firstChunk = chunks[0] + const firstChunkSubContent = firstChunk.content.split('\n').slice(0, 3).join('\n').trimEnd() + const newFirstChunk = { + fileName: firstChunk.fileName, + content: firstChunkSubContent, + nextContent: firstChunk.content, + } + updatedChunks.push(newFirstChunk) + + const n = chunks.length + for (let i = 0; i < n; i++) { + const chunk = chunks[i] + const nextChunk = i < n - 1 ? chunks[i + 1] : chunk + + chunk.nextContent = nextChunk.content + updatedChunks.push(chunk) + } + + return updatedChunks +} + +export async function splitFileToChunks(filePath: string, chunkSize: number): Promise { + const chunks: Chunk[] = [] + + const fileContent = (await fs.readFileText(filePath)).trimEnd() + const lines = fileContent.split('\n') + + for (let i = 0; i < lines.length; i += chunkSize) { + const chunkContent = lines.slice(i, Math.min(i + chunkSize, lines.length)).join('\n') + const chunk = { fileName: filePath, content: chunkContent.trimEnd(), nextContent: '' } + chunks.push(chunk) + } + return chunks +} + +/** + * This function will return relevant cross files sorted by file distance for the given editor file + * by referencing open files, imported files and same package files. + */ +export async function getCrossFileCandidates(editor: vscode.TextEditor): Promise { + const targetFile = editor.document.uri.fsPath + const language = editor.document.languageId as CrossFileSupportedLanguage + const dialects = supportedLanguageToDialects[language] + + /** + * Consider a file which + * 1. is different from the target + * 2. has the same file extension or it's one of the dialect of target file (e.g .js vs. .jsx) + * 3. is not a test file + */ + const unsortedCandidates = await getOpenFilesInWindow(async (candidateFile) => { + return ( + targetFile !== candidateFile && + (path.extname(targetFile) === path.extname(candidateFile) || + (dialects && dialects.has(path.extname(candidateFile)))) && + !(await isTestFile(candidateFile, { languageId: language })) + ) + }) + + return unsortedCandidates + .map((candidate) => { + return { + file: candidate, + fileDistance: getFileDistance(targetFile, candidate), + } + }) + .sort((file1, file2) => { + return file1.fileDistance - file2.fileDistance + }) + .map((fileToDistance) => { + return fileToDistance.file + }) +} diff --git a/packages/core/src/codewhisperer/util/supplementalContext/rankBm25.ts b/packages/core/src/codewhisperer/util/supplementalContext/rankBm25.ts new file mode 100644 index 00000000000..a2c77e0b10f --- /dev/null +++ b/packages/core/src/codewhisperer/util/supplementalContext/rankBm25.ts @@ -0,0 +1,137 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// Implementation inspired by https://github.com/dorianbrown/rank_bm25/blob/990470ebbe6b28c18216fd1a8b18fe7446237dd6/rank_bm25.py#L52 + +export interface BM25Document { + content: string + /** The score that the document receives. */ + score: number + + index: number +} + +export abstract class BM25 { + protected readonly corpusSize: number + protected readonly avgdl: number + protected readonly idf: Map = new Map() + protected readonly docLen: number[] = [] + protected readonly docFreqs: Map[] = [] + protected readonly nd: Map = new Map() + + constructor( + protected readonly corpus: string[], + protected readonly tokenizer: (str: string) => string[] = defaultTokenizer, + protected readonly k1: number, + protected readonly b: number, + protected readonly epsilon: number + ) { + this.corpusSize = corpus.length + + let numDoc = 0 + for (const document of corpus.map((document) => { + return tokenizer(document) + })) { + this.docLen.push(document.length) + numDoc += document.length + + const frequencies = new Map() + for (const word of document) { + frequencies.set(word, (frequencies.get(word) || 0) + 1) + } + this.docFreqs.push(frequencies) + + for (const [word, _] of frequencies.entries()) { + this.nd.set(word, (this.nd.get(word) || 0) + 1) + } + } + + this.avgdl = numDoc / this.corpusSize + + this.calIdf(this.nd) + } + + abstract calIdf(nd: Map): void + + abstract score(query: string): BM25Document[] + + topN(query: string, n: number): BM25Document[] { + const notSorted = this.score(query) + const sorted = notSorted.sort((a, b) => b.score - a.score) + return sorted.slice(0, Math.min(n, sorted.length)) + } +} + +export class BM25Okapi extends BM25 { + constructor(corpus: string[], tokenizer: (str: string) => string[] = defaultTokenizer) { + super(corpus, tokenizer, 1.5, 0.75, 0.25) + } + + calIdf(nd: Map): void { + let idfSum = 0 + + const negativeIdfs: string[] = [] + for (const [word, freq] of nd) { + const idf = Math.log(this.corpusSize - freq + 0.5) - Math.log(freq + 0.5) + this.idf.set(word, idf) + idfSum += idf + + if (idf < 0) { + negativeIdfs.push(word) + } + } + + const averageIdf = idfSum / this.idf.size + const eps = this.epsilon * averageIdf + for (const word of negativeIdfs) { + this.idf.set(word, eps) + } + } + + score(query: string): BM25Document[] { + const queryWords = defaultTokenizer(query) + return this.docFreqs.map((docFreq, index) => { + let score = 0 + for (const [_, queryWord] of queryWords.entries()) { + const queryWordFreqForDocument = docFreq.get(queryWord) || 0 + const numerator = (this.idf.get(queryWord) || 0.0) * queryWordFreqForDocument * (this.k1 + 1) + const denominator = + queryWordFreqForDocument + this.k1 * (1 - this.b + (this.b * this.docLen[index]) / this.avgdl) + + score += numerator / denominator + } + + return { + content: this.corpus[index], + score: score, + index: index, + } + }) + } +} + +// TODO: This is a very simple tokenizer, we want to replace this by more sophisticated one. +function defaultTokenizer(content: string): string[] { + const regex = /\w+/g + const words = content.split(' ') + const result = [] + for (const word of words) { + const wordList = findAll(word, regex) + result.push(...wordList) + } + + return result +} + +function findAll(str: string, re: RegExp): string[] { + let match: RegExpExecArray | null + const matches: string[] = [] + + while ((match = re.exec(str)) !== null) { + matches.push(match[0]) + } + + return matches +} diff --git a/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts b/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts new file mode 100644 index 00000000000..edda43ddcf6 --- /dev/null +++ b/packages/core/src/codewhisperer/util/supplementalContext/supplementalContextUtil.ts @@ -0,0 +1,139 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { fetchSupplementalContextForTest } from './utgUtils' +import { fetchSupplementalContextForSrc } from './crossFileContextUtil' +import { isTestFile } from './codeParsingUtil' +import * as vscode from 'vscode' +import { CancellationError } from '../../../shared/utilities/timeoutUtils' +import { ToolkitError } from '../../../shared/errors' +import { getLogger } from '../../../shared/logger/logger' +import { CodeWhispererSupplementalContext } from '../../models/model' +import * as os from 'os' +import { crossFileContextConfig } from '../../models/constants' +import { LanguageClient } from 'vscode-languageclient' + +export async function fetchSupplementalContext( + editor: vscode.TextEditor, + cancellationToken: vscode.CancellationToken, + languageClient?: LanguageClient +): Promise { + const timesBeforeFetching = Date.now() + + const isUtg = await isTestFile(editor.document.uri.fsPath, { + languageId: editor.document.languageId, + fileContent: editor.document.getText(), + }) + + let supplementalContextPromise: Promise< + Pick | undefined + > + + if (isUtg) { + supplementalContextPromise = fetchSupplementalContextForTest(editor, cancellationToken) + } else { + supplementalContextPromise = fetchSupplementalContextForSrc(editor, cancellationToken, languageClient) + } + + return supplementalContextPromise + .then((value) => { + if (value) { + const resBeforeTruncation = { + isUtg: isUtg, + isProcessTimeout: false, + supplementalContextItems: value.supplementalContextItems.filter( + (item) => item.content.trim().length !== 0 + ), + contentsLength: value.supplementalContextItems.reduce((acc, curr) => acc + curr.content.length, 0), + latency: Date.now() - timesBeforeFetching, + strategy: value.strategy, + } + + return truncateSuppelementalContext(resBeforeTruncation) + } else { + return undefined + } + }) + .catch((err) => { + if (err instanceof ToolkitError && err.cause instanceof CancellationError) { + return { + isUtg: isUtg, + isProcessTimeout: true, + supplementalContextItems: [], + contentsLength: 0, + latency: Date.now() - timesBeforeFetching, + strategy: 'empty', + } + } else { + getLogger().error( + `Fail to fetch supplemental context for target file ${editor.document.fileName}: ${err}` + ) + return undefined + } + }) +} + +/** + * Requirement + * - Maximum 5 supplemental context. + * - Each chunk can't exceed 10240 characters + * - Sum of all chunks can't exceed 20480 characters + */ +export function truncateSuppelementalContext( + context: CodeWhispererSupplementalContext +): CodeWhispererSupplementalContext { + let c = context.supplementalContextItems.map((item) => { + if (item.content.length > crossFileContextConfig.maxLengthEachChunk) { + return { + ...item, + content: truncateLineByLine(item.content, crossFileContextConfig.maxLengthEachChunk), + } + } else { + return item + } + }) + + if (c.length > crossFileContextConfig.maxContextCount) { + c = c.slice(0, crossFileContextConfig.maxContextCount) + } + + let curTotalLength = c.reduce((acc, cur) => { + return acc + cur.content.length + }, 0) + while (curTotalLength >= 20480 && c.length - 1 >= 0) { + const last = c[c.length - 1] + c = c.slice(0, -1) + curTotalLength -= last.content.length + } + + return { + ...context, + supplementalContextItems: c, + contentsLength: curTotalLength, + } +} + +export function truncateLineByLine(input: string, l: number): string { + const maxLength = l > 0 ? l : -1 * l + if (input.length === 0) { + return '' + } + + const shouldAddNewLineBack = input.endsWith(os.EOL) + let lines = input.trim().split(os.EOL) + let curLen = input.length + while (curLen > maxLength && lines.length - 1 >= 0) { + const last = lines[lines.length - 1] + lines = lines.slice(0, -1) + curLen -= last.length + 1 + } + + const r = lines.join(os.EOL) + if (shouldAddNewLineBack) { + return r + os.EOL + } else { + return r + } +} diff --git a/packages/core/src/codewhisperer/util/supplementalContext/utgUtils.ts b/packages/core/src/codewhisperer/util/supplementalContext/utgUtils.ts new file mode 100644 index 00000000000..0d33969773e --- /dev/null +++ b/packages/core/src/codewhisperer/util/supplementalContext/utgUtils.ts @@ -0,0 +1,229 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as path from 'path' +import { fs } from '../../../shared/fs/fs' +import * as vscode from 'vscode' +import { + countSubstringMatches, + extractClasses, + extractFunctions, + isTestFile, + utgLanguageConfig, + utgLanguageConfigs, +} from './codeParsingUtil' +import { ToolkitError } from '../../../shared/errors' +import { supplemetalContextFetchingTimeoutMsg } from '../../models/constants' +import { CancellationError } from '../../../shared/utilities/timeoutUtils' +import { utgConfig } from '../../models/constants' +import { getOpenFilesInWindow } from '../../../shared/utilities/editorUtilities' +import { getLogger } from '../../../shared/logger/logger' +import { CodeWhispererSupplementalContext, CodeWhispererSupplementalContextItem, UtgStrategy } from '../../models/model' + +const utgSupportedLanguages: vscode.TextDocument['languageId'][] = ['java', 'python'] + +type UtgSupportedLanguage = (typeof utgSupportedLanguages)[number] + +function isUtgSupportedLanguage(languageId: vscode.TextDocument['languageId']): languageId is UtgSupportedLanguage { + return utgSupportedLanguages.includes(languageId) +} + +export function shouldFetchUtgContext(languageId: vscode.TextDocument['languageId']): boolean | undefined { + if (!isUtgSupportedLanguage(languageId)) { + return undefined + } + + return languageId === 'java' +} + +/** + * This function attempts to find a focal file for the given trigger file. + * Attempt 1: If naming patterns followed correctly, source file can be found by name referencing. + * Attempt 2: Compare the function and class names of trigger file and all other open files in editor + * to find the closest match. + * Once found the focal file, we split it into multiple pieces as supplementalContext. + * @param editor + * @returns + */ +export async function fetchSupplementalContextForTest( + editor: vscode.TextEditor, + cancellationToken: vscode.CancellationToken +): Promise | undefined> { + const shouldProceed = shouldFetchUtgContext(editor.document.languageId) + + if (!shouldProceed) { + return shouldProceed === undefined ? undefined : { supplementalContextItems: [], strategy: 'empty' } + } + + const languageConfig = utgLanguageConfigs[editor.document.languageId] + + // TODO (Metrics): 1. Total number of calls to fetchSupplementalContextForTest + throwIfCancelled(cancellationToken) + + let crossSourceFile = await findSourceFileByName(editor, languageConfig, cancellationToken) + if (crossSourceFile) { + // TODO (Metrics): 2. Success count for fetchSourceFileByName (find source file by name) + getLogger().debug(`CodeWhisperer finished fetching utg context by file name`) + return { + supplementalContextItems: await generateSupplementalContextFromFocalFile( + crossSourceFile, + 'byName', + cancellationToken + ), + strategy: 'byName', + } + } + throwIfCancelled(cancellationToken) + + crossSourceFile = await findSourceFileByContent(editor, languageConfig, cancellationToken) + if (crossSourceFile) { + // TODO (Metrics): 3. Success count for fetchSourceFileByContent (find source file by content) + getLogger().debug(`CodeWhisperer finished fetching utg context by file content`) + return { + supplementalContextItems: await generateSupplementalContextFromFocalFile( + crossSourceFile, + 'byContent', + cancellationToken + ), + strategy: 'byContent', + } + } + + // TODO (Metrics): 4. Failure count - when unable to find focal file (supplemental context empty) + getLogger().debug(`CodeWhisperer failed to fetch utg context`) + return { + supplementalContextItems: [], + strategy: 'empty', + } +} + +async function generateSupplementalContextFromFocalFile( + filePath: string, + strategy: UtgStrategy, + cancellationToken: vscode.CancellationToken +): Promise { + const fileContent = await fs.readFileText(vscode.Uri.parse(filePath!).fsPath) + + // DO NOT send code chunk with empty content + if (fileContent.trim().length === 0) { + return [] + } + + return [ + { + filePath: filePath, + content: 'UTG\n' + fileContent.slice(0, Math.min(fileContent.length, utgConfig.maxSegmentSize)), + }, + ] +} + +async function findSourceFileByContent( + editor: vscode.TextEditor, + languageConfig: utgLanguageConfig, + cancellationToken: vscode.CancellationToken +): Promise { + const testFileContent = await fs.readFileText(editor.document.fileName) + const testElementList = extractFunctions(testFileContent, languageConfig.functionExtractionPattern) + + throwIfCancelled(cancellationToken) + + testElementList.push(...extractClasses(testFileContent, languageConfig.classExtractionPattern)) + + throwIfCancelled(cancellationToken) + + let sourceFilePath: string | undefined = undefined + let maxMatchCount = 0 + + if (testElementList.length === 0) { + // TODO: Add metrics here, as unable to parse test file using Regex. + return sourceFilePath + } + + const relevantFilePaths = await getRelevantUtgFiles(editor) + + throwIfCancelled(cancellationToken) + + // TODO (Metrics):Add metrics for relevantFilePaths length + for (const filePath of relevantFilePaths) { + throwIfCancelled(cancellationToken) + + const fileContent = await fs.readFileText(filePath) + const elementList = extractFunctions(fileContent, languageConfig.functionExtractionPattern) + elementList.push(...extractClasses(fileContent, languageConfig.classExtractionPattern)) + const matchCount = countSubstringMatches(elementList, testElementList) + if (matchCount > maxMatchCount) { + maxMatchCount = matchCount + sourceFilePath = filePath + } + } + return sourceFilePath +} + +async function getRelevantUtgFiles(editor: vscode.TextEditor): Promise { + const targetFile = editor.document.uri.fsPath + const language = editor.document.languageId + + return await getOpenFilesInWindow(async (candidateFile) => { + return ( + targetFile !== candidateFile && + path.extname(targetFile) === path.extname(candidateFile) && + !(await isTestFile(candidateFile, { languageId: language })) + ) + }) +} + +export function guessSrcFileName( + testFileName: string, + languageId: vscode.TextDocument['languageId'] +): string | undefined { + const languageConfig = utgLanguageConfigs[languageId] + if (!languageConfig) { + return undefined + } + + for (const pattern of languageConfig.testFilenamePattern) { + try { + const match = testFileName.match(pattern) + if (match) { + return match[1] + match[2] + } + } catch (err) { + if (err instanceof Error) { + getLogger().error( + `codewhisperer: error while guessing source file name from file ${testFileName} and pattern ${pattern}: ${err.message}` + ) + } + } + } + + return undefined +} + +async function findSourceFileByName( + editor: vscode.TextEditor, + languageConfig: utgLanguageConfig, + cancellationToken: vscode.CancellationToken +): Promise { + const testFileName = path.basename(editor.document.fileName) + const assumedSrcFileName = guessSrcFileName(testFileName, editor.document.languageId) + if (!assumedSrcFileName) { + return undefined + } + + const sourceFiles = await vscode.workspace.findFiles(`**/${assumedSrcFileName}`) + + throwIfCancelled(cancellationToken) + + if (sourceFiles.length > 0) { + return sourceFiles[0].toString() + } + return undefined +} + +function throwIfCancelled(token: vscode.CancellationToken): void | never { + if (token.isCancellationRequested) { + throw new ToolkitError(supplemetalContextFetchingTimeoutMsg, { cause: new CancellationError('timeout') }) + } +} diff --git a/packages/core/src/codewhisperer/util/telemetryHelper.ts b/packages/core/src/codewhisperer/util/telemetryHelper.ts index 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/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/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/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..e2f9e4c32f4 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 * as nls from 'vscode-nls' + import { Lambda } from 'aws-sdk' 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)) @@ -87,10 +237,14 @@ export async function activate(context: ExtContext): Promise { Commands.register('aws.appBuilder.tailLogs', async (node: LambdaFunctionNode | TreeNode) => { let functionConfiguration: Lambda.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/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..6bfd3777463 100644 --- a/packages/core/src/lambda/commands/uploadLambda.ts +++ b/packages/core/src/lambda/commands/uploadLambda.ts @@ -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..e2d9e9aae27 100644 --- a/packages/core/src/lambda/explorer/cloudFormationNodes.ts +++ b/packages/core/src/lambda/explorer/cloudFormationNodes.ts @@ -153,8 +153,7 @@ function makeCloudFormationLambdaFunctionNode( regionCode: string, configuration: Lambda.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..1feb40f437a 100644 --- a/packages/core/src/lambda/explorer/lambdaFunctionNode.ts +++ b/packages/core/src/lambda/explorer/lambdaFunctionNode.ts @@ -5,26 +5,60 @@ import { Lambda } from 'aws-sdk' 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: Lambda.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 { 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..62a01c6445a 100644 --- a/packages/core/src/lambda/explorer/lambdaNodes.ts +++ b/packages/core/src/lambda/explorer/lambdaNodes.ts @@ -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. @@ -70,10 +73,16 @@ function makeLambdaFunctionNode( regionCode: string, configuration: Lambda.FunctionConfiguration ): LambdaFunctionNode { - const node = new LambdaFunctionNode(parent, regionCode, configuration) - node.contextValue = samLambdaImportableRuntimes.contains(node.configuration.Runtime ?? '') - ? contextValueLambdaFunctionImportable - : contextValueLambdaFunction + let contextValue = contextValueLambdaFunction + const isImportableRuntime = 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..06e35dbcd2b 100644 --- a/packages/core/src/lambda/models/samLambdaRuntime.ts +++ b/packages/core/src/lambda/models/samLambdaRuntime.ts @@ -68,7 +68,7 @@ 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']) /** * Deprecated runtimes can be found at https://docs.aws.amazon.com/lambda/latest/dg/runtime-support-policy.html @@ -99,6 +99,16 @@ const defaultRuntimes = ImmutableMap([ [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, @@ -125,7 +135,11 @@ 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 diff --git a/packages/core/src/lambda/remoteDebugging/lambdaDebugger.ts b/packages/core/src/lambda/remoteDebugging/lambdaDebugger.ts new file mode 100644 index 00000000000..bdb8ba4ff64 --- /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 type { Lambda } from 'aws-sdk' +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: Lambda.FunctionConfiguration, + region: string + ): Promise + waitForSetup( + progress: vscode.Progress<{ message?: string; increment?: number }>, + functionConfig: Lambda.FunctionConfiguration, + region: string + ): Promise + waitForFunctionUpdates(progress: vscode.Progress<{ message?: string; increment?: number }>): Promise + cleanup(functionConfig: Lambda.FunctionConfiguration): Promise +} + +// this should be called when the debug session is started +export async function persistLambdaSnapshot(config: Lambda.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(): Lambda.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..b30c165d4a4 --- /dev/null +++ b/packages/core/src/lambda/remoteDebugging/ldkClient.ts @@ -0,0 +1,474 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { IoTSecureTunneling, Lambda } from 'aws-sdk' +import { getClientId } from '../../shared/telemetry/util' +import { DefaultLambdaClient } from '../../shared/clients/lambdaClient' +import { LocalProxy } from './localProxy' +import globals from '../../shared/extensionGlobals' +import { getLogger } from '../../shared/logger/logger' +import { getIoTSTClientWithAgent, getLambdaClientWithAgent } from './utils' +import { 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: Lambda.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)) + } + return this.lambdaClientCache.get(region)! + } + + private async getIoTSTClient(region: string): Promise { + if (!this.iotSTClientCache.has(region)) { + this.iotSTClientCache.set(region, await 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 = await this.getIoTSTClient(region) + + // Define tunnel identifier + const tunnelIdentifier = `RemoteDebugging+${vscodeUuid}` + const timeoutInMinutes = 720 + // List existing tunnels + const listTunnelsResponse = await iotSecureTunneling.listTunnels({}).promise() + + // 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 + .closeTunnel({ + tunnelId: existingTunnel.tunnelId, + delete: false, + }) + .promise() + + getLogger().info(`Closed tunnel ${existingTunnel.tunnelId} with less than 15 minutes remaining`) + } + } + + // Create new tunnel + const openTunnelResponse = await iotSecureTunneling + .openTunnel({ + description: tunnelIdentifier, + timeoutConfig: { + maxLifetimeTimeoutMinutes: timeoutInMinutes, // 12 hours + }, + destinationConfig: { + services: ['WSS'], + }, + }) + .promise() + + 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 = await this.getIoTSTClient(region) + const rotateResponse = await iotSecureTunneling + .rotateTunnelAccessToken({ + tunnelId: tunnelId, + clientMode: 'ALL', + }) + .promise() + + 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 Lambda.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: Lambda.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: Lambda.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: Lambda.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..a12f0254b33 --- /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 type { Lambda } from 'aws-sdk' +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: Lambda.FunctionConfiguration, + currentConfig: Lambda.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: Lambda.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: Lambda.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: string | 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: string | undefined): boolean { + if (!runtime) { + return false + } + try { + return ['node', 'python', 'java'].includes(mapFamilyToDebugType.get(getFamily(runtime)) ?? '') + } catch { + return false + } + } + + public async installDebugExtension(runtime: string | 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)) { + 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..a7d98f06668 --- /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 type { Lambda } from 'aws-sdk' +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: Lambda.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: Lambda.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: Lambda.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..716f91d7e01 --- /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 type { Lambda } from 'aws-sdk' +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: Lambda.ArchitecturesList | 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: Lambda.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: Lambda.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: Lambda.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..7d09fb46f49 --- /dev/null +++ b/packages/core/src/lambda/remoteDebugging/utils.ts @@ -0,0 +1,41 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import IoTSecureTunneling from 'aws-sdk/clients/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): Promise { + const customUserAgent = `${customUserAgentBase} ${getUserAgent({ includePlatform: true, includeClientId: true })}` + return globals.sdkClientBuilder.createAwsService( + IoTSecureTunneling, + { + customUserAgent, + }, + region + ) +} 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..a17783d37b8 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 * 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 @@ -46,6 +50,23 @@ 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) @@ -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..7f80bd8370f 100644 --- a/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts +++ b/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts @@ -7,7 +7,7 @@ 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 +15,11 @@ 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 } from '../../../shared/telemetry/telemetry' 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?: string + 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,203 @@ 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 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('') + } 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 +343,70 @@ 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 = await getLambdaHandlerFile( + vscode.Uri.file(this.data.LocalRootPath), + '', + this.data.LambdaFunctionNode?.configuration.Handler ?? '', + this.data.Runtime ?? 'unknown' + ) + 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 +428,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 +440,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 +648,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 Runtime | 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 +900,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 ?? 'unknown' + 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/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..4531c117978 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/sageMakerUnifiedStudioSpacesParentNode.ts @@ -0,0 +1,231 @@ +/*! + * 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' + +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(5, 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..10b52f83728 --- /dev/null +++ b/packages/core/src/sagemakerunifiedstudio/explorer/nodes/utils.ts @@ -0,0 +1,385 @@ +/*! + * 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' + +/** + * 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/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/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/lambdaClient.ts b/packages/core/src/shared/clients/lambdaClient.ts index 9dac95eee50..6a89e508967 100644 --- a/packages/core/src/shared/clients/lambdaClient.ts +++ b/packages/core/src/shared/clients/lambdaClient.ts @@ -10,21 +10,31 @@ import globals from '../extensionGlobals' import { getLogger } from '../logger/logger' import { ClassToInterfaceType } from '../utilities/tsUtils' +import { LambdaClient as LambdaSdkClient, GetFunctionCommand, GetFunctionCommandOutput } from '@aws-sdk/client-lambda' +import { CancellationError } from '../utilities/timeoutUtils' +import { fromSSO } from '@aws-sdk/credential-provider-sso' +import { getIAMConnection } from '../../auth/utils' +import { WaiterConfiguration } from 'aws-sdk/lib/service' + 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({ FunctionName: name, + Qualifier: qualifier, }) .promise() @@ -33,7 +43,7 @@ export class DefaultLambdaClient { } } - public async invoke(name: string, payload?: _Blob): Promise { + public async invoke(name: string, payload?: _Blob, version?: string): Promise { const sdkClient = await this.createSdkClient() const response = await sdkClient @@ -41,6 +51,7 @@ export class DefaultLambdaClient { FunctionName: name, LogType: 'Tail', Payload: payload, + Qualifier: version, }) .promise() @@ -80,6 +91,39 @@ export class DefaultLambdaClient { } } + public async getLayerVersion(name: string, version: number): Promise { + getLogger().debug(`getLayerVersion called for LayerName: ${name}, VersionNumber ${version}`) + const client = await this.createSdkClient() + + try { + const response = await client.getLayerVersion({ LayerName: name, VersionNumber: version }).promise() + // 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: Lambda.ListLayerVersionsRequest = { LayerName: name } + do { + const response: Lambda.ListLayerVersionsResponse = await client.listLayerVersions(request).promise() + + 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() @@ -132,11 +176,160 @@ export class DefaultLambdaClient { } } + public async updateFunctionConfiguration( + params: Lambda.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.updateFunctionConfiguration(params).promise() + 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.getFunctionConfiguration({ FunctionName: params.FunctionName }).promise() + 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 + .publishVersion({ + FunctionName: name, + }) + .promise() + + if (waitForUpdate) { + let state = 'Pending' + while (state === 'Pending') { + await new Promise((resolve) => setTimeout(resolve, 1000)) + const statusResponse = await client + .getFunctionConfiguration({ FunctionName: name, Qualifier: response.Version }) + .promise() + 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?: WaiterConfiguration): Promise { + const sdkClient = await this.createSdkClient() + + await sdkClient + .waitFor('functionActive', { + FunctionName: functionName, + $waiter: waiter ?? { + delay: 1, + // In LocalStack, it requires 2 MBit/s connection to download ~150 MB Lambda image in 600 seconds + maxAttempts: 600, + }, + }) + .promise() + } + private async createSdkClient(): Promise { return await globals.sdkClientBuilder.createAwsService( Lambda, - { httpOptions: { timeout: this.defaultTimeoutInMs } }, + { + httpOptions: { timeout: this.defaultTimeoutInMs }, + customUserAgent: this.userAgent, + }, this.regionCode ) } } + +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/sagemaker.ts b/packages/core/src/shared/clients/sagemaker.ts new file mode 100644 index 00000000000..ff086ed1d9e --- /dev/null +++ b/packages/core/src/shared/clients/sagemaker.ts @@ -0,0 +1,359 @@ +/*! + * 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' + +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 !== 'JupyterLab' && appType !== 'CodeEditor') { + throw new ToolkitError(`Unsupported AppType "${appType}" for space "${spaceName}"`) + } + + // Get app resource spec + const requestedResourceSpec = + 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 + } + + // Get remote access flag + if (!spaceDetails.SpaceSettings?.RemoteAccess || spaceDetails.SpaceSettings?.RemoteAccess === 'DISABLED') { + try { + await this.updateSpace({ + DomainId: domainId, + SpaceName: spaceName, + SpaceSettings: { + RemoteAccess: 'ENABLED', + }, + }) + 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 + + const createAppRequest: CreateAppCommandInput = { + DomainId: domainId, + SpaceName: spaceName, + AppType: appType, + AppName: 'default', + ResourceSpec: cleanedResourceSpec, + } + + try { + 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/stsClient.ts b/packages/core/src/shared/clients/stsClient.ts index a090a846bf8..6cc01f57fa8 100644 --- a/packages/core/src/shared/clients/stsClient.ts +++ b/packages/core/src/shared/clients/stsClient.ts @@ -8,11 +8,13 @@ import { Credentials } from '@aws-sdk/types' import globals from '../extensionGlobals' import { ClassToInterfaceType } from '../utilities/tsUtils' +export type GetCallerIdentityResponse = STS.GetCallerIdentityResponse export type StsClient = ClassToInterfaceType export class DefaultStsClient { public constructor( public readonly regionCode: string, - private readonly credentials?: Credentials + private readonly credentials?: Credentials, + private readonly endpointUrl?: string ) {} public async assumeRole(request: STS.AssumeRoleRequest): Promise { @@ -33,6 +35,7 @@ export class DefaultStsClient { { credentials: this.credentials, stsRegionalEndpoints: 'regional', + endpoint: this.endpointUrl, }, this.regionCode ) 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..5d114043be3 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' @@ -849,6 +849,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 8037dfa0381..b8b5780c612 100644 --- a/packages/core/src/shared/extensionUtilities.ts +++ b/packages/core/src/shared/extensionUtilities.ts @@ -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,26 +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') - return true + hasSMEnvVars = true } - // Fall back to app name checks 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/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/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 c89360a01dd..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' 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/utils/platform.ts b/packages/core/src/shared/lsp/utils/platform.ts index fadeefb7e68..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' @@ -101,7 +108,48 @@ export function createServerOptions({ 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/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/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 1a518651a4f..55bc77f9828 100644 --- a/packages/core/src/shared/settings-toolkit.gen.ts +++ b/packages/core/src/shared/settings-toolkit.gen.ts @@ -52,7 +52,8 @@ export const toolkitSettings = { "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..bba23b9a4d8 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)}User '%r'\n` + } 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/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..fee97143abd 100644 --- a/packages/core/src/shared/telemetry/vscodeTelemetry.json +++ b/packages/core/src/shared/telemetry/vscodeTelemetry.json @@ -238,9 +238,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 +1213,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/utilities/diffUtils.ts b/packages/core/src/shared/utilities/diffUtils.ts index 64d09c19036..994f91a5434 100644 --- a/packages/core/src/shared/utilities/diffUtils.ts +++ b/packages/core/src/shared/utilities/diffUtils.ts @@ -25,7 +25,7 @@ import jaroWinkler from 'jaro-winkler' */ 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 @@ -152,24 +152,12 @@ export function getDiffCharsAndLines( } /** - * Extracts modified lines from a unified diff string. - * @param unifiedDiff The unified diff patch as a string. + * 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 getModifiedLinesFromUnifiedDiff(unifiedDiff: string): Map { - const removedLines: string[] = [] - const addedLines: string[] = [] - - // Parse the unified diff to extract removed and added lines - const lines = unifiedDiff.split('\n') - for (const line of lines) { - if (line.startsWith('-') && !line.startsWith('---')) { - removedLines.push(line.slice(1)) - } else if (line.startsWith('+') && !line.startsWith('+++')) { - addedLines.push(line.slice(1)) - } - } - +export function getModifiedLinesFromCode(addedLines: string[], removedLines: string[]): Map { const modifiedMap = new Map() let addedIndex = 0 diff --git a/packages/core/src/shared/utilities/functionUtils.ts b/packages/core/src/shared/utilities/functionUtils.ts index 214721b1cdb..fa0e61847bb 100644 --- a/packages/core/src/shared/utilities/functionUtils.ts +++ b/packages/core/src/shared/utilities/functionUtils.ts @@ -63,6 +63,32 @@ export function onceChanged(fn: (...args: U) => T): (...args : ((val = fn(...args)), (ran = true), (prevArgs = args.map(String).join(':')), val) } +/** + * Creates a function that runs only if the args changed versus the previous invocation, + * using a custom comparator function for argument comparison. + * + * @param fn The function to wrap + * @param comparator Function that returns true if arguments are equal + */ +export function onceChangedWithComparator( + fn: (...args: U) => T, + comparator: (prev: U, current: U) => boolean +): (...args: U) => T { + let val: T + let ran = false + let prevArgs: U + + return (...args) => { + if (ran && comparator(prevArgs, args)) { + return val + } + val = fn(...args) + ran = true + prevArgs = args + return val + } +} + /** * Creates a new function that stores the result of a call. * diff --git a/packages/core/src/shared/utilities/index.ts b/packages/core/src/shared/utilities/index.ts index ecf753090ca..e86f941456d 100644 --- a/packages/core/src/shared/utilities/index.ts +++ b/packages/core/src/shared/utilities/index.ts @@ -7,3 +7,6 @@ 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 14e628e87f7..a05d57118d2 100644 --- a/packages/core/src/shared/utilities/proxyUtil.ts +++ b/packages/core/src/shared/utilities/proxyUtil.ts @@ -14,6 +14,7 @@ interface ProxyConfig { noProxy: string | undefined proxyStrictSSL: boolean | true certificateAuthority: string | undefined + isProxyAndCertAutoDiscoveryEnabled: boolean } /** @@ -56,13 +57,15 @@ export class ProxyUtil { const amazonQConfig = vscode.workspace.getConfiguration('amazonQ') const proxySettings = amazonQConfig.get<{ certificateAuthority?: string - }>('proxy', {}) + enableProxyAndCertificateAutoDiscovery: boolean + }>('proxy', { enableProxyAndCertificateAutoDiscovery: true }) return { proxyUrl, noProxy, proxyStrictSSL, certificateAuthority: proxySettings.certificateAuthority, + isProxyAndCertAutoDiscoveryEnabled: proxySettings.enableProxyAndCertificateAutoDiscovery, } } @@ -70,8 +73,8 @@ export class ProxyUtil { * Sets environment variables based on proxy configuration */ private static async setProxyEnvironmentVariables(config: ProxyConfig): Promise { - // Always enable experimental proxy support for better handling of both explicit and transparent proxies - process.env.EXPERIMENTAL_HTTP_PROXY_SUPPORT = 'true' + // Set experimental proxy support based on user setting + process.env.EXPERIMENTAL_HTTP_PROXY_SUPPORT = config.isProxyAndCertAutoDiscoveryEnabled.toString() const proxyUrl = config.proxyUrl // Set proxy environment variables diff --git a/packages/core/src/shared/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 5ee891cc7d3..1ddb042e415 100644 --- a/packages/core/src/shared/vscode/env.ts +++ b/packages/core/src/shared/vscode/env.ts @@ -6,12 +6,12 @@ import * as semver from 'semver' import * as vscode from 'vscode' import * as packageJson from '../../../package.json' -import * as os from 'os' import { getLogger } from '../logger/logger' import { onceChanged } from '../utilities/functionUtils' import { ChildProcess } from '../utilities/processUtils' import globals, { isWeb } from '../extensionGlobals' import * as devConfig from '../../dev/config' +import * as os from 'os' /** * Returns true if the current build is running on CI (build server). @@ -124,6 +124,35 @@ export function isRemoteWorkspace(): boolean { return vscode.env.remoteName === 'ssh-remote' } +/** + * Parses an os-release file according to the freedesktop.org standard. + * + * @param content The content of the os-release file + * @returns A record of key-value pairs from the os-release file + * + * @see https://www.freedesktop.org/software/systemd/man/latest/os-release.html + */ +function parseOsRelease(content: string): Record { + const result: Record = {} + + for (let line of content.split('\n')) { + line = line.trim() + // Skip empty lines and comments + if (!line || line.startsWith('#')) { + continue + } + + const eqIndex = line.indexOf('=') + if (eqIndex > 0) { + const key = line.slice(0, eqIndex) + const value = line.slice(eqIndex + 1).replace(/^["']|["']$/g, '') + result[key] = value + } + } + + return result +} + /** * Checks if the current environment has SageMaker-specific environment variables * @returns true if SageMaker environment variables are detected @@ -146,36 +175,83 @@ export function hasSageMakerEnvVars(): boolean { /** * Checks if the current environment is running on Amazon Linux 2. * - * This function attempts to detect if we're running in a container on an AL2 host - * by checking both the OS release and container-specific indicators. + * This function detects the container/runtime OS, not the host OS. + * In containerized environments, we check the container's OS identity. * - * Example: `5.10.220-188.869.amzn2int.x86_64` or `5.10.236-227.928.amzn2.x86_64` (Cloud Dev Machine) + * 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() { + // Skip AL2 detection for web environments + // In web mode, we're running in a browser, not on AL2 + if (isWeb()) { + return false + } + // First check if we're in a SageMaker environment, which should not be treated as AL2 - // even if the underlying host is AL2 + // even if the underlying container is AL2 if (hasSageMakerEnvVars()) { return false } - // Check if we're in a container environment that's not AL2 - if (process.env.container === 'docker' || process.env.DOCKER_HOST || process.env.DOCKER_BUILDKIT) { - // Additional check for container OS - if we can determine it's not AL2 - try { - const fs = require('fs') - if (fs.existsSync('/etc/os-release')) { - const osRelease = fs.readFileSync('/etc/os-release', 'utf8') - if (!osRelease.includes('Amazon Linux 2') && !osRelease.includes('amzn2')) { - return false + // Only proceed with file checks on Linux platforms + if (process.platform !== 'linux') { + return false + } + + // Check the container/runtime OS identity via os-release files + // This correctly identifies AL2 containers regardless of host OS + try { + const fs = require('fs') + // Check /etc/os-release with fallback to /usr/lib/os-release as per freedesktop.org spec + const osReleasePaths = ['/etc/os-release', '/usr/lib/os-release'] + + for (const osReleasePath of osReleasePaths) { + if (fs.existsSync(osReleasePath)) { + try { + const osReleaseContent = fs.readFileSync(osReleasePath, 'utf8') + const osRelease = parseOsRelease(osReleaseContent) + + // Check if this is Amazon Linux 2 + // We trust os-release as the authoritative source for container OS identity + return osRelease.VERSION_ID === '2' && osRelease.ID === 'amzn' + } catch (e) { + // Continue to next path if parsing fails + getLogger().error(`Parsing os-release file ${osReleasePath} failed: ${e}`) } } - } catch (e) { - // If we can't read the file, fall back to the os.release() check } + } catch (e) { + // If we can't read the files, we cannot determine AL2 status + getLogger().error(`Checking os-release files failed: ${e}`) } - // Standard check for AL2 in the OS release string - return (os.release().includes('.amzn2int.') || os.release().includes('.amzn2.')) && process.platform === 'linux' + // Fall back to kernel version check if os-release files are unavailable or failed + // This is needed for environments where os-release might not be accessible + const kernelRelease = os.release() + const hasAL2Kernel = kernelRelease.includes('.amzn2int.') || kernelRelease.includes('.amzn2.') + + return hasAL2Kernel } /** @@ -217,9 +293,9 @@ export function getExtRuntimeContext(): { extensionHost: ExtensionHostLocation } { const extensionHost = - // taken from https://github.com/microsoft/vscode/blob/7c9e4bb23992c63f20cd86bbe7a52a3aa4bed89d/extensions/github-authentication/src/githubServer.ts#L121 to help determine which auth flows - // should be used - typeof navigator === 'undefined' + // Check if we're in a Node.js environment (desktop/remote) vs web worker + // Updated to be compatible with Node.js v22 which includes navigator global + typeof process === 'object' && process.versions?.node ? globals.context.extension.extensionKind === vscode.ExtensionKind.UI ? 'local' : 'remote' @@ -307,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 08d651578dc..3d45d93e14a 100644 --- a/packages/core/src/shared/vscode/setContext.ts +++ b/packages/core/src/shared/vscode/setContext.ts @@ -30,6 +30,8 @@ export type contextKey = | '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' @@ -40,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/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..b286ad6537f --- /dev/null +++ b/packages/core/src/test/amazonqGumby/transformationJobHistory.test.ts @@ -0,0 +1,333 @@ +/*! + * 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') + + const expectedPath = path.join(os.homedir(), '.aws', 'transform', 'transformation_history.tsv') + const fileContent = writtenFiles.get(expectedPath) + + assert(fileContent) + assert(fileContent.includes('date\tproject_name\tstatus\tduration\tdiff_patch\tsummary\tjob_id\n')) + assert( + fileContent.includes( + `01/01/25, 10:00 AM\ttest-project\tCOMPLETED\t5 min\t${path.join('/job/path', 'diff.patch')}\t${path.join('/job/path', 'summary', 'summary.md')}\tjob-123\n` + ) + ) + }) + + 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') + + 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\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') + + 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') + ) + + // 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/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..05a5aad9ed4 --- /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 { ResourcesToImport } from 'aws-sdk/clients/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: ResourcesToImport = [ + { + 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: ResourcesToImport = [ + { + 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..eaaa69254d7 100644 --- a/packages/core/src/test/awsService/appBuilder/utils.test.ts +++ b/packages/core/src/test/awsService/appBuilder/utils.test.ts @@ -12,9 +12,20 @@ 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' interface TestScenario { runtime: string @@ -303,4 +314,553 @@ 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', '{}') + 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: any + let enhancedClient: EnhancedCloudFormationClient + + beforeEach(function () { + // Create a mock CloudFormation client with all required methods + mockCfnClient = { + describeStacks: sandbox.stub(), + getTemplate: sandbox.stub(), + createChangeSet: sandbox.stub(), + describeStackResource: sandbox.stub(), + describeStackResources: sandbox.stub(), + } + enhancedClient = new EnhancedCloudFormationClient(mockCfnClient, 'us-east-1') + }) + + 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.describeStacks.returns({ + promise: sandbox.stub().rejects(permissionError), + } as any) + + 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.getTemplate.returns({ + promise: sandbox.stub().rejects(permissionError), + } as any) + + 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.createChangeSet.returns({ + promise: sandbox.stub().rejects(permissionError), + } as any) + + 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.describeStackResource.returns({ + promise: sandbox.stub().rejects(permissionError), + } as any) + + 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.describeStackResources.returns({ + promise: sandbox.stub().rejects(permissionError), + } as any) + + 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.describeStacks.returns({ + promise: sandbox.stub().rejects(nonPermissionError), + } as any) + + 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.describeStacks.returns({ + promise: sandbox.stub().resolves(mockResponse), + } as any) + + 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.sdkClientBuilder, '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..988f01902fd 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 { @@ -460,5 +462,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/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..e6a9637ed15 --- /dev/null +++ b/packages/core/src/test/awsService/sagemaker/model.test.ts @@ -0,0 +1,282 @@ +/*! + * 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('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..9ff24b2a3f9 --- /dev/null +++ b/packages/core/src/test/awsService/sagemaker/uriHandlers.test.ts @@ -0,0 +1,59 @@ +/*! + * 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', + } + + 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') + }) +}) 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 b911c9687ee..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, @@ -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 554d24c855a..f205f075872 100644 --- a/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts +++ b/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts @@ -67,7 +67,8 @@ dependencyManagement: plugins: - identifier: "com.example:plugin" targetVersion: "1.2.0" - versionProperty: "plugin.version" # Optional` + versionProperty: "plugin.version" # Optional + originType: "FIRST_PARTY" # or "THIRD_PARTY"` const validSctFile = ` @@ -570,15 +571,39 @@ 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 identifier format THEN fails validation`, function () { + const invalidFile = validCustomVersionsFile.replace('com.example:library1', 'com.example-library1') + const errorMessage = validateCustomVersionsFile(invalidFile) + assert.strictEqual( + errorMessage, + `Invalid identifier format: \`com.example-library1\`. Must be in format \`groupId:artifactId\` without spaces` + ) + }) + + it(`WHEN validateCustomVersionsFile on .yaml file with invalid originType THEN fails validation`, function () { + const invalidFile = validCustomVersionsFile.replace('FIRST_PARTY', 'INVALID_TYPE') + const errorMessage = validateCustomVersionsFile(invalidFile) + assert.strictEqual( + errorMessage, + `Invalid originType: \`INVALID_TYPE\`. Must be either \`FIRST_PARTY\` or \`THIRD_PARTY\`` + ) + }) + + it(`WHEN validateCustomVersionsFile on .yaml file with missing targetVersion THEN fails validation`, function () { + const invalidFile = validCustomVersionsFile.replace('targetVersion: "2.1.0"', '') + const errorMessage = validateCustomVersionsFile(invalidFile) + assert.strictEqual(errorMessage, `Missing \`targetVersion\` in: \`com.example:library1\``) }) it(`WHEN validateMetadataFile on fully valid .sct file THEN passes validation`, async function () { diff --git a/packages/core/src/test/codewhisperer/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/zipUtil.test.ts b/packages/core/src/test/codewhisperer/zipUtil.test.ts index e6c4f4148e5..102bf2fc441 100644 --- a/packages/core/src/test/codewhisperer/zipUtil.test.ts +++ b/packages/core/src/test/codewhisperer/zipUtil.test.ts @@ -7,15 +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' describe('zipUtil', function () { const workspaceFolder = getTestWorkspaceFolder() @@ -140,43 +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) - }) - }) }) 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/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/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/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/remoteDebugging/ldkClient.test.ts b/packages/core/src/test/lambda/remoteDebugging/ldkClient.test.ts new file mode 100644 index 00000000000..91f99aa0409 --- /dev/null +++ b/packages/core/src/test/lambda/remoteDebugging/ldkClient.test.ts @@ -0,0 +1,471 @@ +/*! + * 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 { Lambda } from 'aws-sdk' +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' + +describe('LdkClient', () => { + let sandbox: sinon.SinonSandbox + let ldkClient: LdkClient + let mockLambdaClient: any + let mockIoTSTClient: any + 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) + + // Mock IoT ST client with proper promise structure + const createPromiseStub = () => sandbox.stub() + mockIoTSTClient = { + listTunnels: sandbox.stub().returns({ promise: createPromiseStub() }), + openTunnel: sandbox.stub().returns({ promise: createPromiseStub() }), + closeTunnel: sandbox.stub().returns({ promise: createPromiseStub() }), + rotateTunnelAccessToken: sandbox.stub().returns({ promise: createPromiseStub() }), + } + sandbox.stub(utils, 'getIoTSTClientWithAgent').resolves(mockIoTSTClient) + + // 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.listTunnels().promise.resolves({ tunnelSummaries: [] }) + mockIoTSTClient.openTunnel().promise.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(mockIoTSTClient.listTunnels.called, 'Should list existing tunnels') + assert(mockIoTSTClient.openTunnel.called, 'Should create new tunnel') + }) + + it('should reuse existing tunnel with sufficient time remaining', async () => { + const existingTunnel = { + tunnelId: 'existing-tunnel', + description: 'RemoteDebugging+test-client-id', + status: 'OPEN', + createdAt: new Date(Date.now() - 60 * 60 * 1000), // 1 hour ago + } + + mockIoTSTClient.listTunnels().promise.resolves({ tunnelSummaries: [existingTunnel] }) + mockIoTSTClient.rotateTunnelAccessToken().promise.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.listTunnels().promise.resolves({ tunnelSummaries: [] }) + mockIoTSTClient.openTunnel().promise.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.rotateTunnelAccessToken().promise.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.rotateTunnelAccessToken().promise.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: Lambda.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: Lambda.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: Lambda.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('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..3975fc5a3c9 --- /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 { Lambda } from 'aws-sdk' +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: Lambda.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: Lambda.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: Lambda.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: Lambda.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: Lambda.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: Lambda.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: Lambda.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: Lambda.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: Lambda.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: Lambda.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: Lambda.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..46448cbbc08 --- /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 { Lambda } from 'aws-sdk' +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: Lambda.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' }) + }) + + 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..03aec290426 --- /dev/null +++ b/packages/core/src/test/lambda/remoteDebugging/testUtils.ts @@ -0,0 +1,178 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import sinon from 'sinon' +import { Lambda } from 'aws-sdk' +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 = {} +): Lambda.FunctionConfiguration { + return { + FunctionName: 'testFunction', + FunctionArn: 'arn:aws:lambda:us-west-2:123456789012:function:testFunction', + Runtime: 'nodejs18.x', + Handler: 'index.handler', + Timeout: 30, + Layers: [], + Environment: { Variables: {} }, + Architectures: ['x86_64'], + SnapStart: { ApplyOn: '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..a3eebe043a7 100644 --- a/packages/core/src/test/lambda/utils.test.ts +++ b/packages/core/src/test/lambda/utils.test.ts @@ -4,9 +4,27 @@ */ 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' -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({ @@ -52,4 +70,123 @@ describe('lambda utils', async function () { assert.throws(() => getLambdaDetails({ Runtime: 'COBOL-60', 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..c560331f606 100644 --- a/packages/core/src/test/lambda/vue/remoteInvoke/invokeLambda.test.ts +++ b/packages/core/src/test/lambda/vue/remoteInvoke/invokeLambda.test.ts @@ -28,6 +28,7 @@ describe('RemoteInvokeWebview', () => { let client: SinonStubbedInstance let remoteInvokeWebview: RemoteInvokeWebview let data: InitialData + let sandbox: sinon.SinonSandbox beforeEach(() => { client = createStubInstance(DefaultLambdaClient) @@ -42,7 +43,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', () => { @@ -150,10 +151,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 +241,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 +672,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 +693,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 +816,55 @@ 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 + + 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 +913,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..8e2bc15b001 --- /dev/null +++ b/packages/core/src/test/lambda/vue/remoteInvoke/invokeLambdaDebugging.test.ts @@ -0,0 +1,580 @@ +/*! + * 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' + +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: '{"result": "debug success"}', + } + 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: '{"result": "debug success"}', + } + 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: '{"debugResult": "success"}', + } + 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: '{"versionResult": "success"}', + } + 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..0fc94674c1e 100644 --- a/packages/core/src/test/lambda/vue/remoteInvoke/remoteInvoke.test.ts +++ b/packages/core/src/test/lambda/vue/remoteInvoke/remoteInvoke.test.ts @@ -26,7 +26,7 @@ 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 () { 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/shared/applicationBuilder/explorer/nodes/deployedNode.test.ts b/packages/core/src/test/shared/applicationBuilder/explorer/nodes/deployedNode.test.ts index afe8fb54dda..a24fd745fd7 100644 --- a/packages/core/src/test/shared/applicationBuilder/explorer/nodes/deployedNode.test.ts +++ b/packages/core/src/test/shared/applicationBuilder/explorer/nodes/deployedNode.test.ts @@ -137,6 +137,7 @@ describe('generateDeployedNode', () => { label: 'iam', getCredentials: sinon.stub(), state: 'valid', + endpointUrl: undefined, } const lambdaDeployedNodeInput = { @@ -147,6 +148,7 @@ describe('generateDeployedNode', () => { regionCode: expectedRegionCode, stackName: expectedStackName, resourceTreeEntity: { + Id: 'MyLambdaFunction', Type: 'AWS::Serverless::Function', }, } @@ -177,12 +179,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 +239,7 @@ describe('generateDeployedNode', () => { regionCode: expectedRegionCode, stackName: expectedStackName, resourceTreeEntity: { + Id: 'my-project-source-bucket-physical-id', Type: 'AWS::S3::Bucket', }, } @@ -256,7 +259,7 @@ describe('generateDeployedNode', () => { const expectedS3BucketName = 'my-project-source-bucket-physical-id' const deployedResourceNodeExplorerNode: S3BucketNode = validateBasicProperties( - deployedResourceNodes, + deployedResourceNodes as DeployedResourceNode[], expectedS3BucketArn, 'awsS3BucketNode', expectedRegionCode, @@ -284,6 +287,7 @@ describe('generateDeployedNode', () => { regionCode: expectedRegionCode, stackName: expectedStackName, resourceTreeEntity: { + Id: 'my-project-apigw-physical-id', Type: 'AWS::Serverless::Api', }, } @@ -330,7 +334,7 @@ describe('generateDeployedNode', () => { ) const deployedResourceNodeExplorerNode: RestApiNode = validateBasicProperties( - deployedResourceNodes, + deployedResourceNodes as DeployedResourceNode[], expectedApiGatewayArn, 'awsApiGatewayNode', expectedRegionCode, @@ -356,6 +360,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..8c30933dbf7 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 = { @@ -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/sagemakerClient.test.ts b/packages/core/src/test/shared/clients/sagemakerClient.test.ts new file mode 100644 index 00000000000..ecd60af5ad1 --- /dev/null +++ b/packages/core/src/test/shared/clients/sagemakerClient.test.ts @@ -0,0 +1,388 @@ +/*! + * 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(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..8f4ade282a4 100644 --- a/packages/core/src/test/shared/defaultAwsContext.test.ts +++ b/packages/core/src/test/shared/defaultAwsContext.test.ts @@ -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, 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..16d3792c63b 100644 --- a/packages/core/src/test/shared/extensionUtilities.test.ts +++ b/packages/core/src/test/shared/extensionUtilities.test.ts @@ -9,7 +9,7 @@ import { AWSError } from 'aws-sdk' 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 () => { @@ -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/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/sshConfig.test.ts b/packages/core/src/test/shared/sshConfig.test.ts index 96ca450ae14..03841644e24 100644 --- a/packages/core/src/test/shared/sshConfig.test.ts +++ b/packages/core/src/test/shared/sshConfig.test.ts @@ -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 b675fe74feb..3ba11518414 100644 --- a/packages/core/src/test/shared/utilities/functionUtils.test.ts +++ b/packages/core/src/test/shared/utilities/functionUtils.test.ts @@ -4,7 +4,13 @@ */ import assert from 'assert' -import { once, onceChanged, debounce, oncePerUniqueArg } from '../../../shared/utilities/functionUtils' +import { + once, + onceChanged, + debounce, + oncePerUniqueArg, + onceChangedWithComparator, +} from '../../../shared/utilities/functionUtils' import { installFakeClock } from '../../testUtil' describe('functionUtils', function () { @@ -49,6 +55,36 @@ describe('functionUtils', function () { assert.strictEqual(counter, 3) }) + it('onceChangedWithComparator()', function () { + let counter = 0 + const credentialsEqual = ([prev]: [any], [current]: [any]) => { + if (!prev && !current) { + return true + } + if (!prev || !current) { + return false + } + return prev.accessKeyId === current.accessKeyId && prev.secretAccessKey === current.secretAccessKey + } + const fn = onceChangedWithComparator((creds: any) => void counter++, credentialsEqual) + + const creds1 = { accessKeyId: 'key1', secretAccessKey: 'secret1' } + const creds2 = { accessKeyId: 'key1', secretAccessKey: 'secret1' } + const creds3 = { accessKeyId: 'key2', secretAccessKey: 'secret2' } + + fn(creds1) + assert.strictEqual(counter, 1) + + fn(creds2) // Same values, should not execute + assert.strictEqual(counter, 1) + + fn(creds3) // Different values, should execute + assert.strictEqual(counter, 2) + + fn(creds3) // Same as previous, should not execute + assert.strictEqual(counter, 2) + }) + it('oncePerUniqueArg()', function () { let counter = 0 const fn = oncePerUniqueArg((s: string) => { diff --git a/packages/core/src/test/shared/vscode/env.test.ts b/packages/core/src/test/shared/vscode/env.test.ts index cf09d085e68..a71aca33e8d 100644 --- a/packages/core/src/test/shared/vscode/env.test.ts +++ b/packages/core/src/test/shared/vscode/env.test.ts @@ -5,13 +5,21 @@ import assert from 'assert' import path from 'path' -import { isCloudDesktop, getEnvVars, getServiceEnvVarConfig, isAmazonLinux2, isBeta } from '../../../shared/vscode/env' +import { + isCloudDesktop, + getEnvVars, + getServiceEnvVarConfig, + isAmazonLinux2, + isBeta, + hasSageMakerEnvVars, +} from '../../../shared/vscode/env' import { ChildProcess } from '../../../shared/utilities/processUtils' import * as sinon from 'sinon' import os from 'os' import fs from '../../../shared/fs/fs' import vscode from 'vscode' import { getComputeEnvType } from '../../../shared/telemetry/util' +import * as globals from '../../../shared/extensionGlobals' describe('env', function () { // create a sinon sandbox instance and instantiate in a beforeEach @@ -97,22 +105,355 @@ describe('env', function () { assert.strictEqual(isBeta(), expected) }) - it('isAmazonLinux2', function () { - sandbox.stub(process, 'platform').value('linux') - const versionStub = stubOsVersion('5.10.220-188.869.amzn2int.x86_64') - assert.strictEqual(isAmazonLinux2(), true) + describe('isAmazonLinux2', function () { + let fsExistsStub: sinon.SinonStub + let fsReadFileStub: sinon.SinonStub + let isWebStub: sinon.SinonStub + let platformStub: sinon.SinonStub + let osReleaseStub: sinon.SinonStub + let moduleLoadStub: sinon.SinonStub + + beforeEach(function () { + // Default stubs + platformStub = sandbox.stub(process, 'platform').value('linux') + osReleaseStub = stubOsVersion('5.10.220-188.869.amzn2int.x86_64') + isWebStub = sandbox.stub(globals, 'isWeb').returns(false) + + // Mock fs module + const fsMock = { + existsSync: sandbox.stub().returns(false), + readFileSync: sandbox.stub().returns(''), + } + fsExistsStub = fsMock.existsSync + fsReadFileStub = fsMock.readFileSync + + // Stub Module._load to intercept require calls + const Module = require('module') + moduleLoadStub = sandbox.stub(Module, '_load').callThrough() + moduleLoadStub.withArgs('fs').returns(fsMock) + }) + + it('returns false in web environment', function () { + isWebStub.returns(true) + assert.strictEqual(isAmazonLinux2(), false) + }) + + it('returns false in SageMaker environment with SAGEMAKER_APP_TYPE', function () { + const originalValue = process.env.SAGEMAKER_APP_TYPE + process.env.SAGEMAKER_APP_TYPE = 'JupyterLab' + try { + assert.strictEqual(isAmazonLinux2(), false) + } finally { + if (originalValue === undefined) { + delete process.env.SAGEMAKER_APP_TYPE + } else { + process.env.SAGEMAKER_APP_TYPE = originalValue + } + } + }) + + it('returns false in SageMaker environment with SM_APP_TYPE', function () { + const originalValue = process.env.SM_APP_TYPE + process.env.SM_APP_TYPE = 'JupyterLab' + try { + assert.strictEqual(isAmazonLinux2(), false) + } finally { + if (originalValue === undefined) { + delete process.env.SM_APP_TYPE + } else { + process.env.SM_APP_TYPE = originalValue + } + } + }) + + it('returns false in SageMaker environment with SERVICE_NAME', function () { + const originalValue = process.env.SERVICE_NAME + process.env.SERVICE_NAME = 'SageMakerUnifiedStudio' + try { + assert.strictEqual(isAmazonLinux2(), false) + } finally { + if (originalValue === undefined) { + delete process.env.SERVICE_NAME + } else { + process.env.SERVICE_NAME = originalValue + } + } + }) + + it('returns false when /etc/os-release indicates Ubuntu in container', function () { + fsExistsStub.returns(true) + fsReadFileStub.returns(` +NAME="Ubuntu" +VERSION="20.04.6 LTS (Focal Fossa)" +ID=ubuntu +ID_LIKE=debian +PRETTY_NAME="Ubuntu 20.04.6 LTS" +VERSION_ID="20.04" + `) + + // Even with AL2 kernel (host is AL2), should return false (container is Ubuntu) + assert.strictEqual(isAmazonLinux2(), false) + }) + + it('returns false when /etc/os-release indicates Amazon Linux 2023', function () { + fsExistsStub.returns(true) + fsReadFileStub.returns(` +NAME="Amazon Linux" +VERSION="2023" +ID="amzn" +ID_LIKE="fedora" +VERSION_ID="2023" +PLATFORM_ID="platform:al2023" +PRETTY_NAME="Amazon Linux 2023" + `) + + assert.strictEqual(isAmazonLinux2(), false) + }) + + it('returns true when /etc/os-release indicates Amazon Linux 2', function () { + fsExistsStub.returns(true) + fsReadFileStub.returns(` +NAME="Amazon Linux 2" +VERSION="2" +ID="amzn" +ID_LIKE="centos rhel fedora" +VERSION_ID="2" +PRETTY_NAME="Amazon Linux 2" + `) + + assert.strictEqual(isAmazonLinux2(), true) + }) + + it('returns true when /etc/os-release has ID="amzn" and VERSION_ID="2"', function () { + fsExistsStub.returns(true) + fsReadFileStub.returns(` +NAME="Amazon Linux" +VERSION="2" +ID="amzn" +VERSION_ID="2" + `) + + assert.strictEqual(isAmazonLinux2(), true) + }) + + it('returns false when /etc/os-release indicates CentOS', function () { + fsExistsStub.returns(true) + fsReadFileStub.returns(` +NAME="CentOS Linux" +VERSION="7 (Core)" +ID="centos" +ID_LIKE="rhel fedora" +VERSION_ID="7" + `) + + // Even with AL2 kernel + assert.strictEqual(isAmazonLinux2(), false) + }) + + it('falls back to kernel check when /etc/os-release does not exist', function () { + fsExistsStub.returns(false) + + // Test with AL2 kernel + assert.strictEqual(isAmazonLinux2(), true) + + // Test with non-AL2 kernel + osReleaseStub.returns('5.10.220-188.869.NOT_INTERNAL.x86_64') + assert.strictEqual(isAmazonLinux2(), false) + }) + + it('falls back to kernel check when /etc/os-release read fails', function () { + fsExistsStub.returns(true) + fsReadFileStub.throws(new Error('Permission denied')) + + // Should fall back to kernel check + assert.strictEqual(isAmazonLinux2(), true) + }) + + it('returns true with .amzn2. kernel pattern', function () { + fsExistsStub.returns(false) + osReleaseStub.returns('5.10.236-227.928.amzn2.x86_64') + assert.strictEqual(isAmazonLinux2(), true) + }) + + it('returns true with .amzn2int. kernel pattern', function () { + fsExistsStub.returns(false) + osReleaseStub.returns('5.10.220-188.869.amzn2int.x86_64') + assert.strictEqual(isAmazonLinux2(), true) + }) + + it('returns false with non-AL2 kernel', function () { + fsExistsStub.returns(false) + osReleaseStub.returns('5.15.0-91-generic') + assert.strictEqual(isAmazonLinux2(), false) + }) + + it('returns false on non-Linux platforms', function () { + platformStub.value('darwin') + fsExistsStub.returns(false) + assert.strictEqual(isAmazonLinux2(), false) + + platformStub.value('win32') + assert.strictEqual(isAmazonLinux2(), false) + }) - versionStub.returns('5.10.236-227.928.amzn2.x86_64') - assert.strictEqual(isAmazonLinux2(), true) + it('returns false when container OS is different from host OS', function () { + // Scenario: Host is AL2 (kernel shows AL2) but container is Ubuntu + fsExistsStub.returns(true) + fsReadFileStub.returns(` +NAME="Ubuntu" +VERSION="22.04" +ID=ubuntu +VERSION_ID="22.04" + `) + osReleaseStub.returns('5.10.220-188.869.amzn2int.x86_64') // AL2 kernel from host - versionStub.returns('5.10.220-188.869.NOT_INTERNAL.x86_64') - assert.strictEqual(isAmazonLinux2(), false) + // Should trust container OS over kernel + assert.strictEqual(isAmazonLinux2(), false) + }) + + it('handles os-release with comments correctly', function () { + fsExistsStub.returns(true) + fsReadFileStub.returns(` +# This is a comment with VERSION_ID="2023" that should be ignored +NAME="Amazon Linux 2" +VERSION="2" +ID="amzn" +# Another comment with PLATFORM_ID="platform:al2023" +VERSION_ID="2" +PRETTY_NAME="Amazon Linux 2" + `) + + // Should correctly identify as AL2 despite comments containing AL2023 identifiers + assert.strictEqual(isAmazonLinux2(), true) + }) + + it('handles os-release with quoted values correctly', function () { + fsExistsStub.returns(true) + fsReadFileStub.returns(` +NAME="Amazon Linux 2" +VERSION='2' +ID=amzn +VERSION_ID="2" +PRETTY_NAME='Amazon Linux 2' + `) + + // Should correctly parse both single and double quoted values + assert.strictEqual(isAmazonLinux2(), true) + }) + + it('handles os-release with empty lines and whitespace', function () { + fsExistsStub.returns(true) + fsReadFileStub.returns(` + +NAME="Amazon Linux 2" + +VERSION="2" + ID="amzn" +VERSION_ID="2" + +PRETTY_NAME="Amazon Linux 2" + + `) + + // Should correctly parse despite empty lines and whitespace + assert.strictEqual(isAmazonLinux2(), true) + }) + + it('rejects Amazon Linux 2023 even with misleading comments', function () { + fsExistsStub.returns(true) + fsReadFileStub.returns(` +# This comment mentions Amazon Linux 2 but should not affect parsing +NAME="Amazon Linux" +VERSION="2023" +ID="amzn" +# Comment with VERSION_ID="2" should be ignored +VERSION_ID="2023" +PLATFORM_ID="platform:al2023" +PRETTY_NAME="Amazon Linux 2023" + `) + + // Should correctly identify as AL2023 (not AL2) despite misleading comments + assert.strictEqual(isAmazonLinux2(), false) + }) + + it('handles malformed os-release lines gracefully', function () { + fsExistsStub.returns(true) + fsReadFileStub.returns(` +NAME="Amazon Linux 2" +VERSION="2" +ID="amzn" +INVALID_LINE_WITHOUT_EQUALS +=INVALID_LINE_STARTING_WITH_EQUALS +VERSION_ID="2" +PRETTY_NAME="Amazon Linux 2" + `) + + // Should correctly parse valid lines and ignore malformed ones + assert.strictEqual(isAmazonLinux2(), true) + }) + }) + + describe('hasSageMakerEnvVars', function () { + afterEach(function () { + // Clean up environment variables + delete process.env.SAGEMAKER_APP_TYPE + delete process.env.SAGEMAKER_INTERNAL_IMAGE_URI + delete process.env.STUDIO_LOGGING_DIR + delete process.env.SM_APP_TYPE + delete process.env.SM_INTERNAL_IMAGE_URI + delete process.env.SERVICE_NAME + }) + + it('returns true when SAGEMAKER_APP_TYPE is set', function () { + process.env.SAGEMAKER_APP_TYPE = 'JupyterLab' + assert.strictEqual(hasSageMakerEnvVars(), true) + }) + + it('returns true when SM_APP_TYPE is set', function () { + process.env.SM_APP_TYPE = 'JupyterLab' + assert.strictEqual(hasSageMakerEnvVars(), true) + }) + + it('returns true when SERVICE_NAME is SageMakerUnifiedStudio', function () { + process.env.SERVICE_NAME = 'SageMakerUnifiedStudio' + assert.strictEqual(hasSageMakerEnvVars(), true) + }) + + it('returns true when STUDIO_LOGGING_DIR contains /var/log/studio', function () { + process.env.STUDIO_LOGGING_DIR = '/var/log/studio/logs' + assert.strictEqual(hasSageMakerEnvVars(), true) + }) + + it('returns false when no SageMaker env vars are set', function () { + assert.strictEqual(hasSageMakerEnvVars(), false) + }) + + it('returns false when SERVICE_NAME is set but not SageMakerUnifiedStudio', function () { + process.env.SERVICE_NAME = 'SomeOtherService' + assert.strictEqual(hasSageMakerEnvVars(), false) + }) }) it('isCloudDesktop', async function () { + // Mock fs module for isAmazonLinux2() calls + const fsMock = { + existsSync: sandbox.stub().returns(false), + readFileSync: sandbox.stub().returns(''), + } + const fsExistsStub = fsMock.existsSync + + // Stub Module._load to intercept require calls + const Module = require('module') + const moduleLoadStub = sandbox.stub(Module, '_load').callThrough() + moduleLoadStub.withArgs('fs').returns(fsMock) + sandbox.stub(process, 'platform').value('linux') + sandbox.stub(globals, 'isWeb').returns(false) stubOsVersion('5.10.220-188.869.amzn2int.x86_64') + // Mock fs to return false so it falls back to kernel check (which should return true for AL2) + fsExistsStub.returns(false) + const runStub = sandbox.stub(ChildProcess.prototype, 'run').resolves({ exitCode: 0 } as any) assert.strictEqual(await isCloudDesktop(), true) @@ -121,29 +462,58 @@ describe('env', function () { }) describe('getComputeEnvType', async function () { + let fsExistsStub: sinon.SinonStub + let moduleLoadStub: sinon.SinonStub + + beforeEach(function () { + // Mock fs module for isAmazonLinux2() calls + const fsMock = { + existsSync: sandbox.stub().returns(false), + readFileSync: sandbox.stub().returns(''), + } + fsExistsStub = fsMock.existsSync + + // Stub Module._load to intercept require calls + const Module = require('module') + moduleLoadStub = sandbox.stub(Module, '_load').callThrough() + moduleLoadStub.withArgs('fs').returns(fsMock) + }) + it('cloudDesktop', async function () { sandbox.stub(process, 'platform').value('linux') sandbox.stub(vscode.env, 'remoteName').value('ssh-remote') + sandbox.stub(globals, 'isWeb').returns(false) stubOsVersion('5.10.220-188.869.amzn2int.x86_64') sandbox.stub(ChildProcess.prototype, 'run').resolves({ exitCode: 0 } as any) + // Mock fs to return false so it falls back to kernel check (which should return true for AL2) + fsExistsStub.returns(false) + assert.deepStrictEqual(await getComputeEnvType(), 'cloudDesktop-amzn') }) it('ec2-internal', async function () { sandbox.stub(process, 'platform').value('linux') sandbox.stub(vscode.env, 'remoteName').value('ssh-remote') + sandbox.stub(globals, 'isWeb').returns(false) stubOsVersion('5.10.220-188.869.amzn2int.x86_64') sandbox.stub(ChildProcess.prototype, 'run').resolves({ exitCode: 1 } as any) + // Mock fs to return false so it falls back to kernel check (which should return true for AL2) + fsExistsStub.returns(false) + assert.deepStrictEqual(await getComputeEnvType(), 'ec2-amzn') }) it('ec2', async function () { sandbox.stub(process, 'platform').value('linux') sandbox.stub(vscode.env, 'remoteName').value('ssh-remote') + sandbox.stub(globals, 'isWeb').returns(false) stubOsVersion('5.10.220-188.869.NOT_INTERNAL.x86_64') + // Mock fs to return false so it falls back to kernel check (which should return false for non-AL2) + fsExistsStub.returns(false) + assert.deepStrictEqual(await getComputeEnvType(), 'ec2') }) }) diff --git a/packages/core/src/test/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/codewhisperer/referenceTracker.test.ts b/packages/core/src/testE2E/codewhisperer/referenceTracker.test.ts new file mode 100644 index 00000000000..d173500c608 --- /dev/null +++ b/packages/core/src/testE2E/codewhisperer/referenceTracker.test.ts @@ -0,0 +1,124 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as codewhispererClient from '../../codewhisperer/client/codewhisperer' +import { ConfigurationEntry } from '../../codewhisperer/models/model' +import { setValidConnection, skipTestIfNoValidConn } from '../util/connection' +import { RecommendationHandler } from '../../codewhisperer/service/recommendationHandler' +import { createMockTextEditor, resetCodeWhispererGlobalVariables } from '../../test/codewhisperer/testUtil' +import { invokeRecommendation } from '../../codewhisperer/commands/invokeRecommendation' +import { session } from '../../codewhisperer/util/codeWhispererSession' + +/* +New model deployment may impact references returned. +These tests: + 1) are not required for github approval flow + 2) will be auto-skipped until fix for manual runs is posted. +*/ + +const leftContext = `InAuto.GetContent( + InAuto.servers.auto, "vendors.json", + function (data) { + let block = ''; + for(let i = 0; i < data.length; i++) { + block += '' + cars[i].title + ''; + } + $('#cars').html(block); + });` + +describe('CodeWhisperer service invocation', async function () { + let validConnection: boolean + const client = new codewhispererClient.DefaultCodeWhispererClient() + const configWithRefs: ConfigurationEntry = { + isShowMethodsEnabled: true, + isManualTriggerEnabled: true, + isAutomatedTriggerEnabled: true, + isSuggestionsWithCodeReferencesEnabled: true, + } + const configWithNoRefs: ConfigurationEntry = { + isShowMethodsEnabled: true, + isManualTriggerEnabled: true, + isAutomatedTriggerEnabled: true, + isSuggestionsWithCodeReferencesEnabled: false, + } + + before(async function () { + validConnection = await setValidConnection() + }) + + beforeEach(function () { + void resetCodeWhispererGlobalVariables() + RecommendationHandler.instance.clearRecommendations() + // TODO: remove this line (this.skip()) when these tests no longer auto-skipped + this.skip() + // valid connection required to run tests + skipTestIfNoValidConn(validConnection, this) + }) + + it('trigger known to return recs with references returns rec with reference', async function () { + // check that handler is empty before invocation + const requestIdBefore = RecommendationHandler.instance.requestId + const sessionIdBefore = session.sessionId + const validRecsBefore = RecommendationHandler.instance.isValidResponse() + + assert.ok(requestIdBefore.length === 0) + assert.ok(sessionIdBefore.length === 0) + assert.ok(!validRecsBefore) + + const doc = leftContext + rightContext + const filename = 'test.js' + const language = 'javascript' + const line = 5 + const character = 39 + const mockEditor = createMockTextEditor(doc, filename, language, line, character) + + await invokeRecommendation(mockEditor, client, configWithRefs) + + const requestId = RecommendationHandler.instance.requestId + const sessionId = session.sessionId + const validRecs = RecommendationHandler.instance.isValidResponse() + const references = session.recommendations[0].references + + assert.ok(requestId.length > 0) + assert.ok(sessionId.length > 0) + assert.ok(validRecs) + assert.ok(references !== undefined) + // TODO: uncomment this assert when this test is no longer auto-skipped + // assert.ok(references.length > 0) + }) + + // This test will fail if user is logged in with IAM identity center + it('trigger known to return rec with references does not return rec with references when reference tracker setting is off', async function () { + // check that handler is empty before invocation + const requestIdBefore = RecommendationHandler.instance.requestId + const sessionIdBefore = session.sessionId + const validRecsBefore = RecommendationHandler.instance.isValidResponse() + + assert.ok(requestIdBefore.length === 0) + assert.ok(sessionIdBefore.length === 0) + assert.ok(!validRecsBefore) + + const doc = leftContext + rightContext + const filename = 'test.js' + const language = 'javascript' + const line = 5 + const character = 39 + const mockEditor = createMockTextEditor(doc, filename, language, line, character) + + await invokeRecommendation(mockEditor, client, configWithNoRefs) + + const requestId = RecommendationHandler.instance.requestId + const sessionId = session.sessionId + const validRecs = RecommendationHandler.instance.isValidResponse() + + assert.ok(requestId.length > 0) + assert.ok(sessionId.length > 0) + // no recs returned because example request returns 1 rec with reference, so no recs returned when references off + assert.ok(!validRecs) + }) +}) diff --git a/packages/core/src/testE2E/codewhisperer/serviceInvocations.test.ts b/packages/core/src/testE2E/codewhisperer/serviceInvocations.test.ts new file mode 100644 index 00000000000..37f32b130dd --- /dev/null +++ b/packages/core/src/testE2E/codewhisperer/serviceInvocations.test.ts @@ -0,0 +1,124 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as vscode from 'vscode' +import * as path from 'path' +import { setValidConnection, skipTestIfNoValidConn } from '../util/connection' +import { ConfigurationEntry } from '../../codewhisperer/models/model' +import * as codewhispererClient from '../../codewhisperer/client/codewhisperer' +import { RecommendationHandler } from '../../codewhisperer/service/recommendationHandler' +import { + createMockTextEditor, + createTextDocumentChangeEvent, + resetCodeWhispererGlobalVariables, +} from '../../test/codewhisperer/testUtil' +import { KeyStrokeHandler } from '../../codewhisperer/service/keyStrokeHandler' +import { sleep } from '../../shared/utilities/timeoutUtils' +import { invokeRecommendation } from '../../codewhisperer/commands/invokeRecommendation' +import { getTestWorkspaceFolder } from '../../testInteg/integrationTestsUtilities' +import { session } from '../../codewhisperer/util/codeWhispererSession' + +describe('CodeWhisperer service invocation', async function () { + let validConnection: boolean + const client = new codewhispererClient.DefaultCodeWhispererClient() + const config: ConfigurationEntry = { + isShowMethodsEnabled: true, + isManualTriggerEnabled: true, + isAutomatedTriggerEnabled: true, + isSuggestionsWithCodeReferencesEnabled: true, + } + + before(async function () { + validConnection = await setValidConnection() + }) + + beforeEach(function () { + void resetCodeWhispererGlobalVariables() + RecommendationHandler.instance.clearRecommendations() + // valid connection required to run tests + skipTestIfNoValidConn(validConnection, this) + }) + + it('manual trigger returns valid recommendation response', async function () { + // check that handler is empty before invocation + const requestIdBefore = RecommendationHandler.instance.requestId + const sessionIdBefore = session.sessionId + const validRecsBefore = RecommendationHandler.instance.isValidResponse() + + assert.ok(requestIdBefore.length === 0) + assert.ok(sessionIdBefore.length === 0) + assert.ok(!validRecsBefore) + + const mockEditor = createMockTextEditor() + await invokeRecommendation(mockEditor, client, config) + + const requestId = RecommendationHandler.instance.requestId + const sessionId = session.sessionId + const validRecs = RecommendationHandler.instance.isValidResponse() + + assert.ok(requestId.length > 0) + assert.ok(sessionId.length > 0) + assert.ok(validRecs) + }) + + it('auto trigger returns valid recommendation response', async function () { + // check that handler is empty before invocation + const requestIdBefore = RecommendationHandler.instance.requestId + const sessionIdBefore = session.sessionId + const validRecsBefore = RecommendationHandler.instance.isValidResponse() + + assert.ok(requestIdBefore.length === 0) + assert.ok(sessionIdBefore.length === 0) + assert.ok(!validRecsBefore) + + const mockEditor = createMockTextEditor() + + const mockEvent: vscode.TextDocumentChangeEvent = createTextDocumentChangeEvent( + mockEditor.document, + new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)), + '\n' + ) + + await KeyStrokeHandler.instance.processKeyStroke(mockEvent, mockEditor, client, config) + // wait for 5 seconds to allow time for response to be generated + await sleep(5000) + + const requestId = RecommendationHandler.instance.requestId + const sessionId = session.sessionId + const validRecs = RecommendationHandler.instance.isValidResponse() + + assert.ok(requestId.length > 0) + assert.ok(sessionId.length > 0) + assert.ok(validRecs) + }) + + it('invocation in unsupported language does not generate a request', async function () { + const workspaceFolder = getTestWorkspaceFolder() + const appRoot = path.join(workspaceFolder, 'go1-plain-sam-app') + const appCodePath = path.join(appRoot, 'hello-world', 'go.mod') + + // check that handler is empty before invocation + const requestIdBefore = RecommendationHandler.instance.requestId + const sessionIdBefore = session.sessionId + const validRecsBefore = RecommendationHandler.instance.isValidResponse() + + assert.ok(requestIdBefore.length === 0) + assert.ok(sessionIdBefore.length === 0) + assert.ok(!validRecsBefore) + + const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(appCodePath)) + const editor = await vscode.window.showTextDocument(doc) + await invokeRecommendation(editor, client, config) + + const requestId = RecommendationHandler.instance.requestId + const sessionId = session.sessionId + const validRecs = RecommendationHandler.instance.isValidResponse() + + assert.ok(requestId.length === 0) + assert.ok(sessionId.length === 0) + assert.ok(!validRecs) + }) +}) diff --git a/packages/core/src/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/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.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/CHANGELOG.md b/packages/toolkit/CHANGELOG.md index a4592dd068d..6def23f3765 100644 --- a/packages/toolkit/CHANGELOG.md +++ b/packages/toolkit/CHANGELOG.md @@ -1,3 +1,61 @@ +## 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 diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index e7989468a7d..fd04f40ecea 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.68.0-SNAPSHOT", + "version": "3.79.0-SNAPSHOT", "extensionKind": [ "workspace" ], @@ -299,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." } } }, @@ -774,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", @@ -826,6 +836,10 @@ "command": "aws.downloadStateMachineDefinition", "when": "false" }, + { + "command": "aws.toolkit.lambda.convertToSam", + "when": "false" + }, { "command": "aws.ecr.createRepository", "when": "false" @@ -1244,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": [ @@ -1306,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", @@ -1447,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", @@ -1605,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", @@ -1657,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" }, { @@ -1775,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", @@ -1827,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" }, { @@ -2105,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", @@ -2126,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", @@ -2157,7 +2268,7 @@ }, { "command": "aws.appBuilder.openHandler", - "when": "viewItem == awsAppBuilderResourceNode.function", + "when": "viewItem == awsAppBuilderResourceNode.function|| viewItem == awsAppBuilderResourceNode.deployed-function", "group": "1@1" }, { @@ -2167,7 +2278,7 @@ }, { "command": "aws.launchDebugConfigForm", - "when": "viewItem == awsAppBuilderResourceNode.function", + "when": "viewItem == awsAppBuilderResourceNode.function || viewItem == awsAppBuilderResourceNode.deployed-function", "group": "1@2" }, { @@ -2182,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", @@ -2276,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" } ], @@ -2336,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%", @@ -2565,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%", @@ -3004,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%", @@ -3017,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%" @@ -3037,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", @@ -4148,6 +4429,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": [ @@ -4576,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/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 } }