diff --git a/.changeset/forty-buckets-visit.md b/.changeset/forty-buckets-visit.md deleted file mode 100644 index 4f19c5fee7..0000000000 --- a/.changeset/forty-buckets-visit.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@aws-amplify/deployed-backend-client': patch -'@aws-amplify/backend-deployer': patch -'@aws-amplify/backend-function': patch -'@aws-amplify/schema-generator': patch -'@aws-amplify/backend-storage': patch -'@aws-amplify/model-generator': patch -'@aws-amplify/auth-construct': patch -'@aws-amplify/backend-secret': patch -'create-amplify': patch -'@aws-amplify/form-generator': patch -'@aws-amplify/client-config': patch -'@aws-amplify/backend-auth': patch -'@aws-amplify/backend-data': patch -'@aws-amplify/backend': patch -'@aws-amplify/sandbox': patch -'ampx': patch -'@aws-amplify/backend-cli': patch ---- - -added main field to package.json so these packages are resolvable diff --git a/.changeset/fresh-dancers-peel.md b/.changeset/fresh-dancers-peel.md deleted file mode 100644 index 4b78eee2c5..0000000000 --- a/.changeset/fresh-dancers-peel.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@aws-amplify/backend-secret': patch -'@aws-amplify/sandbox': patch ---- - -add ExpiredToken in the list of credentials error diff --git a/.changeset/great-otters-study.md b/.changeset/great-otters-study.md new file mode 100644 index 0000000000..67954f92ca --- /dev/null +++ b/.changeset/great-otters-study.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend-deployer': patch +--- + +add more forms of transform errors to cdk error mapping diff --git a/.changeset/orange-garlics-reflect.md b/.changeset/orange-garlics-reflect.md new file mode 100644 index 0000000000..1be521b152 --- /dev/null +++ b/.changeset/orange-garlics-reflect.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/platform-core': patch +--- + +Handle insufficient disk space errors diff --git a/.changeset/rude-wasps-look.md b/.changeset/rude-wasps-look.md new file mode 100644 index 0000000000..dee6f59b71 --- /dev/null +++ b/.changeset/rude-wasps-look.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend-deployer': patch +--- + +truncate large error messages before printing to customer diff --git a/.changeset/slimy-sheep-wonder.md b/.changeset/slimy-sheep-wonder.md new file mode 100644 index 0000000000..1c79787ba6 --- /dev/null +++ b/.changeset/slimy-sheep-wonder.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend-deployer': patch +--- + +handle not authorized to perform on resource error diff --git a/.changeset/stale-worms-pretend.md b/.changeset/stale-worms-pretend.md deleted file mode 100644 index a845151cc8..0000000000 --- a/.changeset/stale-worms-pretend.md +++ /dev/null @@ -1,2 +0,0 @@ ---- ---- diff --git a/.changeset/strange-jobs-refuse.md b/.changeset/strange-jobs-refuse.md deleted file mode 100644 index e7ee1c9d47..0000000000 --- a/.changeset/strange-jobs-refuse.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@aws-amplify/backend-platform-test-stubs': patch -'@aws-amplify/deployed-backend-client': patch -'@aws-amplify/backend-output-storage': patch -'@aws-amplify/integration-tests': patch -'@aws-amplify/model-generator': patch -'@aws-amplify/client-config': patch -'@aws-amplify/plugin-types': patch -'@aws-amplify/cli-core': patch -'@aws-amplify/sandbox': patch -'@aws-amplify/backend-cli': patch ---- - -fixed errors in plugin-types and cli-core along with any extraneous dependencies in other packages diff --git a/.changeset/warm-garlics-raise.md b/.changeset/warm-garlics-raise.md new file mode 100644 index 0000000000..f30209ff8e --- /dev/null +++ b/.changeset/warm-garlics-raise.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend-deployer': patch +--- + +Handle invalid package.json error diff --git a/.changeset/yellow-jokes-kick.md b/.changeset/yellow-jokes-kick.md deleted file mode 100644 index 83fd0a01b4..0000000000 --- a/.changeset/yellow-jokes-kick.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@aws-amplify/deployed-backend-client': patch -'@aws-amplify/backend-deployer': patch -'@aws-amplify/schema-generator': patch -'@aws-amplify/model-generator': patch -'@aws-amplify/backend-secret': patch -'@aws-amplify/form-generator': patch -'@aws-amplify/client-config': patch -'@aws-amplify/sandbox': patch ---- - -added main field to packages known to lack one diff --git a/.eslint_dictionary.json b/.eslint_dictionary.json index ff69a3796a..4f0f494eee 100644 --- a/.eslint_dictionary.json +++ b/.eslint_dictionary.json @@ -14,10 +14,12 @@ "argv", "arn", "arns", + "aws", "backends", "birthdate", "bundler", "callee", + "cartesian", "cdk", "changelog", "changeset", @@ -38,6 +40,7 @@ "datasync", "debounce", "declarator", + "decrypt", "deployer", "deprecations", "deprecator", @@ -78,6 +81,7 @@ "idps", "implementors", "inheritdoc", + "instanceof", "interop", "invokable", "invoker", @@ -102,6 +106,7 @@ "mysql", "namespace", "namespaces", + "netstat", "nodejs", "nodenext", "nodir", @@ -131,6 +136,7 @@ "renderer", "repo", "resolvers", + "retryable", "saml", "scala", "schema", @@ -143,6 +149,7 @@ "sigint", "signout", "signup", + "SKey", "sms", "stderr", "stdin", @@ -156,6 +163,7 @@ "synthing", "testname", "testnamebucket", + "testuser", "timestamps", "tmpdir", "todos", @@ -168,6 +176,7 @@ "tslint", "typename", "typeof", + "ubuntu", "unauth", "unix", "unlink", @@ -186,6 +195,7 @@ "wildcards", "workspace", "writev", + "xlarge", "yaml", "yargs", "zoneinfo" diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 05f9f0a155..23e09de517 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -160,6 +160,7 @@ module.exports = { }, ], 'jsdoc/require-param': 'off', + 'jsdoc/require-yields': 'off', 'jsdoc/require-returns': 'off', 'spellcheck/spell-checker': [ 'warn', diff --git a/.github/actions/setup_baseline_version/action.yml b/.github/actions/setup_baseline_version/action.yml new file mode 100644 index 0000000000..1594ab6dde --- /dev/null +++ b/.github/actions/setup_baseline_version/action.yml @@ -0,0 +1,52 @@ +name: setup_baseline_version +description: Set up a baseline or "previous" version of the library for testing. Mostly useful for backwards compatibility +outputs: + baseline_dir: + description: 'Path where baseline project directory is setup' + value: ${{ steps.move_baseline_version.outputs.baseline_dir }} +runs: + using: composite + steps: + - name: Get baseline commit sha + id: get_baseline_commit_sha + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + if [[ ${{ github.event_name }} == 'push' ]]; then + # The SHA of the most recent commit on ref before the push. + baseline_commit_sha="${{ github.event.before }}" + elif [[ ${{ github.event_name }} == 'pull_request' ]]; then + # The SHA of the HEAD commit on base branch. + baseline_commit_sha="${{ github.event.pull_request.base.sha }}" + elif [[ ${{ github.event_name }} == 'schedule' ]] || [[ ${{ github.event_name }} == 'workflow_dispatch' ]]; then + # The SHA of the parent of HEAD commit on main branch. + # This assumes linear history of main branch, i.e. one parent. + # These events have only information about HEAD commit, hence the need for lookup. + baseline_commit_sha=$(gh api /repos/${{ github.repository }}/commits/${{ github.sha }} | jq -r '.parents[0].sha') + else + echo Unable to determine baseline commit sha; + exit 1; + fi + echo baseline commit sha is $baseline_commit_sha; + echo "baseline_commit_sha=$baseline_commit_sha" >> "$GITHUB_OUTPUT"; + - name: Checkout baseline version + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 + with: + ref: ${{ steps.get_baseline_commit_sha.outputs.baseline_commit_sha }} + - uses: ./.github/actions/setup_node + - name: Install and build baseline version + shell: bash + run: | + npm ci + npm run build + - name: Move baseline version + id: move_baseline_version + shell: bash + run: | + BASELINE_DIR=$(mktemp -d) + # Command below makes shell include .hidden files in file system commands (i.e. mv). + # This is to make sure that .git directory is moved with the repo content. + shopt -s dotglob + mv ./* $BASELINE_DIR + echo "baseline_dir=$BASELINE_DIR" >> "$GITHUB_OUTPUT"; diff --git a/.github/actions/setup_node/action.yml b/.github/actions/setup_node/action.yml index 6bda314a87..8f407ee437 100644 --- a/.github/actions/setup_node/action.yml +++ b/.github/actions/setup_node/action.yml @@ -12,3 +12,10 @@ runs: with: node-version: ${{ inputs.node-version }} cache: 'npm' + - name: Hydrate npx cache + # This step hydrates npx cache with packages that we use in builds and tests upfront. + # Otherwise, concurrent attempt to use these tools with cache miss results in race conditions between + # two installations. That may result in corrupted npx cache. + shell: bash + run: | + npx which npx diff --git a/.github/actions/setup_profile/action.yml b/.github/actions/setup_profile/action.yml index 4c86490af3..e7ab8be9f7 100644 --- a/.github/actions/setup_profile/action.yml +++ b/.github/actions/setup_profile/action.yml @@ -19,8 +19,15 @@ runs: with: role-to-assume: ${{ inputs.role-to-assume }} aws-region: ${{ inputs.aws-region }} - output-credentials: true # places the credentials in the GH context object rather than setting env vars - # the AWS credentials action does not have an option to configure a profile, so this manually configures one + # Credentials with special characters are not handled correctly on Windows + # when put into profile files. This forces action to retry until credentials without special characters + # are retrieved + # See: https://github.com/aws-actions/configure-aws-credentials/issues/599 + # and https://github.com/aws-actions/configure-aws-credentials/issues/528 + special-characters-workaround: ${{ contains(runner.os, 'Windows') }} + # places the credentials in the GH context object rather than setting env vars + # the AWS credentials action does not have an option to configure a profile, so this manually configures one + output-credentials: true - shell: bash run: | aws configure set aws_access_key_id ${{ steps.credentials.outputs.aws-access-key-id }} --profile ${{ inputs.profile-name }} diff --git a/.github/workflows/health_checks.yml b/.github/workflows/health_checks.yml index c950c12f45..fef7c581b8 100644 --- a/.github/workflows/health_checks.yml +++ b/.github/workflows/health_checks.yml @@ -160,46 +160,13 @@ jobs: id-token: write contents: read steps: - - name: Get baseline commit sha - id: get_baseline_commit_sha - env: - GH_TOKEN: ${{ github.token }} - run: | - if [[ ${{ github.event_name }} == 'push' ]]; then - # The SHA of the most recent commit on ref before the push. - baseline_commit_sha="${{ github.event.before }}" - elif [[ ${{ github.event_name }} == 'pull_request' ]]; then - # The SHA of the HEAD commit on base branch. - baseline_commit_sha="${{ github.event.pull_request.base.sha }}" - elif [[ ${{ github.event_name }} == 'schedule' ]] || [[ ${{ github.event_name }} == 'workflow_dispatch' ]]; then - # The SHA of the parent of HEAD commit on main branch. - # This assumes linear history of main branch, i.e. one parent. - # These events have only information about HEAD commit, hence the need for lookup. - baseline_commit_sha=$(gh api /repos/${{ github.repository }}/commits/${{ github.sha }} | jq -r '.parents[0].sha') - else - echo Unable to determine baseline commit sha; - exit 1; - fi - echo baseline commit sha is $baseline_commit_sha; - echo "baseline_commit_sha=$baseline_commit_sha" >> "$GITHUB_OUTPUT"; - - name: Checkout baseline version + # This checkout is needed for the setup_baseline_version action to run `checkout` inside + # See https://github.com/actions/checkout/issues/692 + - name: Checkout version for baseline uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - with: - ref: ${{ steps.get_baseline_commit_sha.outputs.baseline_commit_sha }} - - uses: ./.github/actions/setup_node - - name: Install and build baseline version - run: | - npm ci - npm run build - - name: Move baseline version - id: move_baseline_version - run: | - BASELINE_DIR=$(mktemp -d) - # Command below makes shell include .hidden files in file system commands (i.e. mv). - # This is to make sure that .git directory is moved with the repo content. - shopt -s dotglob - mv ./* $BASELINE_DIR - echo "baseline_dir=$BASELINE_DIR" >> "$GITHUB_OUTPUT"; + - name: Setup baseline version + uses: ./.github/actions/setup_baseline_version + id: setup_baseline_version - name: Checkout current version uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - name: Run e2e iam access drift test @@ -209,26 +176,64 @@ jobs: node_version: ${{ matrix.node-version }} run: npm run test:dir packages/integration-tests/lib/test-e2e/iam_access_drift.test.js env: - BASELINE_DIR: ${{ steps.move_baseline_version.outputs.baseline_dir }} + BASELINE_DIR: ${{ steps.setup_baseline_version.outputs.baseline_dir }} + e2e_amplify_outputs_backwards_compatibility: + if: needs.do_include_e2e.outputs.run_e2e == 'true' + runs-on: ubuntu-latest + timeout-minutes: 25 + needs: + - do_include_e2e + - build + permissions: + # these permissions are required for the configure-aws-credentials action to get a JWT from GitHub + id-token: write + contents: read + steps: + # This checkout is needed for the setup_baseline_version action to run `checkout` inside + # See https://github.com/actions/checkout/issues/692 + - name: Checkout version for baseline + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 + - name: Setup baseline version + uses: ./.github/actions/setup_baseline_version + id: setup_baseline_version + - name: Checkout current version + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 + - name: Run e2e amplify outputs backwards compatibility test + uses: ./.github/actions/run_with_e2e_account + with: + e2e_test_accounts: ${{ vars.E2E_TEST_ACCOUNTS }} + node_version: ${{ matrix.node-version }} + run: npm run test:dir packages/integration-tests/lib/test-e2e/amplify_outputs_backwards_compatibility.test.js + env: + BASELINE_DIR: ${{ steps.setup_baseline_version.outputs.baseline_dir }} + e2e_generate_deployment_tests_matrix: + if: needs.do_include_e2e.outputs.run_e2e == 'true' + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.generateMatrix.outputs.matrix }} + timeout-minutes: 5 + needs: + - do_include_e2e + - build + steps: + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 + - uses: ./.github/actions/restore_build_cache + - run: echo "$(npx tsx scripts/generate_sparse_test_matrix.ts 'packages/integration-tests/lib/test-e2e/deployment/*.deployment.test.js')" + - id: generateMatrix + run: echo "matrix=$(npx tsx scripts/generate_sparse_test_matrix.ts 'packages/integration-tests/lib/test-e2e/deployment/*.deployment.test.js')" >> "$GITHUB_OUTPUT" e2e_deployment: if: needs.do_include_e2e.outputs.run_e2e == 'true' strategy: # will finish running other test matrices even if one fails fail-fast: false - matrix: - os: [ubuntu-latest, macos-14-xlarge, windows-latest] - node-version: [18, 20] - # skip multiple node version test on other os - exclude: - - os: macos-14-xlarge - node-version: 20 - - os: windows-latest - node-version: 20 + matrix: ${{ fromJson(needs.e2e_generate_deployment_tests_matrix.outputs.matrix) }} runs-on: ${{ matrix.os }} + name: e2e_deployment ${{ matrix.displayNames }} ${{ matrix.node-version }} ${{ matrix.os }} timeout-minutes: ${{ matrix.os == 'windows-latest' && 35 || 25 }} needs: - do_include_e2e - build + - e2e_generate_deployment_tests_matrix permissions: # these permissions are required for the configure-aws-credentials action to get a JWT from GitHub id-token: write @@ -242,26 +247,35 @@ jobs: node_version: ${{ matrix.node-version }} link_cli: true run: | - npm run test:dir packages/integration-tests/lib/test-e2e/deployment.test.js + npm run test:dir ${{ matrix.testPaths }} + e2e_generate_sandbox_tests_matrix: + if: needs.do_include_e2e.outputs.run_e2e == 'true' + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.generateMatrix.outputs.matrix }} + timeout-minutes: 5 + needs: + - do_include_e2e + - build + steps: + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 + - uses: ./.github/actions/restore_build_cache + - run: echo "$(npx tsx scripts/generate_sparse_test_matrix.ts 'packages/integration-tests/lib/test-e2e/sandbox/*.sandbox.test.js')" + - id: generateMatrix + run: echo "matrix=$(npx tsx scripts/generate_sparse_test_matrix.ts 'packages/integration-tests/lib/test-e2e/sandbox/*.sandbox.test.js')" >> "$GITHUB_OUTPUT" e2e_sandbox: if: needs.do_include_e2e.outputs.run_e2e == 'true' strategy: # will finish running other test matrices even if one fails fail-fast: false - matrix: - os: [ubuntu-latest, macos-14-xlarge, windows-latest] - node-version: [18, 20] - # skip multiple node version test on other os - exclude: - - os: macos-14-xlarge - node-version: 20 - - os: windows-latest - node-version: 20 + matrix: ${{ fromJson(needs.e2e_generate_sandbox_tests_matrix.outputs.matrix) }} runs-on: ${{ matrix.os }} + name: e2e_sandbox ${{ matrix.displayNames }} ${{ matrix.node-version }} ${{ matrix.os }} timeout-minutes: ${{ matrix.os == 'windows-latest' && 35 || 25 }} needs: - do_include_e2e - build + - e2e_generate_sandbox_tests_matrix permissions: # these permissions are required for the configure-aws-credentials action to get a JWT from GitHub id-token: write @@ -274,7 +288,7 @@ jobs: e2e_test_accounts: ${{ vars.E2E_TEST_ACCOUNTS }} node_version: ${{ matrix.node-version }} link_cli: true - run: npm run test:dir packages/integration-tests/lib/test-e2e/sandbox.test.js + run: npm run test:dir ${{ matrix.testPaths }} e2e_backend_output: if: needs.do_include_e2e.outputs.run_e2e == 'true' runs-on: ubuntu-latest @@ -415,7 +429,9 @@ jobs: - uses: ./.github/actions/setup_node - uses: ./.github/actions/restore_install_cache - run: git fetch origin - - run: npm run diff:check ${{ github.event.pull_request.base.sha }} + - run: npm run diff:check "$BASE_SHA" + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} check_pr_changesets: if: github.event_name == 'pull_request' && github.event.pull_request.user.login != 'github-actions[bot]' runs-on: ubuntu-latest @@ -429,9 +445,13 @@ jobs: - uses: ./.github/actions/setup_node - uses: ./.github/actions/restore_install_cache - name: Validate that PR has changeset - run: npx changeset status --since origin/${{ github.event.pull_request.base.ref }} + run: npx changeset status --since origin/"$BASE_REF" + env: + BASE_REF: ${{ github.event.pull_request.base.ref }} - name: Validate changeset is not missing packages - run: npx tsx scripts/check_changeset_completeness.ts ${{ github.event.pull_request.base.sha }} + run: npx tsx scripts/check_changeset_completeness.ts "$BASE_SHA" + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} - name: Validate that changeset has necessary dependency updates run: | npx changeset version @@ -440,6 +460,7 @@ jobs: check_package_versions: if: github.event_name == 'pull_request' runs-on: ubuntu-latest + timeout-minutes: 10 needs: - install steps: diff --git a/.prettierignore b/.prettierignore index c4daa53d7c..d7e58b0dd3 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,5 @@ # Ignore artifacts: +.amplify build coverage bin @@ -10,6 +11,7 @@ verdaccio-cache expected-cdk-out .changeset/pre.json concurrent_workspace_script_cache.json +packages/integration-tests/src/e2e-tests scripts/components/api-changes-validator/test-resources/working-directory /test-projects -testDir \ No newline at end of file +testDir diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 424876cb2f..d76eea6cdf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -82,6 +82,8 @@ For local testing we recommend writing unit tests that exercise the code you are npm run test:dir packages//lib/.test.ts ``` +> Note: If your test depends on \_\_dirname or import.meta.url paths, you may see errors resolving paths if you specify the entire path to the test file. You should specify just the `packages/` portion of the test you are running. + > Note: You must rebuild using `npm run build` for tests to pick up your changes. Sometimes it's nice to have a test project to use as a testing environment for local changes. You can create test projects in the `local-testing` directory using diff --git a/package-lock.json b/package-lock.json index 73015bf27c..25adc2dc0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@actions/github": "^6.0.0", "@aws-sdk/client-amplify": "^3.624.0", "@aws-sdk/client-cloudformation": "^3.624.0", + "@aws-sdk/client-cloudwatch-logs": "^3.624.0", "@aws-sdk/client-cognito-identity-provider": "^3.624.0", "@aws-sdk/client-dynamodb": "^3.624.0", "@aws-sdk/client-iam": "^3.624.0", @@ -23,6 +24,7 @@ "@aws-sdk/client-ssm": "^3.624.0", "@changesets/cli": "^2.26.1", "@changesets/get-release-plan": "^4.0.0", + "@changesets/types": "^6.0.0", "@microsoft/api-extractor": "7.43.8", "@octokit/webhooks-types": "^7.5.1", "@shopify/eslint-plugin": "^43.0.0", @@ -47,14 +49,14 @@ "fs-extra": "^11.1.1", "glob": "^10.1.0", "husky": "^8.0.3", - "lint-staged": "^13.2.1", + "lint-staged": "^15.2.10", "prettier": "^2.8.7", "rimraf": "^5.0.0", "semver": "^7.5.4", "tsx": "^4.6.1", "typedoc": "^0.25.3", "typescript": "~5.2.0", - "verdaccio": "^5.24.1" + "verdaccio": "^6.0.1" }, "engines": { "node": ">=18.16.0" @@ -479,12 +481,12 @@ } }, "node_modules/@aws-amplify/appsync-modelgen-plugin": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/@aws-amplify/appsync-modelgen-plugin/-/appsync-modelgen-plugin-2.13.0.tgz", - "integrity": "sha512-j74lEO53Sf5u9o6ZqmH6JqiUBD8VjqYSp4Rb4G+RNdLX8zt6eaEUKlO4wTQ9ejSrKgCDxzbb+2YldZWCMsWUFQ==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@aws-amplify/appsync-modelgen-plugin/-/appsync-modelgen-plugin-2.15.0.tgz", + "integrity": "sha512-k3hU3ZPXcxQgUB1I8mQ7+5zCTU2KCL43U4R/LbNAdGlXzDy0T2tppWJxobxRE+9K3+wtiYBeivtGzc7EmrveWw==", "license": "Apache-2.0", "dependencies": { - "@graphql-codegen/plugin-helpers": "^1.18.8", + "@graphql-codegen/plugin-helpers": "^3.1.1", "@graphql-codegen/visitor-plugin-common": "^1.22.0", "@graphql-tools/utils": "^6.0.18", "chalk": "^3.0.0", @@ -499,6 +501,60 @@ "graphql": "^15.5.0" } }, + "node_modules/@aws-amplify/appsync-modelgen-plugin/node_modules/@graphql-codegen/plugin-helpers": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-3.1.2.tgz", + "integrity": "sha512-emOQiHyIliVOIjKVKdsI5MXj312zmRDwmHpyUTZMjfpvxq/UVAHUJIVdVf+lnjjrI+LXBTgMlTWTgHQfmICxjg==", + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^9.0.0", + "change-case-all": "1.0.15", + "common-tags": "1.8.2", + "import-from": "4.0.0", + "lodash": "~4.17.0", + "tslib": "~2.4.0" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/@aws-amplify/appsync-modelgen-plugin/node_modules/@graphql-codegen/plugin-helpers/node_modules/@graphql-tools/utils": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.2.1.tgz", + "integrity": "sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==", + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@aws-amplify/appsync-modelgen-plugin/node_modules/change-case-all": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/change-case-all/-/change-case-all-1.0.15.tgz", + "integrity": "sha512-3+GIFhk3sNuvFAJKU46o26OdzudQlPNBCu1ZQi3cMeMHhty1bhDxu2WrEilVNYaGvqUtR1VSigFcJOiS13dRhQ==", + "license": "MIT", + "dependencies": { + "change-case": "^4.1.2", + "is-lower-case": "^2.0.2", + "is-upper-case": "^2.0.2", + "lower-case": "^2.0.2", + "lower-case-first": "^2.0.2", + "sponge-case": "^1.0.1", + "swap-case": "^2.0.2", + "title-case": "^3.0.3", + "upper-case": "^2.0.2", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/@aws-amplify/appsync-modelgen-plugin/node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "license": "0BSD" + }, "node_modules/@aws-amplify/auth": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/@aws-amplify/auth/-/auth-6.4.0.tgz", @@ -700,16 +756,12 @@ } }, "node_modules/@aws-amplify/data-construct": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@aws-amplify/data-construct/-/data-construct-1.10.0.tgz", - "integrity": "sha512-2w/SSsaqj0DeHJYKo1rQbNX+lvS9ja7wqqoYvRCJ/VKbSPVrNrYZORjBCQ4WIB9x3ElDVCogMboI7mgmfWeE7w==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@aws-amplify/data-construct/-/data-construct-1.10.1.tgz", + "integrity": "sha512-fG8EHT+LYpBGIOwXx2uw4IKTyZv5IWTRtnSSVBM6AYT76FsRe2qhvNnaDUWP7S5SEROQrfLxMnuWiozfeknTgg==", "bundleDependencies": [ "@aws-amplify/backend-output-schemas", "@aws-amplify/backend-output-storage", - "@aws-amplify/graphql-transformer", - "@aws-amplify/graphql-transformer-core", - "@aws-amplify/graphql-transformer-interfaces", - "zod", "@aws-amplify/graphql-auth-transformer", "@aws-amplify/graphql-conversation-transformer", "@aws-amplify/graphql-default-value-transformer", @@ -724,72 +776,237 @@ "@aws-amplify/graphql-searchable-transformer", "@aws-amplify/graphql-sql-transformer", "@aws-amplify/graphql-generation-transformer", + "@aws-amplify/ai-constructs", + "@aws-amplify/graphql-transformer", + "@aws-amplify/graphql-transformer-core", + "@aws-amplify/graphql-transformer-interfaces", "@aws-amplify/platform-core", "@aws-amplify/plugin-types", - "@aws-amplify/ai-constructs", - "fs-extra", - "graphql", - "graphql-transformer-common", - "hjson", - "lodash", - "md5", - "object-hash", - "ts-dedent", + "@aws-sdk/client-bedrock-runtime", + "@smithy/eventstream-serde-browser", + "@smithy/eventstream-serde-config-resolver", + "@smithy/eventstream-serde-node", + "@smithy/eventstream-serde-universal", + "@smithy/eventstream-codec", + "@aws-crypto/crc32", "charenc", "crypt", + "fs-extra", "graceful-fs", + "graphql", "graphql-mapping-template", + "graphql-transformer-common", + "hjson", "immer", "is-buffer", "jsonfile", "libphonenumber-js", + "lodash", + "md5", + "object-hash", "pluralize", - "universalify" + "ts-dedent", + "universalify", + "zod", + "@aws-sdk/client-sts", + "is-ci", + "lodash.mergewith", + "uuid", + "@aws-crypto/sha256-browser", + "@aws-crypto/sha256-js", + "@aws-sdk/client-sso-oidc", + "@aws-sdk/core", + "@aws-sdk/credential-provider-node", + "@aws-sdk/middleware-host-header", + "@aws-sdk/middleware-logger", + "@aws-sdk/middleware-recursion-detection", + "@aws-sdk/middleware-user-agent", + "@aws-sdk/region-config-resolver", + "@aws-sdk/types", + "@aws-sdk/util-endpoints", + "@aws-sdk/util-user-agent-browser", + "@aws-sdk/util-user-agent-node", + "@smithy/config-resolver", + "@smithy/core", + "@smithy/fetch-http-handler", + "@smithy/hash-node", + "@smithy/invalid-dependency", + "@smithy/middleware-content-length", + "@smithy/middleware-endpoint", + "@smithy/middleware-retry", + "@smithy/middleware-serde", + "@smithy/middleware-stack", + "@smithy/node-config-provider", + "@smithy/node-http-handler", + "@smithy/protocol-http", + "@smithy/smithy-client", + "@smithy/types", + "@smithy/url-parser", + "@smithy/util-base64", + "@smithy/util-body-length-browser", + "@smithy/util-body-length-node", + "@smithy/util-defaults-mode-browser", + "@smithy/util-defaults-mode-node", + "@smithy/util-endpoints", + "@smithy/util-middleware", + "@smithy/util-retry", + "@smithy/util-utf8", + "tslib", + "ci-info", + "@aws-crypto/supports-web-crypto", + "@aws-crypto/util", + "@aws-sdk/util-locate-window", + "@smithy/signature-v4", + "fast-xml-parser", + "@aws-sdk/credential-provider-env", + "@aws-sdk/credential-provider-http", + "@aws-sdk/credential-provider-ini", + "@aws-sdk/credential-provider-process", + "@aws-sdk/credential-provider-sso", + "@aws-sdk/credential-provider-web-identity", + "@smithy/credential-provider-imds", + "@smithy/property-provider", + "@smithy/shared-ini-file-loader", + "@smithy/util-config-provider", + "bowser", + "@smithy/querystring-builder", + "@smithy/util-buffer-from", + "@smithy/service-error-classification", + "@smithy/abort-controller", + "@smithy/util-stream", + "@smithy/querystring-parser", + "@smithy/is-array-buffer", + "@smithy/util-hex-encoding", + "@smithy/util-uri-escape", + "strnum", + "@aws-sdk/token-providers", + "@aws-sdk/client-sso", + "semver" ], - "license": "Apache-2.0", "dependencies": { - "@aws-amplify/ai-constructs": "^0.1.2", - "@aws-amplify/backend-output-schemas": "^0.4.0", - "@aws-amplify/backend-output-storage": "^0.2.2", - "@aws-amplify/graphql-api-construct": "1.12.0", - "@aws-amplify/graphql-auth-transformer": "4.1.0", - "@aws-amplify/graphql-conversation-transformer": "0.2.0", - "@aws-amplify/graphql-default-value-transformer": "3.0.2", - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-function-transformer": "3.0.2", - "@aws-amplify/graphql-generation-transformer": "0.2.0", - "@aws-amplify/graphql-http-transformer": "3.0.2", - "@aws-amplify/graphql-index-transformer": "3.0.2", - "@aws-amplify/graphql-maps-to-transformer": "4.0.2", - "@aws-amplify/graphql-model-transformer": "3.0.2", - "@aws-amplify/graphql-predictions-transformer": "3.0.2", - "@aws-amplify/graphql-relational-transformer": "3.0.2", - "@aws-amplify/graphql-searchable-transformer": "3.0.2", - "@aws-amplify/graphql-sql-transformer": "0.4.2", - "@aws-amplify/graphql-transformer": "2.1.0", - "@aws-amplify/graphql-transformer-core": "3.1.0", + "@aws-amplify/ai-constructs": "^0.1.4", + "@aws-amplify/backend-output-schemas": "^1.0.0", + "@aws-amplify/backend-output-storage": "^1.0.0", + "@aws-amplify/graphql-api-construct": "1.13.0", + "@aws-amplify/graphql-auth-transformer": "4.1.1", + "@aws-amplify/graphql-conversation-transformer": "0.2.1", + "@aws-amplify/graphql-default-value-transformer": "3.0.3", + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-function-transformer": "3.1.0", + "@aws-amplify/graphql-generation-transformer": "0.2.1", + "@aws-amplify/graphql-http-transformer": "3.0.3", + "@aws-amplify/graphql-index-transformer": "3.0.3", + "@aws-amplify/graphql-maps-to-transformer": "4.0.3", + "@aws-amplify/graphql-model-transformer": "3.0.3", + "@aws-amplify/graphql-predictions-transformer": "3.0.3", + "@aws-amplify/graphql-relational-transformer": "3.0.3", + "@aws-amplify/graphql-searchable-transformer": "3.0.3", + "@aws-amplify/graphql-sql-transformer": "0.4.3", + "@aws-amplify/graphql-transformer": "2.1.1", + "@aws-amplify/graphql-transformer-core": "3.1.1", "@aws-amplify/graphql-transformer-interfaces": "4.1.0", - "@aws-amplify/platform-core": "^0.2.0", - "@aws-amplify/plugin-types": "^0.4.1", + "@aws-amplify/platform-core": "^1.0.0", + "@aws-amplify/plugin-types": "^1.0.0", + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/client-bedrock-runtime": "^3.622.0", + "@aws-sdk/client-sso": "3.637.0", + "@aws-sdk/client-sso-oidc": "3.637.0", + "@aws-sdk/client-sts": "^3.624.0", + "@aws-sdk/core": "3.635.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-node": "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/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/token-providers": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.637.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/abort-controller": "^3.1.1", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.4.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/eventstream-codec": "^3.1.2", + "@smithy/eventstream-serde-browser": "^3.0.6", + "@smithy/eventstream-serde-config-resolver": "^3.0.3", + "@smithy/eventstream-serde-node": "^3.0.5", + "@smithy/eventstream-serde-universal": "^3.0.5", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/is-array-buffer": "^3.0.0", + "@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/property-provider": "^3.1.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/querystring-parser": "^3.0.3", + "@smithy/service-error-classification": "^3.0.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/signature-v4": "^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-buffer-from": "^3.0.0", + "@smithy/util-config-provider": "^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-hex-encoding": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-stream": "^3.1.3", + "@smithy/util-uri-escape": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "bowser": "^2.11.0", "charenc": "^0.0.2", + "ci-info": "^3.2.0", "crypt": "^0.0.2", + "fast-xml-parser": "4.4.1", "fs-extra": "^8.1.0", - "graceful-fs": "^4.2.11", + "graceful-fs": "^4.2.0", "graphql": "^15.5.0", "graphql-mapping-template": "5.0.1", "graphql-transformer-common": "5.0.1", "hjson": "^3.2.2", "immer": "^9.0.12", - "is-buffer": "^2.0.5", - "jsonfile": "^6.1.0", + "is-buffer": "~1.1.6", + "is-ci": "^3.0.1", + "jsonfile": "^4.0.0", "libphonenumber-js": "1.9.47", "lodash": "^4.17.21", - "md5": "^2.3.0", + "lodash.mergewith": "^4.6.2", + "md5": "^2.2.1", "object-hash": "^3.0.0", - "pluralize": "^8.0.0", + "pluralize": "8.0.0", + "semver": "^7.6.3", + "strnum": "^1.0.5", "ts-dedent": "^2.0.0", - "universalify": "^2.0.0", - "zod": "^3.22.3" + "tslib": "^2.6.2", + "universalify": "^0.1.0", + "uuid": "^9.0.1", + "zod": "^3.22.2" }, "peerDependencies": { "aws-cdk-lib": "^2.152.0", @@ -797,7 +1014,7 @@ } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/ai-constructs": { - "version": "0.1.2", + "version": "0.1.4", "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -810,45 +1027,49 @@ "constructs": "^10.0.0" } }, - "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/ai-constructs/node_modules/@aws-amplify/plugin-types": { - "version": "1.2.1", + "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/backend-output-schemas": { + "version": "1.2.0", "inBundle": true, "license": "Apache-2.0", "peerDependencies": { - "@aws-sdk/types": "^3.609.0", - "aws-cdk-lib": "^2.152.0", - "constructs": "^10.0.0" + "zod": "^3.22.2" } }, - "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/backend-output-schemas": { - "version": "0.4.0", + "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/backend-output-storage": { + "version": "1.1.1", "inBundle": true, "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/backend-output-schemas": "^1.2.0", + "@aws-amplify/platform-core": "^1.0.6" + }, "peerDependencies": { - "zod": "^3.21.4" + "aws-cdk-lib": "^2.152.0" } }, - "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/backend-output-storage": { - "version": "0.2.2", + "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/backend-output-storage/node_modules/@aws-amplify/platform-core": { + "version": "1.0.7", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/backend-output-schemas": "^0.4.0", - "@aws-amplify/platform-core": "^0.2.0" - }, - "peerDependencies": { - "aws-cdk-lib": "^2.103.0" + "@aws-amplify/plugin-types": "^1.2.1", + "@aws-sdk/client-sts": "^3.624.0", + "is-ci": "^3.0.1", + "lodash.mergewith": "^4.6.2", + "semver": "^7.6.3", + "uuid": "^9.0.1", + "zod": "^3.22.2" } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-auth-transformer": { - "version": "4.1.0", + "version": "4.1.1", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-model-transformer": "3.0.2", - "@aws-amplify/graphql-relational-transformer": "3.0.2", - "@aws-amplify/graphql-transformer-core": "3.1.0", + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-model-transformer": "3.0.3", + "@aws-amplify/graphql-relational-transformer": "3.0.3", + "@aws-amplify/graphql-transformer-core": "3.1.1", "@aws-amplify/graphql-transformer-interfaces": "4.1.0", "graphql": "^15.5.0", "graphql-mapping-template": "5.0.1", @@ -862,16 +1083,16 @@ } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-conversation-transformer": { - "version": "0.2.0", + "version": "0.2.1", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/ai-constructs": "^0.1.2", - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-index-transformer": "3.0.2", - "@aws-amplify/graphql-model-transformer": "3.0.2", - "@aws-amplify/graphql-relational-transformer": "3.0.2", - "@aws-amplify/graphql-transformer-core": "3.1.0", + "@aws-amplify/ai-constructs": "^0.1.4", + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-index-transformer": "3.0.3", + "@aws-amplify/graphql-model-transformer": "3.0.3", + "@aws-amplify/graphql-relational-transformer": "3.0.3", + "@aws-amplify/graphql-transformer-core": "3.1.1", "@aws-amplify/graphql-transformer-interfaces": "4.1.0", "graphql": "^15.5.0", "graphql-mapping-template": "5.0.1", @@ -884,12 +1105,12 @@ } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-default-value-transformer": { - "version": "3.0.2", + "version": "3.0.3", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-transformer-core": "3.1.0", + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-transformer-core": "3.1.1", "@aws-amplify/graphql-transformer-interfaces": "4.1.0", "graphql": "^15.5.0", "graphql-mapping-template": "5.0.1", @@ -898,17 +1119,17 @@ } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-directives": { - "version": "2.1.0", + "version": "2.2.0", "inBundle": true, "license": "Apache-2.0" }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-function-transformer": { - "version": "3.0.2", + "version": "3.1.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-transformer-core": "3.1.0", + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-transformer-core": "3.1.1", "@aws-amplify/graphql-transformer-interfaces": "4.1.0", "graphql": "^15.5.0", "graphql-mapping-template": "5.0.1", @@ -920,12 +1141,12 @@ } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-generation-transformer": { - "version": "0.2.0", + "version": "0.2.1", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-transformer-core": "3.1.0", + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-transformer-core": "3.1.1", "@aws-amplify/graphql-transformer-interfaces": "4.1.0", "graphql": "^15.5.0", "graphql-mapping-template": "5.0.1", @@ -938,12 +1159,12 @@ } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-http-transformer": { - "version": "3.0.2", + "version": "3.0.3", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-transformer-core": "3.1.0", + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-transformer-core": "3.1.1", "@aws-amplify/graphql-transformer-interfaces": "4.1.0", "graphql": "^15.5.0", "graphql-mapping-template": "5.0.1", @@ -955,13 +1176,13 @@ } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-index-transformer": { - "version": "3.0.2", + "version": "3.0.3", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-model-transformer": "3.0.2", - "@aws-amplify/graphql-transformer-core": "3.1.0", + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-model-transformer": "3.0.3", + "@aws-amplify/graphql-transformer-core": "3.1.1", "@aws-amplify/graphql-transformer-interfaces": "4.1.0", "graphql": "^15.5.0", "graphql-mapping-template": "5.0.1", @@ -973,12 +1194,12 @@ } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-maps-to-transformer": { - "version": "4.0.2", + "version": "4.0.3", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-transformer-core": "3.1.0", + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-transformer-core": "3.1.1", "@aws-amplify/graphql-transformer-interfaces": "4.1.0", "graphql-mapping-template": "5.0.1", "graphql-transformer-common": "5.0.1" @@ -989,12 +1210,12 @@ } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-model-transformer": { - "version": "3.0.2", + "version": "3.0.3", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-transformer-core": "3.1.0", + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-transformer-core": "3.1.1", "@aws-amplify/graphql-transformer-interfaces": "4.1.0", "graphql": "^15.5.0", "graphql-mapping-template": "5.0.1", @@ -1006,12 +1227,12 @@ } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-predictions-transformer": { - "version": "3.0.2", + "version": "3.0.3", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-transformer-core": "3.1.0", + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-transformer-core": "3.1.1", "@aws-amplify/graphql-transformer-interfaces": "4.1.0", "graphql": "^15.5.0", "graphql-mapping-template": "5.0.1", @@ -1023,14 +1244,14 @@ } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-relational-transformer": { - "version": "3.0.2", + "version": "3.0.3", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-index-transformer": "3.0.2", - "@aws-amplify/graphql-model-transformer": "3.0.2", - "@aws-amplify/graphql-transformer-core": "3.1.0", + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-index-transformer": "3.0.3", + "@aws-amplify/graphql-model-transformer": "3.0.3", + "@aws-amplify/graphql-transformer-core": "3.1.1", "@aws-amplify/graphql-transformer-interfaces": "4.1.0", "graphql": "^15.5.0", "graphql-mapping-template": "5.0.1", @@ -1043,13 +1264,13 @@ } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-searchable-transformer": { - "version": "3.0.2", + "version": "3.0.3", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-model-transformer": "3.0.2", - "@aws-amplify/graphql-transformer-core": "3.1.0", + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-model-transformer": "3.0.3", + "@aws-amplify/graphql-transformer-core": "3.1.1", "@aws-amplify/graphql-transformer-interfaces": "4.1.0", "graphql": "^15.5.0", "graphql-mapping-template": "5.0.1", @@ -1061,13 +1282,13 @@ } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-sql-transformer": { - "version": "0.4.2", + "version": "0.4.3", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-model-transformer": "3.0.2", - "@aws-amplify/graphql-transformer-core": "3.1.0", + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-model-transformer": "3.0.3", + "@aws-amplify/graphql-transformer-core": "3.1.1", "@aws-amplify/graphql-transformer-interfaces": "4.1.0", "graphql": "^15.5.0", "graphql-mapping-template": "5.0.1", @@ -1079,24 +1300,24 @@ } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-transformer": { - "version": "2.1.0", + "version": "2.1.1", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-auth-transformer": "4.1.0", - "@aws-amplify/graphql-conversation-transformer": "0.2.0", - "@aws-amplify/graphql-default-value-transformer": "3.0.2", - "@aws-amplify/graphql-function-transformer": "3.0.2", - "@aws-amplify/graphql-generation-transformer": "0.2.0", - "@aws-amplify/graphql-http-transformer": "3.0.2", - "@aws-amplify/graphql-index-transformer": "3.0.2", - "@aws-amplify/graphql-maps-to-transformer": "4.0.2", - "@aws-amplify/graphql-model-transformer": "3.0.2", - "@aws-amplify/graphql-predictions-transformer": "3.0.2", - "@aws-amplify/graphql-relational-transformer": "3.0.2", - "@aws-amplify/graphql-searchable-transformer": "3.0.2", - "@aws-amplify/graphql-sql-transformer": "0.4.2", - "@aws-amplify/graphql-transformer-core": "3.1.0", + "@aws-amplify/graphql-auth-transformer": "4.1.1", + "@aws-amplify/graphql-conversation-transformer": "0.2.1", + "@aws-amplify/graphql-default-value-transformer": "3.0.3", + "@aws-amplify/graphql-function-transformer": "3.1.0", + "@aws-amplify/graphql-generation-transformer": "0.2.1", + "@aws-amplify/graphql-http-transformer": "3.0.3", + "@aws-amplify/graphql-index-transformer": "3.0.3", + "@aws-amplify/graphql-maps-to-transformer": "4.0.3", + "@aws-amplify/graphql-model-transformer": "3.0.3", + "@aws-amplify/graphql-predictions-transformer": "3.0.3", + "@aws-amplify/graphql-relational-transformer": "3.0.3", + "@aws-amplify/graphql-searchable-transformer": "3.0.3", + "@aws-amplify/graphql-sql-transformer": "0.4.3", + "@aws-amplify/graphql-transformer-core": "3.1.1", "@aws-amplify/graphql-transformer-interfaces": "4.1.0" }, "peerDependencies": { @@ -1105,11 +1326,11 @@ } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-transformer-core": { - "version": "3.1.0", + "version": "3.1.1", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", + "@aws-amplify/graphql-directives": "2.2.0", "@aws-amplify/graphql-transformer-interfaces": "4.1.0", "fs-extra": "^8.1.0", "graphql": "^15.5.0", @@ -1139,723 +1360,3388 @@ } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/platform-core": { - "version": "0.2.0", + "version": "1.1.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/plugin-types": "^0.4.0" + "@aws-amplify/plugin-types": "^1.2.1", + "@aws-sdk/client-sts": "^3.624.0", + "is-ci": "^3.0.1", + "lodash.mergewith": "^4.6.2", + "semver": "^7.6.3", + "uuid": "^9.0.1", + "zod": "^3.22.2" } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/plugin-types": { - "version": "0.4.1", + "version": "1.2.1", "inBundle": true, "license": "Apache-2.0", "peerDependencies": { - "aws-cdk-lib": "^2.103.0", + "@aws-sdk/types": "^3.609.0", + "aws-cdk-lib": "^2.152.0", "constructs": "^10.0.0" } }, - "node_modules/@aws-amplify/data-construct/node_modules/charenc": { - "version": "0.0.2", + "node_modules/@aws-amplify/data-construct/node_modules/@aws-crypto/crc32": { + "version": "5.2.0", "inBundle": true, - "license": "BSD-3-Clause", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, "engines": { - "node": "*" + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/data-construct/node_modules/crypt": { - "version": "0.0.2", + "node_modules/@aws-amplify/data-construct/node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", "inBundle": true, - "license": "BSD-3-Clause", - "engines": { - "node": "*" + "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-amplify/data-construct/node_modules/fs-extra": { - "version": "8.1.0", + "node_modules/@aws-amplify/data-construct/node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", "inBundle": true, - "license": "MIT", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "inBundle": true, + "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-amplify/data-construct/node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-crypto/util": { + "version": "5.2.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.642.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.637.0", + "@aws-sdk/client-sts": "3.637.0", + "@aws-sdk/core": "3.635.0", + "@aws-sdk/credential-provider-node": "3.637.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.637.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.637.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.4.0", + "@smithy/eventstream-serde-browser": "^3.0.6", + "@smithy/eventstream-serde-config-resolver": "^3.0.3", + "@smithy/eventstream-serde-node": "^3.0.5", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.15", + "@smithy/util-defaults-mode-node": "^3.0.15", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-stream": "^3.1.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-sso": { + "version": "3.637.0", + "inBundle": true, + "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-amplify/data-construct/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.637.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.635.0", + "@aws-sdk/credential-provider-node": "3.637.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.637.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.637.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.4.0", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.15", + "@smithy/util-defaults-mode-node": "^3.0.15", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.637.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-sts": { + "version": "3.637.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.637.0", + "@aws-sdk/core": "3.635.0", + "@aws-sdk/credential-provider-node": "3.637.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.637.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.637.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.4.0", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.15", + "@smithy/util-defaults-mode-node": "^3.0.15", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/core": { + "version": "3.635.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^2.4.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/signature-v4": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.620.1", + "inBundle": true, + "license": "Apache-2.0", + "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_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.635.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/util-stream": "^3.1.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.637.0", + "inBundle": true, + "license": "Apache-2.0", + "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", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.637.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.637.0", + "inBundle": true, + "license": "Apache-2.0", + "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", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.620.1", + "inBundle": true, + "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" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.637.0", + "inBundle": true, + "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" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.621.0", + "inBundle": true, + "license": "Apache-2.0", + "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" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.621.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.620.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/middleware-logger": { + "version": "3.609.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.620.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.637.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.637.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.614.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/types": "^3.3.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/token-providers": { + "version": "3.614.0", + "inBundle": true, + "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" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.614.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/types": { + "version": "3.609.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/util-endpoints": { + "version": "3.637.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "@smithy/util-endpoints": "^2.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/util-locate-window": { + "version": "3.568.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.609.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.614.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/abort-controller": { + "version": "3.1.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/config-resolver": { + "version": "3.0.5", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.4", + "@smithy/types": "^3.3.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/core": { + "version": "2.4.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/credential-provider-imds": { + "version": "3.2.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/eventstream-codec": { + "version": "3.1.2", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^3.3.0", + "@smithy/util-hex-encoding": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/eventstream-serde-browser": { + "version": "3.0.6", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^3.0.5", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "3.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/eventstream-serde-node": { + "version": "3.0.5", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^3.0.5", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/eventstream-serde-universal": { + "version": "3.0.5", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^3.1.2", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.4", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.0", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/hash-node": { + "version": "3.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/invalid-dependency": { + "version": "3.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/middleware-content-length": { + "version": "3.0.5", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/middleware-endpoint": { + "version": "3.1.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/middleware-retry": { + "version": "3.0.15", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/service-error-classification": "^3.0.3", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/middleware-serde": { + "version": "3.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/middleware-stack": { + "version": "3.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/node-config-provider": { + "version": "3.1.4", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@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-amplify/data-construct/node_modules/@smithy/node-http-handler": { + "version": "3.1.4", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^3.1.1", + "@smithy/protocol-http": "^4.1.0", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/property-provider": { + "version": "3.1.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/protocol-http": { + "version": "4.1.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/querystring-builder": { + "version": "3.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "@smithy/util-uri-escape": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/querystring-parser": { + "version": "3.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/service-error-classification": { + "version": "3.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/shared-ini-file-loader": { + "version": "3.1.4", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/signature-v4": { + "version": "4.1.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-uri-escape": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/smithy-client": { + "version": "3.2.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "@smithy/util-stream": "^3.1.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/types": { + "version": "3.3.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/url-parser": { + "version": "3.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^3.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/util-base64": { + "version": "3.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/util-body-length-browser": { + "version": "3.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/util-body-length-node": { + "version": "3.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/util-config-provider": { + "version": "3.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/util-defaults-mode-browser": { + "version": "3.0.15", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^3.1.3", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/util-defaults-mode-node": { + "version": "3.0.15", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^3.0.5", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/util-endpoints": { + "version": "2.0.5", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/util-hex-encoding": { + "version": "3.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/util-middleware": { + "version": "3.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/util-retry": { + "version": "3.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^3.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/util-stream": { + "version": "3.1.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/types": "^3.3.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/util-uri-escape": { + "version": "3.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/bowser": { + "version": "2.11.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@aws-amplify/data-construct/node_modules/charenc": { + "version": "0.0.2", + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/ci-info": { + "version": "3.9.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/crypt": { + "version": "0.0.2", + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/fast-xml-parser": { + "version": "4.4.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "inBundle": true, + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/fs-extra": { + "version": "8.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/@aws-amplify/data-construct/node_modules/graphql": { + "version": "15.9.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/graphql-mapping-template": { + "version": "5.0.1", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/@aws-amplify/data-construct/node_modules/graphql-transformer-common": { + "version": "5.0.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.1", + "md5": "^2.2.1", + "pluralize": "8.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/hjson": { + "version": "3.2.2", + "inBundle": true, + "license": "MIT", + "bin": { + "hjson": "bin/hjson" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/immer": { + "version": "9.0.21", + "inBundle": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/is-buffer": { + "version": "1.1.6", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@aws-amplify/data-construct/node_modules/is-ci": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/jsonfile": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/libphonenumber-js": { + "version": "1.9.47", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@aws-amplify/data-construct/node_modules/lodash": { + "version": "4.17.21", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@aws-amplify/data-construct/node_modules/lodash.mergewith": { + "version": "4.6.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@aws-amplify/data-construct/node_modules/md5": { + "version": "2.3.0", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/object-hash": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/pluralize": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/semver": { + "version": "7.6.3", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/strnum": { + "version": "1.0.5", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@aws-amplify/data-construct/node_modules/ts-dedent": { + "version": "2.2.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/tslib": { + "version": "2.7.0", + "inBundle": true, + "license": "0BSD" + }, + "node_modules/@aws-amplify/data-construct/node_modules/universalify": { + "version": "0.1.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/uuid": { + "version": "9.0.1", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "inBundle": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/zod": { + "version": "3.23.8", + "inBundle": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@aws-amplify/data-schema": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@aws-amplify/data-schema/-/data-schema-1.5.1.tgz", + "integrity": "sha512-hFDqqwHqdoFazmvGOApCX8kqrdoum9YJikmAQN5tP2sgnCT++lqznFw2F4PPqDJRxhQP1AYuwhbbRBvGLMbs/w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/data-schema-types": "*", + "@smithy/util-base64": "^3.0.0", + "@types/aws-lambda": "^8.10.134", + "@types/json-schema": "^7.0.15", + "rxjs": "^7.8.1" + } + }, + "node_modules/@aws-amplify/data-schema-types": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@aws-amplify/data-schema-types/-/data-schema-types-1.2.0.tgz", + "integrity": "sha512-1hy2r7jl3hQ5J/CGjhmPhFPcdGSakfme1ZLjlTMJZILfYifZLSlGRKNCelMb3J5N9203hyeT5XDi5yR47JL1TQ==", + "dependencies": { + "graphql": "15.8.0", + "rxjs": "^7.8.1" + } + }, + "node_modules/@aws-amplify/data-schema-types/node_modules/graphql": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz", + "integrity": "sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==", + "license": "MIT", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/@aws-amplify/datastore": { + "version": "5.0.49", + "resolved": "https://registry.npmjs.org/@aws-amplify/datastore/-/datastore-5.0.49.tgz", + "integrity": "sha512-7ESzc1/5rOth3gPi65IJyx4dQbzRTvRxpefcA132C3vAm5BFqKEcDXyZX5sHbSf5OOMmQDsa68GfzhAM10rWPg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/api": "6.0.49", + "buffer": "4.9.2", + "idb": "5.0.6", + "immer": "9.0.6", + "rxjs": "^7.8.1", + "ulid": "^2.3.0" + }, + "peerDependencies": { + "@aws-amplify/core": "^6.1.0" + } + }, + "node_modules/@aws-amplify/deployed-backend-client": { + "resolved": "packages/deployed-backend-client", + "link": true + }, + "node_modules/@aws-amplify/form-generator": { + "resolved": "packages/form-generator", + "link": true + }, + "node_modules/@aws-amplify/graphql-api-construct": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@aws-amplify/graphql-api-construct/-/graphql-api-construct-1.13.0.tgz", + "integrity": "sha512-4cv4Ko7OP7lZTDhojM/ibKglP1Ospy+/iGMnKguuYoISqhmL2sgnyVHWw+1XnRVc8eUf6gnip2KXMQKnaPU0Lw==", + "bundleDependencies": [ + "@aws-amplify/backend-output-schemas", + "@aws-amplify/backend-output-storage", + "@aws-amplify/graphql-auth-transformer", + "@aws-amplify/graphql-conversation-transformer", + "@aws-amplify/graphql-default-value-transformer", + "@aws-amplify/graphql-directives", + "@aws-amplify/graphql-function-transformer", + "@aws-amplify/graphql-generation-transformer", + "@aws-amplify/graphql-http-transformer", + "@aws-amplify/graphql-index-transformer", + "@aws-amplify/graphql-maps-to-transformer", + "@aws-amplify/graphql-model-transformer", + "@aws-amplify/graphql-predictions-transformer", + "@aws-amplify/graphql-relational-transformer", + "@aws-amplify/graphql-searchable-transformer", + "@aws-amplify/graphql-sql-transformer", + "@aws-amplify/graphql-transformer", + "@aws-amplify/graphql-transformer-core", + "@aws-amplify/graphql-transformer-interfaces", + "@aws-amplify/platform-core", + "@aws-amplify/plugin-types", + "@aws-amplify/ai-constructs", + "@aws-sdk/client-bedrock-runtime", + "@smithy/eventstream-serde-browser", + "@smithy/eventstream-serde-config-resolver", + "@smithy/eventstream-serde-node", + "@smithy/eventstream-serde-universal", + "@smithy/eventstream-codec", + "@aws-crypto/crc32", + "charenc", + "crypt", + "fs-extra", + "graceful-fs", + "graphql", + "graphql-mapping-template", + "graphql-transformer-common", + "hjson", + "immer", + "is-buffer", + "jsonfile", + "libphonenumber-js", + "lodash", + "md5", + "object-hash", + "pluralize", + "ts-dedent", + "universalify", + "zod", + "@aws-sdk/client-sts", + "is-ci", + "lodash.mergewith", + "uuid", + "@aws-crypto/sha256-browser", + "@aws-crypto/sha256-js", + "@aws-sdk/client-sso-oidc", + "@aws-sdk/core", + "@aws-sdk/credential-provider-node", + "@aws-sdk/middleware-host-header", + "@aws-sdk/middleware-logger", + "@aws-sdk/middleware-recursion-detection", + "@aws-sdk/middleware-user-agent", + "@aws-sdk/region-config-resolver", + "@aws-sdk/types", + "@aws-sdk/util-endpoints", + "@aws-sdk/util-user-agent-browser", + "@aws-sdk/util-user-agent-node", + "@smithy/config-resolver", + "@smithy/core", + "@smithy/fetch-http-handler", + "@smithy/hash-node", + "@smithy/invalid-dependency", + "@smithy/middleware-content-length", + "@smithy/middleware-endpoint", + "@smithy/middleware-retry", + "@smithy/middleware-serde", + "@smithy/middleware-stack", + "@smithy/node-config-provider", + "@smithy/node-http-handler", + "@smithy/protocol-http", + "@smithy/smithy-client", + "@smithy/types", + "@smithy/url-parser", + "@smithy/util-base64", + "@smithy/util-body-length-browser", + "@smithy/util-body-length-node", + "@smithy/util-defaults-mode-browser", + "@smithy/util-defaults-mode-node", + "@smithy/util-endpoints", + "@smithy/util-middleware", + "@smithy/util-retry", + "@smithy/util-utf8", + "tslib", + "ci-info", + "@aws-crypto/supports-web-crypto", + "@aws-crypto/util", + "@aws-sdk/util-locate-window", + "@smithy/signature-v4", + "fast-xml-parser", + "@aws-sdk/credential-provider-env", + "@aws-sdk/credential-provider-http", + "@aws-sdk/credential-provider-ini", + "@aws-sdk/credential-provider-process", + "@aws-sdk/credential-provider-sso", + "@aws-sdk/credential-provider-web-identity", + "@smithy/credential-provider-imds", + "@smithy/property-provider", + "@smithy/shared-ini-file-loader", + "@smithy/util-config-provider", + "bowser", + "@smithy/querystring-builder", + "@smithy/util-buffer-from", + "@smithy/service-error-classification", + "@smithy/abort-controller", + "@smithy/util-stream", + "@smithy/querystring-parser", + "@smithy/is-array-buffer", + "@smithy/util-hex-encoding", + "@smithy/util-uri-escape", + "strnum", + "@aws-sdk/token-providers", + "@aws-sdk/client-sso", + "semver" + ], + "dependencies": { + "@aws-amplify/ai-constructs": "^0.1.4", + "@aws-amplify/backend-output-schemas": "^1.0.0", + "@aws-amplify/backend-output-storage": "^1.0.0", + "@aws-amplify/graphql-auth-transformer": "4.1.1", + "@aws-amplify/graphql-conversation-transformer": "0.2.1", + "@aws-amplify/graphql-default-value-transformer": "3.0.3", + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-function-transformer": "3.1.0", + "@aws-amplify/graphql-generation-transformer": "0.2.1", + "@aws-amplify/graphql-http-transformer": "3.0.3", + "@aws-amplify/graphql-index-transformer": "3.0.3", + "@aws-amplify/graphql-maps-to-transformer": "4.0.3", + "@aws-amplify/graphql-model-transformer": "3.0.3", + "@aws-amplify/graphql-predictions-transformer": "3.0.3", + "@aws-amplify/graphql-relational-transformer": "3.0.3", + "@aws-amplify/graphql-searchable-transformer": "3.0.3", + "@aws-amplify/graphql-sql-transformer": "0.4.3", + "@aws-amplify/graphql-transformer": "2.1.1", + "@aws-amplify/graphql-transformer-core": "3.1.1", + "@aws-amplify/graphql-transformer-interfaces": "4.1.0", + "@aws-amplify/platform-core": "^1.0.0", + "@aws-amplify/plugin-types": "^1.0.0", + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/client-bedrock-runtime": "^3.622.0", + "@aws-sdk/client-sso": "3.637.0", + "@aws-sdk/client-sso-oidc": "3.637.0", + "@aws-sdk/client-sts": "^3.624.0", + "@aws-sdk/core": "3.635.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-node": "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/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/token-providers": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.637.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/abort-controller": "^3.1.1", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.4.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/eventstream-codec": "^3.1.2", + "@smithy/eventstream-serde-browser": "^3.0.6", + "@smithy/eventstream-serde-config-resolver": "^3.0.3", + "@smithy/eventstream-serde-node": "^3.0.5", + "@smithy/eventstream-serde-universal": "^3.0.5", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/is-array-buffer": "^3.0.0", + "@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/property-provider": "^3.1.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/querystring-parser": "^3.0.3", + "@smithy/service-error-classification": "^3.0.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/signature-v4": "^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-buffer-from": "^3.0.0", + "@smithy/util-config-provider": "^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-hex-encoding": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-stream": "^3.1.3", + "@smithy/util-uri-escape": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "bowser": "^2.11.0", + "charenc": "^0.0.2", + "ci-info": "^3.2.0", + "crypt": "^0.0.2", + "fast-xml-parser": "4.4.1", + "fs-extra": "^8.1.0", + "graceful-fs": "^4.2.0", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.1", + "graphql-transformer-common": "5.0.1", + "hjson": "^3.2.2", + "immer": "^9.0.12", + "is-buffer": "~1.1.6", + "is-ci": "^3.0.1", + "jsonfile": "^4.0.0", + "libphonenumber-js": "1.9.47", + "lodash": "^4.17.21", + "lodash.mergewith": "^4.6.2", + "md5": "^2.2.1", + "object-hash": "^3.0.0", + "pluralize": "8.0.0", + "semver": "^7.6.3", + "strnum": "^1.0.5", + "ts-dedent": "^2.0.0", + "tslib": "^2.6.2", + "universalify": "^0.1.0", + "uuid": "^9.0.1", + "zod": "^3.22.2" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.152.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/ai-constructs": { + "version": "0.1.4", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/plugin-types": "^1.0.1", + "@aws-sdk/client-bedrock-runtime": "^3.622.0", + "@smithy/types": "^3.3.0" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.152.0", + "constructs": "^10.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/backend-output-schemas": { + "version": "1.2.0", + "inBundle": true, + "license": "Apache-2.0", + "peerDependencies": { + "zod": "^3.22.2" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/backend-output-storage": { + "version": "1.1.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/backend-output-schemas": "^1.2.0", + "@aws-amplify/platform-core": "^1.0.6" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.152.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/backend-output-storage/node_modules/@aws-amplify/platform-core": { + "version": "1.0.7", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/plugin-types": "^1.2.1", + "@aws-sdk/client-sts": "^3.624.0", + "is-ci": "^3.0.1", + "lodash.mergewith": "^4.6.2", + "semver": "^7.6.3", + "uuid": "^9.0.1", + "zod": "^3.22.2" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-auth-transformer": { + "version": "4.1.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-model-transformer": "3.0.3", + "@aws-amplify/graphql-relational-transformer": "3.0.3", + "@aws-amplify/graphql-transformer-core": "3.1.1", + "@aws-amplify/graphql-transformer-interfaces": "4.1.0", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.1", + "graphql-transformer-common": "5.0.1", + "lodash": "^4.17.21", + "md5": "^2.3.0" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.152.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-conversation-transformer": { + "version": "0.2.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/ai-constructs": "^0.1.4", + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-index-transformer": "3.0.3", + "@aws-amplify/graphql-model-transformer": "3.0.3", + "@aws-amplify/graphql-relational-transformer": "3.0.3", + "@aws-amplify/graphql-transformer-core": "3.1.1", + "@aws-amplify/graphql-transformer-interfaces": "4.1.0", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.1", + "graphql-transformer-common": "5.0.1", + "immer": "^9.0.12" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.152.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-default-value-transformer": { + "version": "3.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-transformer-core": "3.1.1", + "@aws-amplify/graphql-transformer-interfaces": "4.1.0", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.1", + "graphql-transformer-common": "5.0.1", + "libphonenumber-js": "1.9.47" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-directives": { + "version": "2.2.0", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-function-transformer": { + "version": "3.1.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-transformer-core": "3.1.1", + "@aws-amplify/graphql-transformer-interfaces": "4.1.0", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.1", + "graphql-transformer-common": "5.0.1" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.152.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-generation-transformer": { + "version": "0.2.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-transformer-core": "3.1.1", + "@aws-amplify/graphql-transformer-interfaces": "4.1.0", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.1", + "graphql-transformer-common": "5.0.1", + "immer": "^9.0.12" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.152.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-http-transformer": { + "version": "3.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-transformer-core": "3.1.1", + "@aws-amplify/graphql-transformer-interfaces": "4.1.0", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.1", + "graphql-transformer-common": "5.0.1" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.152.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-index-transformer": { + "version": "3.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-model-transformer": "3.0.3", + "@aws-amplify/graphql-transformer-core": "3.1.1", + "@aws-amplify/graphql-transformer-interfaces": "4.1.0", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.1", + "graphql-transformer-common": "5.0.1" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.152.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-maps-to-transformer": { + "version": "4.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-transformer-core": "3.1.1", + "@aws-amplify/graphql-transformer-interfaces": "4.1.0", + "graphql-mapping-template": "5.0.1", + "graphql-transformer-common": "5.0.1" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.152.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-model-transformer": { + "version": "3.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-transformer-core": "3.1.1", + "@aws-amplify/graphql-transformer-interfaces": "4.1.0", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.1", + "graphql-transformer-common": "5.0.1" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.152.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-predictions-transformer": { + "version": "3.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-transformer-core": "3.1.1", + "@aws-amplify/graphql-transformer-interfaces": "4.1.0", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.1", + "graphql-transformer-common": "5.0.1" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.152.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-relational-transformer": { + "version": "3.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-index-transformer": "3.0.3", + "@aws-amplify/graphql-model-transformer": "3.0.3", + "@aws-amplify/graphql-transformer-core": "3.1.1", + "@aws-amplify/graphql-transformer-interfaces": "4.1.0", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.1", + "graphql-transformer-common": "5.0.1", + "immer": "^9.0.12" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.152.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-searchable-transformer": { + "version": "3.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-model-transformer": "3.0.3", + "@aws-amplify/graphql-transformer-core": "3.1.1", + "@aws-amplify/graphql-transformer-interfaces": "4.1.0", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.1", + "graphql-transformer-common": "5.0.1" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.152.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-sql-transformer": { + "version": "0.4.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-model-transformer": "3.0.3", + "@aws-amplify/graphql-transformer-core": "3.1.1", + "@aws-amplify/graphql-transformer-interfaces": "4.1.0", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.1", + "graphql-transformer-common": "5.0.1" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.152.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-transformer": { + "version": "2.1.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-auth-transformer": "4.1.1", + "@aws-amplify/graphql-conversation-transformer": "0.2.1", + "@aws-amplify/graphql-default-value-transformer": "3.0.3", + "@aws-amplify/graphql-function-transformer": "3.1.0", + "@aws-amplify/graphql-generation-transformer": "0.2.1", + "@aws-amplify/graphql-http-transformer": "3.0.3", + "@aws-amplify/graphql-index-transformer": "3.0.3", + "@aws-amplify/graphql-maps-to-transformer": "4.0.3", + "@aws-amplify/graphql-model-transformer": "3.0.3", + "@aws-amplify/graphql-predictions-transformer": "3.0.3", + "@aws-amplify/graphql-relational-transformer": "3.0.3", + "@aws-amplify/graphql-searchable-transformer": "3.0.3", + "@aws-amplify/graphql-sql-transformer": "0.4.3", + "@aws-amplify/graphql-transformer-core": "3.1.1", + "@aws-amplify/graphql-transformer-interfaces": "4.1.0" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.152.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-transformer-core": { + "version": "3.1.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.2.0", + "@aws-amplify/graphql-transformer-interfaces": "4.1.0", + "fs-extra": "^8.1.0", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.1", + "graphql-transformer-common": "5.0.1", + "hjson": "^3.2.2", + "lodash": "^4.17.21", + "md5": "^2.3.0", + "object-hash": "^3.0.0", + "ts-dedent": "^2.0.0" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.152.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-transformer-interfaces": { + "version": "4.1.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "graphql": "^15.5.0" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.152.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/platform-core": { + "version": "1.1.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/plugin-types": "^1.2.1", + "@aws-sdk/client-sts": "^3.624.0", + "is-ci": "^3.0.1", + "lodash.mergewith": "^4.6.2", + "semver": "^7.6.3", + "uuid": "^9.0.1", + "zod": "^3.22.2" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/plugin-types": { + "version": "1.2.1", + "inBundle": true, + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/types": "^3.609.0", + "aws-cdk-lib": "^2.152.0", + "constructs": "^10.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "inBundle": true, + "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-amplify/graphql-api-construct/node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "inBundle": true, + "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-amplify/graphql-api-construct/node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "inBundle": true, + "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-amplify/graphql-api-construct/node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-crypto/util": { + "version": "5.2.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.642.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.637.0", + "@aws-sdk/client-sts": "3.637.0", + "@aws-sdk/core": "3.635.0", + "@aws-sdk/credential-provider-node": "3.637.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.637.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.637.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.4.0", + "@smithy/eventstream-serde-browser": "^3.0.6", + "@smithy/eventstream-serde-config-resolver": "^3.0.3", + "@smithy/eventstream-serde-node": "^3.0.5", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.15", + "@smithy/util-defaults-mode-node": "^3.0.15", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-stream": "^3.1.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sso": { + "version": "3.637.0", + "inBundle": true, + "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-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.637.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.635.0", + "@aws-sdk/credential-provider-node": "3.637.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.637.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.637.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.4.0", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.15", + "@smithy/util-defaults-mode-node": "^3.0.15", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.637.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sts": { + "version": "3.637.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.637.0", + "@aws-sdk/core": "3.635.0", + "@aws-sdk/credential-provider-node": "3.637.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.637.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.637.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.4.0", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.15", + "@smithy/util-defaults-mode-node": "^3.0.15", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/core": { + "version": "3.635.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^2.4.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/signature-v4": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.620.1", + "inBundle": true, + "license": "Apache-2.0", + "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_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.635.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/util-stream": "^3.1.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.637.0", + "inBundle": true, + "license": "Apache-2.0", + "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", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.637.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.637.0", + "inBundle": true, + "license": "Apache-2.0", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "@aws-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.635.0", + "@aws-sdk/credential-provider-ini": "3.637.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.637.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6 <7 || >=8" + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/data-construct/node_modules/fs-extra/node_modules/jsonfile": { - "version": "4.0.0", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.620.1", "inBundle": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "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" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/data-construct/node_modules/fs-extra/node_modules/universalify": { - "version": "0.1.2", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.637.0", "inBundle": true, - "license": "MIT", + "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" + }, "engines": { - "node": ">= 4.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/data-construct/node_modules/graceful-fs": { - "version": "4.2.11", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.621.0", "inBundle": true, - "license": "ISC" + "license": "Apache-2.0", + "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" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.621.0" + } }, - "node_modules/@aws-amplify/data-construct/node_modules/graphql": { - "version": "15.8.0", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.620.0", "inBundle": true, - "license": "MIT", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">= 10.x" + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/middleware-logger": { + "version": "3.609.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.620.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.637.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.637.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.614.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/types": "^3.3.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/token-providers": { + "version": "3.614.0", + "inBundle": true, + "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" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.614.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/types": { + "version": "3.609.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/util-endpoints": { + "version": "3.637.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "@smithy/util-endpoints": "^2.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/util-locate-window": { + "version": "3.568.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.609.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.614.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/abort-controller": { + "version": "3.1.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/config-resolver": { + "version": "3.0.5", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.4", + "@smithy/types": "^3.3.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/core": { + "version": "2.4.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/data-construct/node_modules/graphql-mapping-template": { - "version": "5.0.1", - "inBundle": true, - "license": "Apache-2.0" - }, - "node_modules/@aws-amplify/data-construct/node_modules/graphql-transformer-common": { - "version": "5.0.1", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/credential-provider-imds": { + "version": "3.2.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "md5": "^2.2.1", - "pluralize": "8.0.0" + "@smithy/node-config-provider": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/data-construct/node_modules/hjson": { - "version": "3.2.2", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/eventstream-codec": { + "version": "3.1.2", "inBundle": true, - "license": "MIT", - "bin": { - "hjson": "bin/hjson" + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^3.3.0", + "@smithy/util-hex-encoding": "^3.0.0", + "tslib": "^2.6.2" } }, - "node_modules/@aws-amplify/data-construct/node_modules/immer": { - "version": "9.0.21", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/eventstream-serde-browser": { + "version": "3.0.6", "inBundle": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^3.0.5", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/data-construct/node_modules/is-buffer": { - "version": "2.0.5", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "3.0.3", "inBundle": true, - "license": "MIT", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=4" + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/data-construct/node_modules/jsonfile": { - "version": "6.1.0", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/eventstream-serde-node": { + "version": "3.0.5", "inBundle": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "universalify": "^2.0.0" + "@smithy/eventstream-serde-universal": "^3.0.5", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/data-construct/node_modules/libphonenumber-js": { - "version": "1.9.47", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/eventstream-serde-universal": { + "version": "3.0.5", "inBundle": true, - "license": "MIT" + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^3.1.2", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } }, - "node_modules/@aws-amplify/data-construct/node_modules/lodash": { - "version": "4.17.21", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.4", "inBundle": true, - "license": "MIT" + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.0", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } }, - "node_modules/@aws-amplify/data-construct/node_modules/md5": { - "version": "2.3.0", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/hash-node": { + "version": "3.0.3", "inBundle": true, - "license": "BSD-3-Clause", + "license": "Apache-2.0", "dependencies": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" + "@smithy/types": "^3.3.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/data-construct/node_modules/md5/node_modules/is-buffer": { - "version": "1.1.6", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/invalid-dependency": { + "version": "3.0.3", "inBundle": true, - "license": "MIT" + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + } }, - "node_modules/@aws-amplify/data-construct/node_modules/object-hash": { + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/is-array-buffer": { "version": "3.0.0", "inBundle": true, - "license": "MIT", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, "engines": { - "node": ">= 6" + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/data-construct/node_modules/pluralize": { - "version": "8.0.0", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/middleware-content-length": { + "version": "3.0.5", "inBundle": true, - "license": "MIT", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=4" + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/data-construct/node_modules/ts-dedent": { - "version": "2.2.0", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/middleware-endpoint": { + "version": "3.1.0", "inBundle": true, - "license": "MIT", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.10" + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/data-construct/node_modules/universalify": { - "version": "2.0.0", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/middleware-retry": { + "version": "3.0.15", "inBundle": true, - "license": "MIT", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/service-error-classification": "^3.0.3", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, "engines": { - "node": ">= 10.0.0" + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/data-construct/node_modules/zod": { - "version": "3.22.4", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/middleware-serde": { + "version": "3.0.3", "inBundle": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/data-schema": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@aws-amplify/data-schema/-/data-schema-1.5.1.tgz", - "integrity": "sha512-hFDqqwHqdoFazmvGOApCX8kqrdoum9YJikmAQN5tP2sgnCT++lqznFw2F4PPqDJRxhQP1AYuwhbbRBvGLMbs/w==", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/middleware-stack": { + "version": "3.0.3", + "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/data-schema-types": "*", - "@smithy/util-base64": "^3.0.0", - "@types/aws-lambda": "^8.10.134", - "@types/json-schema": "^7.0.15", - "rxjs": "^7.8.1" + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/data-schema-types": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@aws-amplify/data-schema-types/-/data-schema-types-1.1.1.tgz", - "integrity": "sha512-WhWEEsztpSSxIY0lJ3Ge5iA4g3PBm66SQmy1fBH1FBq0T+cxUBijifOU8MNwf+tf6lGpArMX0RS54HRVF5fUSA==", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/node-config-provider": { + "version": "3.1.4", + "inBundle": true, "license": "Apache-2.0", "dependencies": { - "graphql": "15.8.0", - "rxjs": "^7.8.1" + "@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-amplify/data-schema-types/node_modules/graphql": { - "version": "15.8.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz", - "integrity": "sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==", - "license": "MIT", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/node-http-handler": { + "version": "3.1.4", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^3.1.1", + "@smithy/protocol-http": "^4.1.0", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">= 10.x" + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/datastore": { - "version": "5.0.49", - "resolved": "https://registry.npmjs.org/@aws-amplify/datastore/-/datastore-5.0.49.tgz", - "integrity": "sha512-7ESzc1/5rOth3gPi65IJyx4dQbzRTvRxpefcA132C3vAm5BFqKEcDXyZX5sHbSf5OOMmQDsa68GfzhAM10rWPg==", - "dev": true, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/property-provider": { + "version": "3.1.3", + "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/api": "6.0.49", - "buffer": "4.9.2", - "idb": "5.0.6", - "immer": "9.0.6", - "rxjs": "^7.8.1", - "ulid": "^2.3.0" + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@aws-amplify/core": "^6.1.0" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/deployed-backend-client": { - "resolved": "packages/deployed-backend-client", - "link": true - }, - "node_modules/@aws-amplify/form-generator": { - "resolved": "packages/form-generator", - "link": true - }, - "node_modules/@aws-amplify/graphql-api-construct": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@aws-amplify/graphql-api-construct/-/graphql-api-construct-1.12.0.tgz", - "integrity": "sha512-h/FWriGl39zise+EofwHh2FmHmmEINefN/DEdXgQLT0/Cw5WUKCMKaIS/30Nk27N8V0anFWBVVbVUDMOjARLTQ==", - "bundleDependencies": [ - "@aws-amplify/backend-output-schemas", - "@aws-amplify/backend-output-storage", - "@aws-amplify/graphql-auth-transformer", - "@aws-amplify/graphql-conversation-transformer", - "@aws-amplify/graphql-default-value-transformer", - "@aws-amplify/graphql-directives", - "@aws-amplify/graphql-function-transformer", - "@aws-amplify/graphql-generation-transformer", - "@aws-amplify/graphql-http-transformer", - "@aws-amplify/graphql-index-transformer", - "@aws-amplify/graphql-maps-to-transformer", - "@aws-amplify/graphql-model-transformer", - "@aws-amplify/graphql-predictions-transformer", - "@aws-amplify/graphql-relational-transformer", - "@aws-amplify/graphql-searchable-transformer", - "@aws-amplify/graphql-sql-transformer", - "@aws-amplify/graphql-transformer", - "@aws-amplify/graphql-transformer-core", - "@aws-amplify/graphql-transformer-interfaces", - "@aws-amplify/platform-core", - "@aws-amplify/plugin-types", - "@aws-amplify/ai-constructs", - "charenc", - "crypt", - "fs-extra", - "graceful-fs", - "graphql", - "graphql-mapping-template", - "graphql-transformer-common", - "hjson", - "immer", - "is-buffer", - "jsonfile", - "libphonenumber-js", - "lodash", - "md5", - "object-hash", - "pluralize", - "ts-dedent", - "universalify", - "zod" - ], + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/protocol-http": { + "version": "4.1.0", + "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/ai-constructs": "^0.1.2", - "@aws-amplify/backend-output-schemas": "^0.4.0", - "@aws-amplify/backend-output-storage": "^0.2.2", - "@aws-amplify/graphql-auth-transformer": "4.1.0", - "@aws-amplify/graphql-conversation-transformer": "0.2.0", - "@aws-amplify/graphql-default-value-transformer": "3.0.2", - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-function-transformer": "3.0.2", - "@aws-amplify/graphql-generation-transformer": "0.2.0", - "@aws-amplify/graphql-http-transformer": "3.0.2", - "@aws-amplify/graphql-index-transformer": "3.0.2", - "@aws-amplify/graphql-maps-to-transformer": "4.0.2", - "@aws-amplify/graphql-model-transformer": "3.0.2", - "@aws-amplify/graphql-predictions-transformer": "3.0.2", - "@aws-amplify/graphql-relational-transformer": "3.0.2", - "@aws-amplify/graphql-searchable-transformer": "3.0.2", - "@aws-amplify/graphql-sql-transformer": "0.4.2", - "@aws-amplify/graphql-transformer": "2.1.0", - "@aws-amplify/graphql-transformer-core": "3.1.0", - "@aws-amplify/graphql-transformer-interfaces": "4.1.0", - "@aws-amplify/platform-core": "^0.2.0", - "@aws-amplify/plugin-types": "^0.4.1", - "charenc": "^0.0.2", - "crypt": "^0.0.2", - "fs-extra": "^8.1.0", - "graceful-fs": "^4.2.11", - "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1", - "hjson": "^3.2.2", - "immer": "^9.0.12", - "is-buffer": "^2.0.5", - "jsonfile": "^6.1.0", - "libphonenumber-js": "1.9.47", - "lodash": "^4.17.21", - "md5": "^2.3.0", - "object-hash": "^3.0.0", - "pluralize": "^8.0.0", - "ts-dedent": "^2.0.0", - "universalify": "^2.0.0", - "zod": "^3.22.3" + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "aws-cdk-lib": "^2.152.0", - "constructs": "^10.3.0" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/ai-constructs": { - "version": "0.1.2", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/querystring-builder": { + "version": "3.0.3", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/plugin-types": "^1.0.1", - "@aws-sdk/client-bedrock-runtime": "^3.622.0", - "@smithy/types": "^3.3.0" + "@smithy/types": "^3.3.0", + "@smithy/util-uri-escape": "^3.0.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "aws-cdk-lib": "^2.152.0", - "constructs": "^10.0.0" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/ai-constructs/node_modules/@aws-amplify/plugin-types": { - "version": "1.2.1", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/querystring-parser": { + "version": "3.0.3", "inBundle": true, "license": "Apache-2.0", - "peerDependencies": { - "@aws-sdk/types": "^3.609.0", - "aws-cdk-lib": "^2.152.0", - "constructs": "^10.0.0" + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/backend-output-schemas": { - "version": "0.4.0", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/service-error-classification": { + "version": "3.0.3", "inBundle": true, "license": "Apache-2.0", - "peerDependencies": { - "zod": "^3.21.4" + "dependencies": { + "@smithy/types": "^3.3.0" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/backend-output-storage": { - "version": "0.2.2", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/shared-ini-file-loader": { + "version": "3.1.4", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/backend-output-schemas": "^0.4.0", - "@aws-amplify/platform-core": "^0.2.0" + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "aws-cdk-lib": "^2.103.0" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-auth-transformer": { + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/signature-v4": { "version": "4.1.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-model-transformer": "3.0.2", - "@aws-amplify/graphql-relational-transformer": "3.0.2", - "@aws-amplify/graphql-transformer-core": "3.1.0", - "@aws-amplify/graphql-transformer-interfaces": "4.1.0", - "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1", - "lodash": "^4.17.21", - "md5": "^2.3.0" + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-uri-escape": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "aws-cdk-lib": "^2.152.0", - "constructs": "^10.3.0" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-conversation-transformer": { - "version": "0.2.0", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/smithy-client": { + "version": "3.2.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/ai-constructs": "^0.1.2", - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-index-transformer": "3.0.2", - "@aws-amplify/graphql-model-transformer": "3.0.2", - "@aws-amplify/graphql-relational-transformer": "3.0.2", - "@aws-amplify/graphql-transformer-core": "3.1.0", - "@aws-amplify/graphql-transformer-interfaces": "4.1.0", - "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1", - "immer": "^9.0.12" + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "@smithy/util-stream": "^3.1.3", + "tslib": "^2.6.2" }, - "peerDependencies": { - "aws-cdk-lib": "^2.152.0", - "constructs": "^10.3.0" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-default-value-transformer": { - "version": "3.0.2", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/types": { + "version": "3.3.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-transformer-core": "3.1.0", - "@aws-amplify/graphql-transformer-interfaces": "4.1.0", - "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1", - "libphonenumber-js": "1.9.47" + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-directives": { - "version": "2.1.0", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/url-parser": { + "version": "3.0.3", "inBundle": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^3.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-function-transformer": { - "version": "3.0.2", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/util-base64": { + "version": "3.0.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-transformer-core": "3.1.0", - "@aws-amplify/graphql-transformer-interfaces": "4.1.0", - "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1" + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "aws-cdk-lib": "^2.152.0", - "constructs": "^10.3.0" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-generation-transformer": { - "version": "0.2.0", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/util-body-length-browser": { + "version": "3.0.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-transformer-core": "3.1.0", - "@aws-amplify/graphql-transformer-interfaces": "4.1.0", - "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1", - "immer": "^9.0.12" - }, - "peerDependencies": { - "aws-cdk-lib": "^2.152.0", - "constructs": "^10.3.0" + "tslib": "^2.6.2" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-http-transformer": { - "version": "3.0.2", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/util-body-length-node": { + "version": "3.0.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-transformer-core": "3.1.0", - "@aws-amplify/graphql-transformer-interfaces": "4.1.0", - "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1" + "tslib": "^2.6.2" }, - "peerDependencies": { - "aws-cdk-lib": "^2.152.0", - "constructs": "^10.3.0" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-index-transformer": { - "version": "3.0.2", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-model-transformer": "3.0.2", - "@aws-amplify/graphql-transformer-core": "3.1.0", - "@aws-amplify/graphql-transformer-interfaces": "4.1.0", - "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1" + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "aws-cdk-lib": "^2.152.0", - "constructs": "^10.3.0" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-maps-to-transformer": { - "version": "4.0.2", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/util-config-provider": { + "version": "3.0.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-transformer-core": "3.1.0", - "@aws-amplify/graphql-transformer-interfaces": "4.1.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1" + "tslib": "^2.6.2" }, - "peerDependencies": { - "aws-cdk-lib": "^2.152.0", - "constructs": "^10.3.0" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-model-transformer": { - "version": "3.0.2", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/util-defaults-mode-browser": { + "version": "3.0.15", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-transformer-core": "3.1.0", - "@aws-amplify/graphql-transformer-interfaces": "4.1.0", - "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1" + "@smithy/property-provider": "^3.1.3", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "aws-cdk-lib": "^2.152.0", - "constructs": "^10.3.0" + "engines": { + "node": ">= 10.0.0" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-predictions-transformer": { - "version": "3.0.2", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/util-defaults-mode-node": { + "version": "3.0.15", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-transformer-core": "3.1.0", - "@aws-amplify/graphql-transformer-interfaces": "4.1.0", - "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1" + "@smithy/config-resolver": "^3.0.5", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "aws-cdk-lib": "^2.152.0", - "constructs": "^10.3.0" + "engines": { + "node": ">= 10.0.0" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-relational-transformer": { - "version": "3.0.2", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/util-endpoints": { + "version": "2.0.5", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-index-transformer": "3.0.2", - "@aws-amplify/graphql-model-transformer": "3.0.2", - "@aws-amplify/graphql-transformer-core": "3.1.0", - "@aws-amplify/graphql-transformer-interfaces": "4.1.0", - "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1", - "immer": "^9.0.12" + "@smithy/node-config-provider": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "aws-cdk-lib": "^2.152.0", - "constructs": "^10.3.0" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-searchable-transformer": { - "version": "3.0.2", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/util-hex-encoding": { + "version": "3.0.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-model-transformer": "3.0.2", - "@aws-amplify/graphql-transformer-core": "3.1.0", - "@aws-amplify/graphql-transformer-interfaces": "4.1.0", - "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1" + "tslib": "^2.6.2" }, - "peerDependencies": { - "aws-cdk-lib": "^2.152.0", - "constructs": "^10.3.0" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-sql-transformer": { - "version": "0.4.2", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/util-middleware": { + "version": "3.0.3", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-model-transformer": "3.0.2", - "@aws-amplify/graphql-transformer-core": "3.1.0", - "@aws-amplify/graphql-transformer-interfaces": "4.1.0", - "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1" + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "aws-cdk-lib": "^2.152.0", - "constructs": "^10.3.0" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-transformer": { - "version": "2.1.0", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/util-retry": { + "version": "3.0.3", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-auth-transformer": "4.1.0", - "@aws-amplify/graphql-conversation-transformer": "0.2.0", - "@aws-amplify/graphql-default-value-transformer": "3.0.2", - "@aws-amplify/graphql-function-transformer": "3.0.2", - "@aws-amplify/graphql-generation-transformer": "0.2.0", - "@aws-amplify/graphql-http-transformer": "3.0.2", - "@aws-amplify/graphql-index-transformer": "3.0.2", - "@aws-amplify/graphql-maps-to-transformer": "4.0.2", - "@aws-amplify/graphql-model-transformer": "3.0.2", - "@aws-amplify/graphql-predictions-transformer": "3.0.2", - "@aws-amplify/graphql-relational-transformer": "3.0.2", - "@aws-amplify/graphql-searchable-transformer": "3.0.2", - "@aws-amplify/graphql-sql-transformer": "0.4.2", - "@aws-amplify/graphql-transformer-core": "3.1.0", - "@aws-amplify/graphql-transformer-interfaces": "4.1.0" + "@smithy/service-error-classification": "^3.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "aws-cdk-lib": "^2.152.0", - "constructs": "^10.3.0" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-transformer-core": { - "version": "3.1.0", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/util-stream": { + "version": "3.1.3", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-transformer-interfaces": "4.1.0", - "fs-extra": "^8.1.0", - "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1", - "hjson": "^3.2.2", - "lodash": "^4.17.21", - "md5": "^2.3.0", - "object-hash": "^3.0.0", - "ts-dedent": "^2.0.0" + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/types": "^3.3.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "aws-cdk-lib": "^2.152.0", - "constructs": "^10.3.0" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-transformer-interfaces": { - "version": "4.1.0", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/util-uri-escape": { + "version": "3.0.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "graphql": "^15.5.0" + "tslib": "^2.6.2" }, - "peerDependencies": { - "aws-cdk-lib": "^2.152.0", - "constructs": "^10.3.0" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/platform-core": { - "version": "0.2.0", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/util-utf8": { + "version": "3.0.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/plugin-types": "^0.4.0" + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/plugin-types": { - "version": "0.4.1", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/bowser": { + "version": "2.11.0", "inBundle": true, - "license": "Apache-2.0", - "peerDependencies": { - "aws-cdk-lib": "^2.103.0", - "constructs": "^10.0.0" - } + "license": "MIT" }, "node_modules/@aws-amplify/graphql-api-construct/node_modules/charenc": { "version": "0.0.2", @@ -1865,6 +4751,20 @@ "node": "*" } }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/ci-info": { + "version": "3.9.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@aws-amplify/graphql-api-construct/node_modules/crypt": { "version": "0.0.2", "inBundle": true, @@ -1873,6 +4773,27 @@ "node": "*" } }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/fast-xml-parser": { + "version": "4.4.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "inBundle": true, + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/@aws-amplify/graphql-api-construct/node_modules/fs-extra": { "version": "8.1.0", "inBundle": true, @@ -1886,29 +4807,13 @@ "node": ">=6 <7 || >=8" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/fs-extra/node_modules/jsonfile": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/fs-extra/node_modules/universalify": { - "version": "0.1.2", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/@aws-amplify/graphql-api-construct/node_modules/graceful-fs": { "version": "4.2.11", "inBundle": true, "license": "ISC" }, "node_modules/@aws-amplify/graphql-api-construct/node_modules/graphql": { - "version": "15.8.0", + "version": "15.9.0", "inBundle": true, "license": "MIT", "engines": { @@ -1949,34 +4854,25 @@ } }, "node_modules/@aws-amplify/graphql-api-construct/node_modules/is-buffer": { - "version": "2.0.5", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "version": "1.1.6", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/is-ci": { + "version": "3.0.1", "inBundle": true, "license": "MIT", - "engines": { - "node": ">=4" + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" } }, "node_modules/@aws-amplify/graphql-api-construct/node_modules/jsonfile": { - "version": "6.1.0", + "version": "4.0.0", "inBundle": true, "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -1991,6 +4887,11 @@ "inBundle": true, "license": "MIT" }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/lodash.mergewith": { + "version": "4.6.2", + "inBundle": true, + "license": "MIT" + }, "node_modules/@aws-amplify/graphql-api-construct/node_modules/md5": { "version": "2.3.0", "inBundle": true, @@ -2001,11 +4902,6 @@ "is-buffer": "~1.1.6" } }, - "node_modules/@aws-amplify/graphql-api-construct/node_modules/md5/node_modules/is-buffer": { - "version": "1.1.6", - "inBundle": true, - "license": "MIT" - }, "node_modules/@aws-amplify/graphql-api-construct/node_modules/object-hash": { "version": "3.0.0", "inBundle": true, @@ -2022,6 +4918,22 @@ "node": ">=4" } }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/semver": { + "version": "7.6.3", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/strnum": { + "version": "1.0.5", + "inBundle": true, + "license": "MIT" + }, "node_modules/@aws-amplify/graphql-api-construct/node_modules/ts-dedent": { "version": "2.2.0", "inBundle": true, @@ -2030,16 +4942,33 @@ "node": ">=6.10" } }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/tslib": { + "version": "2.7.0", + "inBundle": true, + "license": "0BSD" + }, "node_modules/@aws-amplify/graphql-api-construct/node_modules/universalify": { - "version": "2.0.0", + "version": "0.1.2", "inBundle": true, "license": "MIT", "engines": { - "node": ">= 10.0.0" + "node": ">= 4.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/uuid": { + "version": "9.0.1", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "inBundle": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" } }, "node_modules/@aws-amplify/graphql-api-construct/node_modules/zod": { - "version": "3.22.4", + "version": "3.23.8", "inBundle": true, "license": "MIT", "funding": { @@ -2070,17 +4999,20 @@ } }, "node_modules/@aws-amplify/graphql-generator": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@aws-amplify/graphql-generator/-/graphql-generator-0.4.5.tgz", - "integrity": "sha512-yxAxb9KJjUEtgW32nBy2ZzR4bFVe1Em8oR+w63WSnoEmpsW3D0SAa7H2oDocaePPZCVVYC6AsqH5Ne6Q3i608Q==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@aws-amplify/graphql-generator/-/graphql-generator-0.5.1.tgz", + "integrity": "sha512-30t/1QvK6klDHL30IJ8/S6nGkfZNC4s534U0y6rbYGhMSpKtmWy63HozxAwxz5HBUzkom+HmWIMHdLW+UVgQeA==", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/appsync-modelgen-plugin": "2.13.0", + "@aws-amplify/appsync-modelgen-plugin": "2.15.0", "@aws-amplify/graphql-directives": "^1.0.1", "@aws-amplify/graphql-docs-generator": "4.2.1", "@aws-amplify/graphql-types-generator": "3.6.0", "@graphql-codegen/core": "^2.6.6", + "@graphql-codegen/plugin-helpers": "^3.1.1", "@graphql-tools/apollo-engine-loader": "^8.0.0", + "@graphql-tools/schema": "^9.0.0", + "@graphql-tools/utils": "^9.2.1", "graphql": "^15.5.0", "prettier": "^1.19.1" } @@ -2100,7 +5032,7 @@ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@aws-amplify/graphql-generator/node_modules/@graphql-codegen/core/node_modules/@graphql-codegen/plugin-helpers": { + "node_modules/@aws-amplify/graphql-generator/node_modules/@graphql-codegen/plugin-helpers": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-3.1.2.tgz", "integrity": "sha512-emOQiHyIliVOIjKVKdsI5MXj312zmRDwmHpyUTZMjfpvxq/UVAHUJIVdVf+lnjjrI+LXBTgMlTWTgHQfmICxjg==", @@ -2117,7 +5049,7 @@ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@aws-amplify/graphql-generator/node_modules/@graphql-codegen/core/node_modules/@graphql-tools/schema": { + "node_modules/@aws-amplify/graphql-generator/node_modules/@graphql-tools/schema": { "version": "9.0.19", "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-9.0.19.tgz", "integrity": "sha512-oBRPoNBtCkk0zbUsyP4GaIzCt8C0aCI4ycIRUL67KK5pOHljKLBBtGT+Jr6hkzA74C8Gco8bpZPe7aWFjiaK2w==", @@ -2132,7 +5064,7 @@ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@aws-amplify/graphql-generator/node_modules/@graphql-codegen/core/node_modules/@graphql-tools/schema/node_modules/@graphql-tools/merge": { + "node_modules/@aws-amplify/graphql-generator/node_modules/@graphql-tools/schema/node_modules/@graphql-tools/merge": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.4.2.tgz", "integrity": "sha512-XbrHAaj8yDuINph+sAfuq3QCZ/tKblrTLOpirK0+CAgNlZUCHs0Fa+xtMUURgwCVThLle1AF7svJCxFizygLsw==", @@ -2145,7 +5077,7 @@ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@aws-amplify/graphql-generator/node_modules/@graphql-codegen/core/node_modules/@graphql-tools/utils": { + "node_modules/@aws-amplify/graphql-generator/node_modules/@graphql-tools/utils": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.2.1.tgz", "integrity": "sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==", @@ -2195,13 +5127,13 @@ "license": "0BSD" }, "node_modules/@aws-amplify/graphql-schema-generator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/@aws-amplify/graphql-schema-generator/-/graphql-schema-generator-0.9.4.tgz", - "integrity": "sha512-GXoPOes5Sj93p7RWunJlMdxPQyoh+dBaJq3qpQUOSYQU1UxUqAstnD+gqAWEG58opiupHby7jTIi1ljK1e9CrQ==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@aws-amplify/graphql-schema-generator/-/graphql-schema-generator-0.11.0.tgz", + "integrity": "sha512-c5pDuoh8UWD0qQ2N4HjR3ZC/JO6ai8DrsK40oQKwQhG2V/VkxUGdqsg0B9nYiKepxiTw0gXabLq8JfwW4o8uBg==", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-transformer-core": "2.9.3", - "@aws-amplify/graphql-transformer-interfaces": "3.10.1", + "@aws-amplify/graphql-transformer-core": "3.2.2", + "@aws-amplify/graphql-transformer-interfaces": "4.1.2", "@aws-sdk/client-ec2": "3.624.0", "@aws-sdk/client-iam": "3.624.0", "@aws-sdk/client-lambda": "3.624.0", @@ -2209,7 +5141,7 @@ "csv-parse": "^5.5.2", "fs-extra": "11.1.1", "graphql": "^15.5.0", - "graphql-transformer-common": "4.31.1", + "graphql-transformer-common": "5.1.0", "knex": "~2.4.0", "mysql2": "~3.9.7", "ora": "^4.0.3", @@ -2837,6 +5769,24 @@ "node": ">=14.14" } }, + "node_modules/@aws-amplify/graphql-schema-generator/node_modules/graphql-mapping-template": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/graphql-mapping-template/-/graphql-mapping-template-5.0.1.tgz", + "integrity": "sha512-hgFkXUS6Q35zE/uyPGIZYof2kutwTZmVqwJfnQofiCYWRRQS0zjzUdyqmOcCBkbJB4Zi7G7mXcl3fSIs5I5vgA==", + "license": "Apache-2.0" + }, + "node_modules/@aws-amplify/graphql-schema-generator/node_modules/graphql-transformer-common": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/graphql-transformer-common/-/graphql-transformer-common-5.1.0.tgz", + "integrity": "sha512-i1Ja0bjlsrSNT5TzjGOrPyxYGJPTutDOLTJENcGC47+KYzMfQS80KpVpUZlIVlcCbDYeSZbv8HaMtJlJpmjbmw==", + "license": "Apache-2.0", + "dependencies": { + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.1", + "md5": "^2.2.1", + "pluralize": "8.0.0" + } + }, "node_modules/@aws-amplify/graphql-schema-generator/node_modules/typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", @@ -2851,17 +5801,17 @@ } }, "node_modules/@aws-amplify/graphql-transformer-core": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/@aws-amplify/graphql-transformer-core/-/graphql-transformer-core-2.9.3.tgz", - "integrity": "sha512-gz9PbNTqsyQQn6W5d4HPN/pafvFH7spwd6R/hImisEBFD+80liJc/21nBC8UgUMPu2eXVZrsiWBfWnO8Rbqomg==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@aws-amplify/graphql-transformer-core/-/graphql-transformer-core-3.2.2.tgz", + "integrity": "sha512-nHocW0Uy/pHrrt5iMFMzz+9IsJKnaPk9BcWZHcQSJ/9F0Kn0s/vIFT5/Ee2nJFN/h0VK3fTkT9QKOuiQ4UH3Jg==", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "1.1.0", - "@aws-amplify/graphql-transformer-interfaces": "3.10.1", + "@aws-amplify/graphql-directives": "2.4.0", + "@aws-amplify/graphql-transformer-interfaces": "4.1.2", "fs-extra": "^8.1.0", "graphql": "^15.5.0", - "graphql-mapping-template": "4.20.16", - "graphql-transformer-common": "4.31.1", + "graphql-mapping-template": "5.0.1", + "graphql-transformer-common": "5.1.0", "hjson": "^3.2.2", "lodash": "^4.17.21", "md5": "^2.3.0", @@ -2869,10 +5819,16 @@ "ts-dedent": "^2.0.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.129.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.3.0" } }, + "node_modules/@aws-amplify/graphql-transformer-core/node_modules/@aws-amplify/graphql-directives": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@aws-amplify/graphql-directives/-/graphql-directives-2.4.0.tgz", + "integrity": "sha512-+oO9Lb22eIuS8rvLOR+x4F79J5aCF1GIkqYS0paRUTw78NjLTOq1LWjtGMYAfLpbHgoYtrkC2zwpw7sHbmNnzQ==", + "license": "Apache-2.0" + }, "node_modules/@aws-amplify/graphql-transformer-core/node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -2887,6 +5843,24 @@ "node": ">=6 <7 || >=8" } }, + "node_modules/@aws-amplify/graphql-transformer-core/node_modules/graphql-mapping-template": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/graphql-mapping-template/-/graphql-mapping-template-5.0.1.tgz", + "integrity": "sha512-hgFkXUS6Q35zE/uyPGIZYof2kutwTZmVqwJfnQofiCYWRRQS0zjzUdyqmOcCBkbJB4Zi7G7mXcl3fSIs5I5vgA==", + "license": "Apache-2.0" + }, + "node_modules/@aws-amplify/graphql-transformer-core/node_modules/graphql-transformer-common": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/graphql-transformer-common/-/graphql-transformer-common-5.1.0.tgz", + "integrity": "sha512-i1Ja0bjlsrSNT5TzjGOrPyxYGJPTutDOLTJENcGC47+KYzMfQS80KpVpUZlIVlcCbDYeSZbv8HaMtJlJpmjbmw==", + "license": "Apache-2.0", + "dependencies": { + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.1", + "md5": "^2.2.1", + "pluralize": "8.0.0" + } + }, "node_modules/@aws-amplify/graphql-transformer-core/node_modules/jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -2915,15 +5889,15 @@ } }, "node_modules/@aws-amplify/graphql-transformer-interfaces": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@aws-amplify/graphql-transformer-interfaces/-/graphql-transformer-interfaces-3.10.1.tgz", - "integrity": "sha512-daf+cpOSw3lKiS+Tpc5Oo5H+FCkHi/8z+0mAR/greQGPJWzcHv9j2u1Jiy36UvI01ypOhHme58pAs/fKWLWDBQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@aws-amplify/graphql-transformer-interfaces/-/graphql-transformer-interfaces-4.1.2.tgz", + "integrity": "sha512-fW4BIo2stFYOc6LDrSDKW0NTKmBp/c+UJUG5YjDef5fUUTbE8RZMzUGgSjgzDgwXpAT8CyYuncqMLchVkQSFFQ==", "license": "Apache-2.0", "dependencies": { "graphql": "^15.5.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.129.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.3.0" } }, @@ -3203,9 +6177,9 @@ "license": "Apache-2.0" }, "node_modules/@aws-cdk/cloud-assembly-schema": { - "version": "36.0.25", - "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-36.0.25.tgz", - "integrity": "sha512-AK86v4IMV4zcWfp392e3wlaVJPT72/dk39Lo2SDDFxQR+sikMOyY2IGrULyhK1TwQmPiyxM7QB/0MkTbMDAPrw==", + "version": "38.0.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-38.0.1.tgz", + "integrity": "sha512-KvPe+NMWAulfNVwY7jenFhzhuLhLqJ/OPy5jx7wUstbjnYnjRVLpUHPU3yCjXFE0J8cuJVdx95BJ4rOs66Pi9w==", "bundleDependencies": [ "jsonschema", "semver" @@ -3214,9 +6188,6 @@ "dependencies": { "jsonschema": "^1.4.1", "semver": "^7.6.3" - }, - "engines": { - "node": ">= 18.18.0" } }, "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { @@ -3931,45 +6902,593 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/node-config-provider": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.5.tgz", - "integrity": "sha512-dq/oR3/LxgCgizVk7in7FGTm0w9a3qM4mg3IIXLTCHeW3fV+ipssSvBZ2bvEx1+asfQJTyCnVLeYf7JKfd9v3Q==", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/node-config-provider": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.5.tgz", + "integrity": "sha512-dq/oR3/LxgCgizVk7in7FGTm0w9a3qM4mg3IIXLTCHeW3fV+ipssSvBZ2bvEx1+asfQJTyCnVLeYf7JKfd9v3Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^3.1.4", + "@smithy/shared-ini-file-loader": "^3.1.5", + "@smithy/types": "^3.4.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/shared-ini-file-loader": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.5.tgz", + "integrity": "sha512-6jxsJ4NOmY5Du4FD0enYegNJl4zTSuKLiChIMqIkh+LapxiP7lmz5lYUNLE9/4cvA65mbBmtdzZ8yxmcqM5igg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.4.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudformation/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@aws-sdk/client-cloudtrail": { + "version": "3.658.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudtrail/-/client-cloudtrail-3.658.1.tgz", + "integrity": "sha512-OWc5A0zRntybmYsogI+9MjKLbbAz57Mg6gQuyxJJO0d1njKGZfaYn+fYXfI5wEHe4InydbFQuiZOD0LUvXtQMw==", + "dev": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.658.1", + "@aws-sdk/client-sts": "3.658.1", + "@aws-sdk/core": "3.658.1", + "@aws-sdk/credential-provider-node": "3.658.1", + "@aws-sdk/middleware-host-header": "3.654.0", + "@aws-sdk/middleware-logger": "3.654.0", + "@aws-sdk/middleware-recursion-detection": "3.654.0", + "@aws-sdk/middleware-user-agent": "3.654.0", + "@aws-sdk/region-config-resolver": "3.654.0", + "@aws-sdk/types": "3.654.0", + "@aws-sdk/util-endpoints": "3.654.0", + "@aws-sdk/util-user-agent-browser": "3.654.0", + "@aws-sdk/util-user-agent-node": "3.654.0", + "@smithy/config-resolver": "^3.0.8", + "@smithy/core": "^2.4.6", + "@smithy/fetch-http-handler": "^3.2.8", + "@smithy/hash-node": "^3.0.6", + "@smithy/invalid-dependency": "^3.0.6", + "@smithy/middleware-content-length": "^3.0.8", + "@smithy/middleware-endpoint": "^3.1.3", + "@smithy/middleware-retry": "^3.0.21", + "@smithy/middleware-serde": "^3.0.6", + "@smithy/middleware-stack": "^3.0.6", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/node-http-handler": "^3.2.3", + "@smithy/protocol-http": "^4.1.3", + "@smithy/smithy-client": "^3.3.5", + "@smithy/types": "^3.4.2", + "@smithy/url-parser": "^3.0.6", + "@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.21", + "@smithy/util-defaults-mode-node": "^3.0.21", + "@smithy/util-endpoints": "^2.1.2", + "@smithy/util-middleware": "^3.0.6", + "@smithy/util-retry": "^3.0.6", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudtrail/node_modules/@aws-sdk/client-sso": { + "version": "3.658.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.658.1.tgz", + "integrity": "sha512-lOuaBtqPTYGn6xpXlQF4LsNDsQ8Ij2kOdnk+i69Kp6yS76TYvtUuukyLL5kx8zE1c8WbYtxj9y8VNw9/6uKl7Q==", + "dev": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.658.1", + "@aws-sdk/middleware-host-header": "3.654.0", + "@aws-sdk/middleware-logger": "3.654.0", + "@aws-sdk/middleware-recursion-detection": "3.654.0", + "@aws-sdk/middleware-user-agent": "3.654.0", + "@aws-sdk/region-config-resolver": "3.654.0", + "@aws-sdk/types": "3.654.0", + "@aws-sdk/util-endpoints": "3.654.0", + "@aws-sdk/util-user-agent-browser": "3.654.0", + "@aws-sdk/util-user-agent-node": "3.654.0", + "@smithy/config-resolver": "^3.0.8", + "@smithy/core": "^2.4.6", + "@smithy/fetch-http-handler": "^3.2.8", + "@smithy/hash-node": "^3.0.6", + "@smithy/invalid-dependency": "^3.0.6", + "@smithy/middleware-content-length": "^3.0.8", + "@smithy/middleware-endpoint": "^3.1.3", + "@smithy/middleware-retry": "^3.0.21", + "@smithy/middleware-serde": "^3.0.6", + "@smithy/middleware-stack": "^3.0.6", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/node-http-handler": "^3.2.3", + "@smithy/protocol-http": "^4.1.3", + "@smithy/smithy-client": "^3.3.5", + "@smithy/types": "^3.4.2", + "@smithy/url-parser": "^3.0.6", + "@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.21", + "@smithy/util-defaults-mode-node": "^3.0.21", + "@smithy/util-endpoints": "^2.1.2", + "@smithy/util-middleware": "^3.0.6", + "@smithy/util-retry": "^3.0.6", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudtrail/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.658.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.658.1.tgz", + "integrity": "sha512-RGcZAI3qEA05JszPKwa0cAyp8rnS1nUvs0Sqw4hqLNQ1kD7b7V6CPjRXe7EFQqCOMvM4kGqx0+cEEVTOmBsFLw==", + "dev": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.658.1", + "@aws-sdk/credential-provider-node": "3.658.1", + "@aws-sdk/middleware-host-header": "3.654.0", + "@aws-sdk/middleware-logger": "3.654.0", + "@aws-sdk/middleware-recursion-detection": "3.654.0", + "@aws-sdk/middleware-user-agent": "3.654.0", + "@aws-sdk/region-config-resolver": "3.654.0", + "@aws-sdk/types": "3.654.0", + "@aws-sdk/util-endpoints": "3.654.0", + "@aws-sdk/util-user-agent-browser": "3.654.0", + "@aws-sdk/util-user-agent-node": "3.654.0", + "@smithy/config-resolver": "^3.0.8", + "@smithy/core": "^2.4.6", + "@smithy/fetch-http-handler": "^3.2.8", + "@smithy/hash-node": "^3.0.6", + "@smithy/invalid-dependency": "^3.0.6", + "@smithy/middleware-content-length": "^3.0.8", + "@smithy/middleware-endpoint": "^3.1.3", + "@smithy/middleware-retry": "^3.0.21", + "@smithy/middleware-serde": "^3.0.6", + "@smithy/middleware-stack": "^3.0.6", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/node-http-handler": "^3.2.3", + "@smithy/protocol-http": "^4.1.3", + "@smithy/smithy-client": "^3.3.5", + "@smithy/types": "^3.4.2", + "@smithy/url-parser": "^3.0.6", + "@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.21", + "@smithy/util-defaults-mode-node": "^3.0.21", + "@smithy/util-endpoints": "^2.1.2", + "@smithy/util-middleware": "^3.0.6", + "@smithy/util-retry": "^3.0.6", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.658.1" + } + }, + "node_modules/@aws-sdk/client-cloudtrail/node_modules/@aws-sdk/client-sts": { + "version": "3.658.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.658.1.tgz", + "integrity": "sha512-yw9hc5blTnbT1V6mR7Cx9HGc9KQpcLQ1QXj8rntiJi6tIYu3aFNVEyy81JHL7NsuBSeQulJTvHO3y6r3O0sfRg==", + "dev": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.658.1", + "@aws-sdk/core": "3.658.1", + "@aws-sdk/credential-provider-node": "3.658.1", + "@aws-sdk/middleware-host-header": "3.654.0", + "@aws-sdk/middleware-logger": "3.654.0", + "@aws-sdk/middleware-recursion-detection": "3.654.0", + "@aws-sdk/middleware-user-agent": "3.654.0", + "@aws-sdk/region-config-resolver": "3.654.0", + "@aws-sdk/types": "3.654.0", + "@aws-sdk/util-endpoints": "3.654.0", + "@aws-sdk/util-user-agent-browser": "3.654.0", + "@aws-sdk/util-user-agent-node": "3.654.0", + "@smithy/config-resolver": "^3.0.8", + "@smithy/core": "^2.4.6", + "@smithy/fetch-http-handler": "^3.2.8", + "@smithy/hash-node": "^3.0.6", + "@smithy/invalid-dependency": "^3.0.6", + "@smithy/middleware-content-length": "^3.0.8", + "@smithy/middleware-endpoint": "^3.1.3", + "@smithy/middleware-retry": "^3.0.21", + "@smithy/middleware-serde": "^3.0.6", + "@smithy/middleware-stack": "^3.0.6", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/node-http-handler": "^3.2.3", + "@smithy/protocol-http": "^4.1.3", + "@smithy/smithy-client": "^3.3.5", + "@smithy/types": "^3.4.2", + "@smithy/url-parser": "^3.0.6", + "@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.21", + "@smithy/util-defaults-mode-node": "^3.0.21", + "@smithy/util-endpoints": "^2.1.2", + "@smithy/util-middleware": "^3.0.6", + "@smithy/util-retry": "^3.0.6", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudtrail/node_modules/@aws-sdk/core": { + "version": "3.658.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.658.1.tgz", + "integrity": "sha512-vJVMoMcSKXK2gBRSu9Ywwv6wQ7tXH8VL1fqB1uVxgCqBZ3IHfqNn4zvpMPWrwgO2/3wv7XFyikGQ5ypPTCw4jA==", + "dev": true, + "dependencies": { + "@smithy/core": "^2.4.6", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/property-provider": "^3.1.6", + "@smithy/protocol-http": "^4.1.3", + "@smithy/signature-v4": "^4.1.4", + "@smithy/smithy-client": "^3.3.5", + "@smithy/types": "^3.4.2", + "@smithy/util-middleware": "^3.0.6", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudtrail/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.654.0.tgz", + "integrity": "sha512-kogsx3Ql81JouHS7DkheCDU9MYAvK0AokxjcshDveGmf7BbgbWCA8Fnb9wjQyNDaOXNvkZu8Z8rgkX91z324/w==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/property-provider": "^3.1.6", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudtrail/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.658.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.658.1.tgz", + "integrity": "sha512-4ubkJjEVCZflxkZnV1JDQv8P2pburxk1LrEp55telfJRzXrnowzBKwuV2ED0QMNC448g2B3VCaffS+Ct7c4IWQ==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/fetch-http-handler": "^3.2.8", + "@smithy/node-http-handler": "^3.2.3", + "@smithy/property-provider": "^3.1.6", + "@smithy/protocol-http": "^4.1.3", + "@smithy/smithy-client": "^3.3.5", + "@smithy/types": "^3.4.2", + "@smithy/util-stream": "^3.1.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudtrail/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.658.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.658.1.tgz", + "integrity": "sha512-2uwOamQg5ppwfegwen1ddPu5HM3/IBSnaGlaKLFhltkdtZ0jiqTZWUtX2V+4Q+buLnT0hQvLS/frQ+7QUam+0Q==", + "dev": true, + "dependencies": { + "@aws-sdk/credential-provider-env": "3.654.0", + "@aws-sdk/credential-provider-http": "3.658.1", + "@aws-sdk/credential-provider-process": "3.654.0", + "@aws-sdk/credential-provider-sso": "3.658.1", + "@aws-sdk/credential-provider-web-identity": "3.654.0", + "@aws-sdk/types": "3.654.0", + "@smithy/credential-provider-imds": "^3.2.3", + "@smithy/property-provider": "^3.1.6", + "@smithy/shared-ini-file-loader": "^3.1.7", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.658.1" + } + }, + "node_modules/@aws-sdk/client-cloudtrail/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.658.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.658.1.tgz", + "integrity": "sha512-XwxW6N+uPXPYAuyq+GfOEdfL/MZGAlCSfB5gEWtLBFmFbikhmEuqfWtI6CD60OwudCUOh6argd21BsJf8o1SJA==", + "dev": true, + "dependencies": { + "@aws-sdk/credential-provider-env": "3.654.0", + "@aws-sdk/credential-provider-http": "3.658.1", + "@aws-sdk/credential-provider-ini": "3.658.1", + "@aws-sdk/credential-provider-process": "3.654.0", + "@aws-sdk/credential-provider-sso": "3.658.1", + "@aws-sdk/credential-provider-web-identity": "3.654.0", + "@aws-sdk/types": "3.654.0", + "@smithy/credential-provider-imds": "^3.2.3", + "@smithy/property-provider": "^3.1.6", + "@smithy/shared-ini-file-loader": "^3.1.7", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudtrail/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.654.0.tgz", + "integrity": "sha512-PmQoo8sZ9Q2Ow8OMzK++Z9lI7MsRUG7sNq3E72DVA215dhtTICTDQwGlXH2AAmIp7n+G9LLRds+4wo2ehG4mkg==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/property-provider": "^3.1.6", + "@smithy/shared-ini-file-loader": "^3.1.7", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudtrail/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.658.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.658.1.tgz", + "integrity": "sha512-YOagVEsZEk9DmgJEBg+4MBXrPcw/tYas0VQ5OVBqC5XHNbi2OBGJqgmjVPesuu393E7W0VQxtJFDS00O1ewQgA==", + "dev": true, + "dependencies": { + "@aws-sdk/client-sso": "3.658.1", + "@aws-sdk/token-providers": "3.654.0", + "@aws-sdk/types": "3.654.0", + "@smithy/property-provider": "^3.1.6", + "@smithy/shared-ini-file-loader": "^3.1.7", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudtrail/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.654.0.tgz", + "integrity": "sha512-6a2g9gMtZToqSu+CusjNK5zvbLJahQ9di7buO3iXgbizXpLXU1rnawCpWxwslMpT5fLgMSKDnKDrr6wdEk7jSw==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/property-provider": "^3.1.6", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.654.0" + } + }, + "node_modules/@aws-sdk/client-cloudtrail/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.654.0.tgz", + "integrity": "sha512-rxGgVHWKp8U2ubMv+t+vlIk7QYUaRCHaVpmUlJv0Wv6Q0KeO9a42T9FxHphjOTlCGQOLcjCreL9CF8Qhtb4mdQ==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/protocol-http": "^4.1.3", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudtrail/node_modules/@aws-sdk/middleware-logger": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.654.0.tgz", + "integrity": "sha512-OQYb+nWlmASyXfRb989pwkJ9EVUMP1CrKn2eyTk3usl20JZmKo2Vjis6I0tLUkMSxMhnBJJlQKyWkRpD/u1FVg==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudtrail/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.654.0.tgz", + "integrity": "sha512-gKSomgltKVmsT8sC6W7CrADZ4GHwX9epk3GcH6QhebVO3LA9LRbkL3TwOPUXakxxOLLUTYdOZLIOtFf7iH00lg==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/protocol-http": "^4.1.3", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudtrail/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.654.0.tgz", + "integrity": "sha512-liCcqPAyRsr53cy2tYu4qeH4MMN0eh9g6k56XzI5xd4SghXH5YWh4qOYAlQ8T66ZV4nPMtD8GLtLXGzsH8moFg==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@aws-sdk/util-endpoints": "3.654.0", + "@smithy/protocol-http": "^4.1.3", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudtrail/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.654.0.tgz", + "integrity": "sha512-ydGOrXJxj3x0sJhsXyTmvJVLAE0xxuTWFJihTl67RtaO7VRNtd82I3P3bwoMMaDn5WpmV5mPo8fEUDRlBm3fPg==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/types": "^3.4.2", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudtrail/node_modules/@aws-sdk/token-providers": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.654.0.tgz", + "integrity": "sha512-D8GeJYmvbfWkQDtTB4owmIobSMexZel0fOoetwvgCQ/7L8VPph3Q2bn1TRRIXvH7wdt6DcDxA3tKMHPBkT3GlA==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/property-provider": "^3.1.6", + "@smithy/shared-ini-file-loader": "^3.1.7", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.654.0" + } + }, + "node_modules/@aws-sdk/client-cloudtrail/node_modules/@aws-sdk/types": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.654.0.tgz", + "integrity": "sha512-VWvbED3SV+10QJIcmU/PKjsKilsTV16d1I7/on4bvD/jo1qGeMXqLDBSen3ks/tuvXZF/mFc7ZW/W2DiLVtO7A==", + "dev": true, + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudtrail/node_modules/@aws-sdk/util-endpoints": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.654.0.tgz", + "integrity": "sha512-i902fcBknHs0Irgdpi62+QMvzxE+bczvILXigYrlHL4+PiEnlMVpni5L5W1qCkNZXf8AaMrSBuR1NZAGp6UOUw==", + "dev": true, "dependencies": { - "@smithy/property-provider": "^3.1.4", - "@smithy/shared-ini-file-loader": "^3.1.5", - "@smithy/types": "^3.4.0", + "@aws-sdk/types": "3.654.0", + "@smithy/types": "^3.4.2", + "@smithy/util-endpoints": "^2.1.2", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudformation/node_modules/@smithy/shared-ini-file-loader": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.5.tgz", - "integrity": "sha512-6jxsJ4NOmY5Du4FD0enYegNJl4zTSuKLiChIMqIkh+LapxiP7lmz5lYUNLE9/4cvA65mbBmtdzZ8yxmcqM5igg==", - "license": "Apache-2.0", + "node_modules/@aws-sdk/client-cloudtrail/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.654.0.tgz", + "integrity": "sha512-ykYAJqvnxLt7wfrqya28wuH3/7NdrwzfiFd7NqEVQf7dXVxL5RPEpD7DxjcyQo3DsHvvdUvGZVaQhozycn1pzA==", + "dev": true, "dependencies": { - "@smithy/types": "^3.4.0", + "@aws-sdk/types": "3.654.0", + "@smithy/types": "^3.4.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cloudtrail/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.654.0.tgz", + "integrity": "sha512-a0ojjdBN6pqv6gB4H/QPPSfhs7mFtlVwnmKCM/QrTaFzN0U810PJ1BST3lBx5sa23I5jWHGaoFY+5q65C3clLQ==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/types": "^3.4.2", + "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-cloudtrail/node_modules/@smithy/node-config-provider": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.8.tgz", + "integrity": "sha512-E0rU0DglpeJn5ge64mk8wTGEXcQwmpUTY5Zr7IzTpDLmHKiIamINERNZYrPQjg58Ck236sEKSwRSHA4CwshU6Q==", + "dev": true, + "dependencies": { + "@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/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" + "node_modules/@aws-sdk/client-cloudtrail/node_modules/@smithy/shared-ini-file-loader": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.8.tgz", + "integrity": "sha512-0NHdQiSkeGl0ICQKcJQ2lCOKH23Nb0EaAa7RDRId6ZqwXkw4LJyIyZ0t3iusD4bnKYDPLGy2/5e2rfUhrt0Acw==", + "dev": true, + "dependencies": { + "@smithy/types": "^3.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" } }, "node_modules/@aws-sdk/client-cloudwatch-logs": { @@ -9925,15 +13444,14 @@ "license": "MIT" }, "node_modules/@changesets/apply-release-plan": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.0.5.tgz", - "integrity": "sha512-1cWCk+ZshEkSVEZrm2fSj1Gz8sYvxgUL4Q78+1ZZqeqfuevPTPk033/yUZ3df8BKMohkqqHfzj0HOOrG0KtXTw==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.0.6.tgz", + "integrity": "sha512-TKhVLtiwtQOgMAC0fCJfmv93faiViKSDqr8oMEqrnNs99gtSC1sZh/aEMS9a+dseU1ESZRCK+ofLgGY7o0fw/Q==", "dev": true, - "license": "MIT", "dependencies": { - "@changesets/config": "^3.0.3", + "@changesets/config": "^3.0.4", "@changesets/get-version-range-type": "^0.4.0", - "@changesets/git": "^3.0.1", + "@changesets/git": "^3.0.2", "@changesets/should-skip-package": "^0.1.1", "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3", @@ -9951,7 +13469,6 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, - "license": "MIT", "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -9966,7 +13483,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, - "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -9976,17 +13492,15 @@ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4.0.0" } }, "node_modules/@changesets/assemble-release-plan": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.4.tgz", - "integrity": "sha512-nqICnvmrwWj4w2x0fOhVj2QEGdlUuwVAwESrUo5HLzWMI1rE5SWfsr9ln+rDqWB6RQ2ZyaMZHUcU7/IRaUJS+Q==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.5.tgz", + "integrity": "sha512-IgvBWLNKZd6k4t72MBTBK3nkygi0j3t3zdC1zrfusYo0KpdsvnDjrMM9vPnTCLCMlfNs55jRL4gIMybxa64FCQ==", "dev": true, - "license": "MIT", "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.2", @@ -10007,41 +13521,38 @@ } }, "node_modules/@changesets/cli": { - "version": "2.27.8", - "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.27.8.tgz", - "integrity": "sha512-gZNyh+LdSsI82wBSHLQ3QN5J30P4uHKJ4fXgoGwQxfXwYFTJzDdvIJasZn8rYQtmKhyQuiBj4SSnLuKlxKWq4w==", + "version": "2.27.10", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.27.10.tgz", + "integrity": "sha512-PfeXjvs9OfQJV8QSFFHjwHX3QnUL9elPEQ47SgkiwzLgtKGyuikWjrdM+lO9MXzOE22FO9jEGkcs4b+B6D6X0Q==", "dev": true, - "license": "MIT", "dependencies": { - "@changesets/apply-release-plan": "^7.0.5", - "@changesets/assemble-release-plan": "^6.0.4", + "@changesets/apply-release-plan": "^7.0.6", + "@changesets/assemble-release-plan": "^6.0.5", "@changesets/changelog-git": "^0.2.0", - "@changesets/config": "^3.0.3", + "@changesets/config": "^3.0.4", "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.2", - "@changesets/get-release-plan": "^4.0.4", - "@changesets/git": "^3.0.1", + "@changesets/get-release-plan": "^4.0.5", + "@changesets/git": "^3.0.2", "@changesets/logger": "^0.1.1", "@changesets/pre": "^2.0.1", - "@changesets/read": "^0.6.1", + "@changesets/read": "^0.6.2", "@changesets/should-skip-package": "^0.1.1", "@changesets/types": "^6.0.0", "@changesets/write": "^0.3.2", "@manypkg/get-packages": "^1.1.3", - "@types/semver": "^7.5.0", "ansi-colors": "^4.1.3", "ci-info": "^3.7.0", "enquirer": "^2.3.0", "external-editor": "^3.1.0", "fs-extra": "^7.0.1", "mri": "^1.2.0", - "outdent": "^0.5.0", "p-limit": "^2.2.0", "package-manager-detector": "^0.2.0", "picocolors": "^1.1.0", "resolve-from": "^5.0.0", "semver": "^7.5.3", - "spawndamnit": "^2.0.0", + "spawndamnit": "^3.0.1", "term-size": "^2.1.0" }, "bin": { @@ -10084,11 +13595,10 @@ } }, "node_modules/@changesets/config": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@changesets/config/-/config-3.0.3.tgz", - "integrity": "sha512-vqgQZMyIcuIpw9nqFIpTSNyc/wgm/Lu1zKN5vECy74u95Qx/Wa9g27HdgO4NkVAaq+BGA8wUc/qvbvVNs93n6A==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@changesets/config/-/config-3.0.4.tgz", + "integrity": "sha512-+DiIwtEBpvvv1z30f8bbOsUQGuccnZl9KRKMM/LxUHuDu5oEjmN+bJQ1RIBKNJjfYMQn8RZzoPiX0UgPaLQyXw==", "dev": true, - "license": "MIT", "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.2", @@ -10096,7 +13606,7 @@ "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1", - "micromatch": "^4.0.2" + "micromatch": "^4.0.8" } }, "node_modules/@changesets/config/node_modules/fs-extra": { @@ -10158,16 +13668,15 @@ } }, "node_modules/@changesets/get-release-plan": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.4.tgz", - "integrity": "sha512-SicG/S67JmPTrdcc9Vpu0wSQt7IiuN0dc8iR5VScnnTVPfIaLvKmEGRvIaF0kcn8u5ZqLbormZNTO77bCEvyWw==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.5.tgz", + "integrity": "sha512-E6wW7JoSMcctdVakut0UB76FrrN3KIeJSXvB+DHMFo99CnC3ZVnNYDCVNClMlqAhYGmLmAj77QfApaI3ca4Fkw==", "dev": true, - "license": "MIT", "dependencies": { - "@changesets/assemble-release-plan": "^6.0.4", - "@changesets/config": "^3.0.3", + "@changesets/assemble-release-plan": "^6.0.5", + "@changesets/config": "^3.0.4", "@changesets/pre": "^2.0.1", - "@changesets/read": "^0.6.1", + "@changesets/read": "^0.6.2", "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3" } @@ -10176,21 +13685,19 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.4.0.tgz", "integrity": "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@changesets/git": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@changesets/git/-/git-3.0.1.tgz", - "integrity": "sha512-pdgHcYBLCPcLd82aRcuO0kxCDbw/yISlOtkmwmE8Odo1L6hSiZrBOsRl84eYG7DRCab/iHnOkWqExqc4wxk2LQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@changesets/git/-/git-3.0.2.tgz", + "integrity": "sha512-r1/Kju9Y8OxRRdvna+nxpQIsMsRQn9dhhAZt94FLDeu0Hij2hnOozW8iqnHBgvu+KdnJppCveQwK4odwfw/aWQ==", "dev": true, - "license": "MIT", "dependencies": { "@changesets/errors": "^0.2.0", "@manypkg/get-packages": "^1.1.3", "is-subdir": "^1.1.1", - "micromatch": "^4.0.2", - "spawndamnit": "^2.0.0" + "micromatch": "^4.0.8", + "spawndamnit": "^3.0.1" } }, "node_modules/@changesets/logger": { @@ -10208,7 +13715,6 @@ "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.4.0.tgz", "integrity": "sha512-TS/9KG2CdGXS27S+QxbZXgr8uPsP4yNJYb4BC2/NeFUj80Rni3TeD2qwWmabymxmrLo7JEsytXH1FbpKTbvivw==", "dev": true, - "license": "MIT", "dependencies": { "@changesets/types": "^6.0.0", "js-yaml": "^3.13.1" @@ -10263,13 +13769,12 @@ } }, "node_modules/@changesets/read": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.6.1.tgz", - "integrity": "sha512-jYMbyXQk3nwP25nRzQQGa1nKLY0KfoOV7VLgwucI0bUO8t8ZLCr6LZmgjXsiKuRDc+5A6doKPr9w2d+FEJ55zQ==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.6.2.tgz", + "integrity": "sha512-wjfQpJvryY3zD61p8jR87mJdyx2FIhEcdXhKUqkja87toMrP/3jtg/Yg29upN+N4Ckf525/uvV7a4tzBlpk6gg==", "dev": true, - "license": "MIT", "dependencies": { - "@changesets/git": "^3.0.1", + "@changesets/git": "^3.0.2", "@changesets/logger": "^0.1.1", "@changesets/parse": "^0.4.0", "@changesets/types": "^6.0.0", @@ -10283,7 +13788,6 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, - "license": "MIT", "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -10298,7 +13802,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, - "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -10308,7 +13811,6 @@ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4.0.0" } @@ -10380,9 +13882,9 @@ } }, "node_modules/@cypress/request": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", - "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.5.tgz", + "integrity": "sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -10392,14 +13894,14 @@ "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "http-signature": "~1.3.6", + "form-data": "~4.0.0", + "http-signature": "~1.4.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "6.10.4", + "qs": "6.13.0", "safe-buffer": "^5.1.2", "tough-cookie": "^4.1.3", "tunnel-agent": "^0.6.0", @@ -13256,12 +16758,11 @@ "license": "(Unlicense OR Apache-2.0)" }, "node_modules/@smithy/abort-controller": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.2.tgz", - "integrity": "sha512-b5g+PNujlfqIib9BjkNB108NyO5aZM/RXjfOCXRCqXQ1oPnIkfvdORrztbGgCZdPe/BN/MKDlrGA7PafKPM2jw==", - "license": "Apache-2.0", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.5.tgz", + "integrity": "sha512-DhNPnqTqPoG8aZ5dWkFOgsuY+i0GQ3CI6hMmvCoduNsnU9gUZWZBwGfDQsTTB7NvFPkom1df7jMIJWU90kuXXg==", "dependencies": { - "@smithy/types": "^3.4.0", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { @@ -13288,15 +16789,14 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.6.tgz", - "integrity": "sha512-j7HuVNoRd8EhcFp0MzcUb4fG40C7BcyshH+fAd3Jhd8bINNFvEQYBrZoS/SK6Pun9WPlfoI8uuU2SMz8DsEGlA==", - "license": "Apache-2.0", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.9.tgz", + "integrity": "sha512-5d9oBf40qC7n2xUoHmntKLdqsyTMMo/r49+eqSIjJ73eDfEtljAxEhzIQ3bkgXJtR3xiv7YzMT/3FF3ORkjWdg==", "dependencies": { - "@smithy/node-config-provider": "^3.1.5", - "@smithy/types": "^3.4.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.4", + "@smithy/util-middleware": "^3.0.7", "tslib": "^2.6.2" }, "engines": { @@ -13304,14 +16804,13 @@ } }, "node_modules/@smithy/config-resolver/node_modules/@smithy/node-config-provider": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.5.tgz", - "integrity": "sha512-dq/oR3/LxgCgizVk7in7FGTm0w9a3qM4mg3IIXLTCHeW3fV+ipssSvBZ2bvEx1+asfQJTyCnVLeYf7JKfd9v3Q==", - "license": "Apache-2.0", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.8.tgz", + "integrity": "sha512-E0rU0DglpeJn5ge64mk8wTGEXcQwmpUTY5Zr7IzTpDLmHKiIamINERNZYrPQjg58Ck236sEKSwRSHA4CwshU6Q==", "dependencies": { - "@smithy/property-provider": "^3.1.4", - "@smithy/shared-ini-file-loader": "^3.1.5", - "@smithy/types": "^3.4.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": { @@ -13319,12 +16818,11 @@ } }, "node_modules/@smithy/config-resolver/node_modules/@smithy/shared-ini-file-loader": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.5.tgz", - "integrity": "sha512-6jxsJ4NOmY5Du4FD0enYegNJl4zTSuKLiChIMqIkh+LapxiP7lmz5lYUNLE9/4cvA65mbBmtdzZ8yxmcqM5igg==", - "license": "Apache-2.0", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.8.tgz", + "integrity": "sha512-0NHdQiSkeGl0ICQKcJQ2lCOKH23Nb0EaAa7RDRId6ZqwXkw4LJyIyZ0t3iusD4bnKYDPLGy2/5e2rfUhrt0Acw==", "dependencies": { - "@smithy/types": "^3.4.0", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { @@ -13332,19 +16830,18 @@ } }, "node_modules/@smithy/core": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.4.1.tgz", - "integrity": "sha512-7cts7/Oni7aCHebHGiBeWoz5z+vmH+Vx2Z/UW3XtXMslcxI3PEwBZxNinepwZjixS3n12fPc247PHWmjU7ndsQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/middleware-endpoint": "^3.1.1", - "@smithy/middleware-retry": "^3.0.16", - "@smithy/middleware-serde": "^3.0.4", - "@smithy/protocol-http": "^4.1.1", - "@smithy/smithy-client": "^3.3.0", - "@smithy/types": "^3.4.0", + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.4.7.tgz", + "integrity": "sha512-goqMjX+IoVEnHZjYuzu8xwoZjoteMiLXsPHuXPBkWsGwu0o9c3nTjqkUlP1Ez/V8E501aOU7CJ3INk8mQcW2gw==", + "dependencies": { + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-retry": "^3.0.22", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/protocol-http": "^4.1.4", + "@smithy/smithy-client": "^3.3.6", + "@smithy/types": "^3.5.0", "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-middleware": "^3.0.4", + "@smithy/util-middleware": "^3.0.7", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, @@ -13353,15 +16850,14 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.1.tgz", - "integrity": "sha512-4z/oTWpRF2TqQI3aCM89/PWu3kim58XU4kOCTtuTJnoaS4KT95cPWMxbQfTN2vzcOe96SOKO8QouQW/+ESB1fQ==", - "license": "Apache-2.0", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.4.tgz", + "integrity": "sha512-S9bb0EIokfYEuar4kEbLta+ivlKCWOCFsLZuilkNy9i0uEUEHSi47IFLPaxqqCl+0ftKmcOTHayY5nQhAuq7+w==", "dependencies": { - "@smithy/node-config-provider": "^3.1.5", - "@smithy/property-provider": "^3.1.4", - "@smithy/types": "^3.4.0", - "@smithy/url-parser": "^3.0.4", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/property-provider": "^3.1.7", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", "tslib": "^2.6.2" }, "engines": { @@ -13369,14 +16865,13 @@ } }, "node_modules/@smithy/credential-provider-imds/node_modules/@smithy/node-config-provider": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.5.tgz", - "integrity": "sha512-dq/oR3/LxgCgizVk7in7FGTm0w9a3qM4mg3IIXLTCHeW3fV+ipssSvBZ2bvEx1+asfQJTyCnVLeYf7JKfd9v3Q==", - "license": "Apache-2.0", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.8.tgz", + "integrity": "sha512-E0rU0DglpeJn5ge64mk8wTGEXcQwmpUTY5Zr7IzTpDLmHKiIamINERNZYrPQjg58Ck236sEKSwRSHA4CwshU6Q==", "dependencies": { - "@smithy/property-provider": "^3.1.4", - "@smithy/shared-ini-file-loader": "^3.1.5", - "@smithy/types": "^3.4.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": { @@ -13384,12 +16879,11 @@ } }, "node_modules/@smithy/credential-provider-imds/node_modules/@smithy/shared-ini-file-loader": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.5.tgz", - "integrity": "sha512-6jxsJ4NOmY5Du4FD0enYegNJl4zTSuKLiChIMqIkh+LapxiP7lmz5lYUNLE9/4cvA65mbBmtdzZ8yxmcqM5igg==", - "license": "Apache-2.0", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.8.tgz", + "integrity": "sha512-0NHdQiSkeGl0ICQKcJQ2lCOKH23Nb0EaAa7RDRId6ZqwXkw4LJyIyZ0t3iusD4bnKYDPLGy2/5e2rfUhrt0Acw==", "dependencies": { - "@smithy/types": "^3.4.0", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { @@ -13464,14 +16958,13 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.5.tgz", - "integrity": "sha512-DjRtGmK8pKQMIo9+JlAKUt14Z448bg8nAN04yKIvlrrpmpRSG57s5d2Y83npks1r4gPtTRNbAFdQCoj9l3P2KQ==", - "license": "Apache-2.0", + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.9.tgz", + "integrity": "sha512-hYNVQOqhFQ6vOpenifFME546f0GfJn2OiQ3M0FDmuUu8V/Uiwy2wej7ZXxFBNqdx0R5DZAqWM1l6VRhGz8oE6A==", "dependencies": { - "@smithy/protocol-http": "^4.1.1", - "@smithy/querystring-builder": "^3.0.4", - "@smithy/types": "^3.4.0", + "@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" } @@ -13489,12 +16982,11 @@ } }, "node_modules/@smithy/hash-node": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.4.tgz", - "integrity": "sha512-6FgTVqEfCr9z/7+Em8BwSkJKA2y3krf1em134x3yr2NHWVCo2KYI8tcA53cjeO47y41jwF84ntsEE0Pe6pNKlg==", - "license": "Apache-2.0", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.7.tgz", + "integrity": "sha512-SAGHN+QkrwcHFjfWzs/czX94ZEjPJ0CrWJS3M43WswDXVEuP4AVy9gJ3+AF6JQHZD13bojmuf/Ap/ItDeZ+Qfw==", "dependencies": { - "@smithy/types": "^3.4.0", + "@smithy/types": "^3.5.0", "@smithy/util-buffer-from": "^3.0.0", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" @@ -13518,12 +17010,11 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.4.tgz", - "integrity": "sha512-MJBUrojC4SEXi9aJcnNOE3oNAuYNphgCGFXscaCj2TA/59BTcXhzHACP8jnnEU3n4yir/NSLKzxqez0T4x4tjA==", - "license": "Apache-2.0", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.7.tgz", + "integrity": "sha512-Bq00GsAhHeYSuZX8Kpu4sbI9agH2BNYnqUmmbTGWOhki9NVsWn2jFr896vvoTMH8KAjNX/ErC/8t5QHuEXG+IA==", "dependencies": { - "@smithy/types": "^3.4.0", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" } }, @@ -13551,13 +17042,12 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.6.tgz", - "integrity": "sha512-AFyHCfe8rumkJkz+hCOVJmBagNBj05KypyDwDElA4TgMSA4eYDZRjVePFZuyABrJZFDc7uVj3dpFIDCEhf59SA==", - "license": "Apache-2.0", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.9.tgz", + "integrity": "sha512-t97PidoGElF9hTtLCrof32wfWMqC5g2SEJNxaVH3NjlatuNGsdxXRYO/t+RPnxA15RpYiS0f+zG7FuE2DeGgjA==", "dependencies": { - "@smithy/protocol-http": "^4.1.1", - "@smithy/types": "^3.4.0", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { @@ -13565,17 +17055,16 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.1.1.tgz", - "integrity": "sha512-Irv+soW8NKluAtFSEsF8O3iGyLxa5oOevJb/e1yNacV9H7JP/yHyJuKST5YY2ORS1+W34VR8EuUrOF+K29Pl4g==", - "license": "Apache-2.0", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.1.4.tgz", + "integrity": "sha512-/ChcVHekAyzUbyPRI8CzPPLj6y8QRAfJngWcLMgsWxKVzw/RzBV69mSOzJYDD3pRwushA1+5tHtPF8fjmzBnrQ==", "dependencies": { - "@smithy/middleware-serde": "^3.0.4", - "@smithy/node-config-provider": "^3.1.5", - "@smithy/shared-ini-file-loader": "^3.1.5", - "@smithy/types": "^3.4.0", - "@smithy/url-parser": "^3.0.4", - "@smithy/util-middleware": "^3.0.4", + "@smithy/middleware-serde": "^3.0.7", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/shared-ini-file-loader": "^3.1.8", + "@smithy/types": "^3.5.0", + "@smithy/url-parser": "^3.0.7", + "@smithy/util-middleware": "^3.0.7", "tslib": "^2.6.2" }, "engines": { @@ -13583,14 +17072,13 @@ } }, "node_modules/@smithy/middleware-endpoint/node_modules/@smithy/node-config-provider": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.5.tgz", - "integrity": "sha512-dq/oR3/LxgCgizVk7in7FGTm0w9a3qM4mg3IIXLTCHeW3fV+ipssSvBZ2bvEx1+asfQJTyCnVLeYf7JKfd9v3Q==", - "license": "Apache-2.0", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.8.tgz", + "integrity": "sha512-E0rU0DglpeJn5ge64mk8wTGEXcQwmpUTY5Zr7IzTpDLmHKiIamINERNZYrPQjg58Ck236sEKSwRSHA4CwshU6Q==", "dependencies": { - "@smithy/property-provider": "^3.1.4", - "@smithy/shared-ini-file-loader": "^3.1.5", - "@smithy/types": "^3.4.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": { @@ -13598,12 +17086,11 @@ } }, "node_modules/@smithy/middleware-endpoint/node_modules/@smithy/shared-ini-file-loader": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.5.tgz", - "integrity": "sha512-6jxsJ4NOmY5Du4FD0enYegNJl4zTSuKLiChIMqIkh+LapxiP7lmz5lYUNLE9/4cvA65mbBmtdzZ8yxmcqM5igg==", - "license": "Apache-2.0", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.8.tgz", + "integrity": "sha512-0NHdQiSkeGl0ICQKcJQ2lCOKH23Nb0EaAa7RDRId6ZqwXkw4LJyIyZ0t3iusD4bnKYDPLGy2/5e2rfUhrt0Acw==", "dependencies": { - "@smithy/types": "^3.4.0", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { @@ -13611,18 +17098,17 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.16.tgz", - "integrity": "sha512-08kI36p1yB4CWO3Qi+UQxjzobt8iQJpnruF0K5BkbZmA/N/sJ51A1JJGJ36GgcbFyPfWw2FU48S5ZoqXt0h0jw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^3.1.5", - "@smithy/protocol-http": "^4.1.1", - "@smithy/service-error-classification": "^3.0.4", - "@smithy/smithy-client": "^3.3.0", - "@smithy/types": "^3.4.0", - "@smithy/util-middleware": "^3.0.4", - "@smithy/util-retry": "^3.0.4", + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.22.tgz", + "integrity": "sha512-svEN7O2Tf7BoaBkPzX/8AE2Bv7p16d9/ulFAD1Gmn5g19iMqNk1WIkMxAY7SpB9/tVtUwKx0NaIsBRl88gumZA==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.8", + "@smithy/protocol-http": "^4.1.4", + "@smithy/service-error-classification": "^3.0.7", + "@smithy/smithy-client": "^3.3.6", + "@smithy/types": "^3.5.0", + "@smithy/util-middleware": "^3.0.7", + "@smithy/util-retry": "^3.0.7", "tslib": "^2.6.2", "uuid": "^9.0.1" }, @@ -13631,14 +17117,13 @@ } }, "node_modules/@smithy/middleware-retry/node_modules/@smithy/node-config-provider": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.5.tgz", - "integrity": "sha512-dq/oR3/LxgCgizVk7in7FGTm0w9a3qM4mg3IIXLTCHeW3fV+ipssSvBZ2bvEx1+asfQJTyCnVLeYf7JKfd9v3Q==", - "license": "Apache-2.0", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.8.tgz", + "integrity": "sha512-E0rU0DglpeJn5ge64mk8wTGEXcQwmpUTY5Zr7IzTpDLmHKiIamINERNZYrPQjg58Ck236sEKSwRSHA4CwshU6Q==", "dependencies": { - "@smithy/property-provider": "^3.1.4", - "@smithy/shared-ini-file-loader": "^3.1.5", - "@smithy/types": "^3.4.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": { @@ -13646,12 +17131,11 @@ } }, "node_modules/@smithy/middleware-retry/node_modules/@smithy/shared-ini-file-loader": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.5.tgz", - "integrity": "sha512-6jxsJ4NOmY5Du4FD0enYegNJl4zTSuKLiChIMqIkh+LapxiP7lmz5lYUNLE9/4cvA65mbBmtdzZ8yxmcqM5igg==", - "license": "Apache-2.0", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.8.tgz", + "integrity": "sha512-0NHdQiSkeGl0ICQKcJQ2lCOKH23Nb0EaAa7RDRId6ZqwXkw4LJyIyZ0t3iusD4bnKYDPLGy2/5e2rfUhrt0Acw==", "dependencies": { - "@smithy/types": "^3.4.0", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { @@ -13672,12 +17156,11 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.4.tgz", - "integrity": "sha512-1lPDB2O6IJ50Ucxgn7XrvZXbbuI48HmPCcMTuSoXT1lDzuTUfIuBjgAjpD8YLVMfnrjdepi/q45556LA51Pubw==", - "license": "Apache-2.0", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.7.tgz", + "integrity": "sha512-VytaagsQqtH2OugzVTq4qvjkLNbWehHfGcGr0JLJmlDRrNCeZoWkWsSOw1nhS/4hyUUWF/TLGGml4X/OnEep5g==", "dependencies": { - "@smithy/types": "^3.4.0", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { @@ -13685,12 +17168,11 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.4.tgz", - "integrity": "sha512-sLMRjtMCqtVcrOqaOZ10SUnlFE25BSlmLsi4bRSGFD7dgR54eqBjfqkVkPBQyrKBortfGM0+2DJoUPcGECR+nQ==", - "license": "Apache-2.0", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.7.tgz", + "integrity": "sha512-EyTbMCdqS1DoeQsO4gI7z2Gzq1MoRFAeS8GkFYIwbedB7Lp5zlLHJdg+56tllIIG5Hnf9ZWX48YKSHlsKvugGA==", "dependencies": { - "@smithy/types": "^3.4.0", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { @@ -13738,15 +17220,14 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.2.0.tgz", - "integrity": "sha512-5TFqaABbiY7uJMKbqR4OARjwI/l4TRoysDJ75pLpVQyO3EcmeloKYwDGyCtgB9WJniFx3BMkmGCB9+j+QiB+Ww==", - "license": "Apache-2.0", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.2.4.tgz", + "integrity": "sha512-49reY3+JgLMFNm7uTAKBWiKCA6XSvkNp9FqhVmusm2jpVnHORYFeFZ704LShtqWfjZW/nhX+7Iexyb6zQfXYIQ==", "dependencies": { - "@smithy/abort-controller": "^3.1.2", - "@smithy/protocol-http": "^4.1.1", - "@smithy/querystring-builder": "^3.0.4", - "@smithy/types": "^3.4.0", + "@smithy/abort-controller": "^3.1.5", + "@smithy/protocol-http": "^4.1.4", + "@smithy/querystring-builder": "^3.0.7", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { @@ -13754,12 +17235,11 @@ } }, "node_modules/@smithy/property-provider": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.4.tgz", - "integrity": "sha512-BmhefQbfkSl9DeU0/e6k9N4sT5bya5etv2epvqLUz3eGyfRBhtQq60nDkc1WPp4c+KWrzK721cUc/3y0f2psPQ==", - "license": "Apache-2.0", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.7.tgz", + "integrity": "sha512-QfzLi1GPMisY7bAM5hOUqBdGYnY5S2JAlr201pghksrQv139f8iiiMalXtjczIP5f6owxFn3MINLNUNvUkgtPw==", "dependencies": { - "@smithy/types": "^3.4.0", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { @@ -13767,12 +17247,11 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.1.tgz", - "integrity": "sha512-Fm5+8LkeIus83Y8jTL1XHsBGP8sPvE1rEVyKf/87kbOPTbzEDMcgOlzcmYXat2h+nC3wwPtRy8hFqtJS71+Wow==", - "license": "Apache-2.0", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.4.tgz", + "integrity": "sha512-MlWK8eqj0JlpZBnWmjQLqmFp71Ug00P+m72/1xQB3YByXD4zZ+y9N4hYrR0EDmrUCZIkyATWHOXFgtavwGDTzQ==", "dependencies": { - "@smithy/types": "^3.4.0", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { @@ -13780,12 +17259,11 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.4.tgz", - "integrity": "sha512-NEoPAsZPdpfVbF98qm8i5k1XMaRKeEnO47CaL5ja6Y1Z2DgJdwIJuJkTJypKm/IKfp8gc0uimIFLwhml8+/pAw==", - "license": "Apache-2.0", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.7.tgz", + "integrity": "sha512-65RXGZZ20rzqqxTsChdqSpbhA6tdt5IFNgG6o7e1lnPVLCe6TNWQq4rTl4N87hTDD8mV4IxJJnvyE7brbnRkQw==", "dependencies": { - "@smithy/types": "^3.4.0", + "@smithy/types": "^3.5.0", "@smithy/util-uri-escape": "^3.0.0", "tslib": "^2.6.2" }, @@ -13794,12 +17272,11 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.4.tgz", - "integrity": "sha512-7CHPXffFcakFzhO0OZs/rn6fXlTHrSDdLhIT6/JIk1u2bvwguTL3fMCc1+CfcbXA7TOhjWXu3TcB1EGMqJQwHg==", - "license": "Apache-2.0", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.7.tgz", + "integrity": "sha512-Fouw4KJVWqqUVIu1gZW8BH2HakwLz6dvdrAhXeXfeymOBrZw+hcqaWs+cS1AZPVp4nlbeIujYrKA921ZW2WMPA==", "dependencies": { - "@smithy/types": "^3.4.0", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { @@ -13807,12 +17284,11 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.4.tgz", - "integrity": "sha512-KciDHHKFVTb9A1KlJHBt2F26PBaDtoE23uTZy5qRvPzHPqrooXFi6fmx98lJb3Jl38PuUTqIuCUmmY3pacuMBQ==", - "license": "Apache-2.0", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.7.tgz", + "integrity": "sha512-91PRkTfiBf9hxkIchhRKJfl1rsplRDyBnmyFca3y0Z3x/q0JJN480S83LBd8R6sBCkm2bBbqw2FHp0Mbh+ecSA==", "dependencies": { - "@smithy/types": "^3.4.0" + "@smithy/types": "^3.5.0" }, "engines": { "node": ">=16.0.0" @@ -13844,16 +17320,15 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-4.1.1.tgz", - "integrity": "sha512-SH9J9be81TMBNGCmjhrgMWu4YSpQ3uP1L06u/K9SDrE2YibUix1qxedPCxEQu02At0P0SrYDjvz+y91vLG0KRQ==", - "license": "Apache-2.0", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-4.2.0.tgz", + "integrity": "sha512-LafbclHNKnsorMgUkKm7Tk7oJ7xizsZ1VwqhGKqoCIrXh4fqDDp73fK99HOEEgcsQbtemmeY/BPv0vTVYYUNEQ==", "dependencies": { "@smithy/is-array-buffer": "^3.0.0", - "@smithy/protocol-http": "^4.1.1", - "@smithy/types": "^3.4.0", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", "@smithy/util-hex-encoding": "^3.0.0", - "@smithy/util-middleware": "^3.0.4", + "@smithy/util-middleware": "^3.0.7", "@smithy/util-uri-escape": "^3.0.0", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" @@ -13863,16 +17338,15 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.3.0.tgz", - "integrity": "sha512-H32nVo8tIX82kB0xI2LBrIcj8jx/3/ITotNLbeG1UL0b3b440YPR/hUvqjFJiaB24pQrMjRbU8CugqH5sV0hkw==", - "license": "Apache-2.0", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.3.6.tgz", + "integrity": "sha512-qdH+mvDHgq1ss6mocyIl2/VjlWXew7pGwZQydwYJczEc22HZyX3k8yVPV9aZsbYbssHPvMDRA5rfBDrjQUbIIw==", "dependencies": { - "@smithy/middleware-endpoint": "^3.1.1", - "@smithy/middleware-stack": "^3.0.4", - "@smithy/protocol-http": "^4.1.1", - "@smithy/types": "^3.4.0", - "@smithy/util-stream": "^3.1.4", + "@smithy/middleware-endpoint": "^3.1.4", + "@smithy/middleware-stack": "^3.0.7", + "@smithy/protocol-http": "^4.1.4", + "@smithy/types": "^3.5.0", + "@smithy/util-stream": "^3.1.9", "tslib": "^2.6.2" }, "engines": { @@ -13880,10 +17354,9 @@ } }, "node_modules/@smithy/types": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.4.0.tgz", - "integrity": "sha512-0shOWSg/pnFXPcsSU8ZbaJ4JBHZJPPzLCJxafJvbMVFo9l1w81CqpgUqjlKGNHVrVB7fhIs+WS82JDTyzaLyLA==", - "license": "Apache-2.0", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.5.0.tgz", + "integrity": "sha512-QN0twHNfe8mNJdH9unwsCK13GURU7oEAZqkBI+rsvpv1jrmserO+WnLE7jidR9W/1dxwZ0u/CB01mV2Gms/K2Q==", "dependencies": { "tslib": "^2.6.2" }, @@ -13892,13 +17365,12 @@ } }, "node_modules/@smithy/url-parser": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.4.tgz", - "integrity": "sha512-XdXfObA8WrloavJYtDuzoDhJAYc5rOt+FirFmKBRKaihu7QtU/METAxJgSo7uMK6hUkx0vFnqxV75urtRaLkLg==", - "license": "Apache-2.0", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.7.tgz", + "integrity": "sha512-70UbSSR8J97c1rHZOWhl+VKiZDqHWxs/iW8ZHrHp5fCCPLSBE7GcUlUvKSle3Ca+J9LLbYCj/A79BxztBvAfpA==", "dependencies": { - "@smithy/querystring-parser": "^3.0.4", - "@smithy/types": "^3.4.0", + "@smithy/querystring-parser": "^3.0.7", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" } }, @@ -13963,14 +17435,13 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.16.tgz", - "integrity": "sha512-Os8ddfNBe7hmc5UMWZxygIHCyAqY0aWR8Wnp/aKbti3f8Df/r0J9ttMZIxeMjsFgtVjEryB0q7SGcwBsHk8WEw==", - "license": "Apache-2.0", + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.22.tgz", + "integrity": "sha512-WKzUxNsOun5ETwEOrvooXeI1mZ8tjDTOcN4oruELWHhEYDgQYWwxZupURVyovcv+h5DyQT/DzK5nm4ZoR/Tw5Q==", "dependencies": { - "@smithy/property-provider": "^3.1.4", - "@smithy/smithy-client": "^3.3.0", - "@smithy/types": "^3.4.0", + "@smithy/property-provider": "^3.1.7", + "@smithy/smithy-client": "^3.3.6", + "@smithy/types": "^3.5.0", "bowser": "^2.11.0", "tslib": "^2.6.2" }, @@ -13979,17 +17450,16 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.16.tgz", - "integrity": "sha512-rNhFIYRtrOrrhRlj6RL8jWA6/dcwrbGYAmy8+OAHjjzQ6zdzUBB1P+3IuJAgwWN6Y5GxI+mVXlM/pOjaoIgHow==", - "license": "Apache-2.0", + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.22.tgz", + "integrity": "sha512-hUsciOmAq8fsGwqg4+pJfNRmrhfqMH4Y9UeGcgeUl88kPAoYANFATJqCND+O4nUvwp5TzsYwGpqpcBKyA8LUUg==", "dependencies": { - "@smithy/config-resolver": "^3.0.6", - "@smithy/credential-provider-imds": "^3.2.1", - "@smithy/node-config-provider": "^3.1.5", - "@smithy/property-provider": "^3.1.4", - "@smithy/smithy-client": "^3.3.0", - "@smithy/types": "^3.4.0", + "@smithy/config-resolver": "^3.0.9", + "@smithy/credential-provider-imds": "^3.2.4", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/property-provider": "^3.1.7", + "@smithy/smithy-client": "^3.3.6", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { @@ -13997,14 +17467,13 @@ } }, "node_modules/@smithy/util-defaults-mode-node/node_modules/@smithy/node-config-provider": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.5.tgz", - "integrity": "sha512-dq/oR3/LxgCgizVk7in7FGTm0w9a3qM4mg3IIXLTCHeW3fV+ipssSvBZ2bvEx1+asfQJTyCnVLeYf7JKfd9v3Q==", - "license": "Apache-2.0", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.8.tgz", + "integrity": "sha512-E0rU0DglpeJn5ge64mk8wTGEXcQwmpUTY5Zr7IzTpDLmHKiIamINERNZYrPQjg58Ck236sEKSwRSHA4CwshU6Q==", "dependencies": { - "@smithy/property-provider": "^3.1.4", - "@smithy/shared-ini-file-loader": "^3.1.5", - "@smithy/types": "^3.4.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": { @@ -14012,12 +17481,11 @@ } }, "node_modules/@smithy/util-defaults-mode-node/node_modules/@smithy/shared-ini-file-loader": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.5.tgz", - "integrity": "sha512-6jxsJ4NOmY5Du4FD0enYegNJl4zTSuKLiChIMqIkh+LapxiP7lmz5lYUNLE9/4cvA65mbBmtdzZ8yxmcqM5igg==", - "license": "Apache-2.0", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.8.tgz", + "integrity": "sha512-0NHdQiSkeGl0ICQKcJQ2lCOKH23Nb0EaAa7RDRId6ZqwXkw4LJyIyZ0t3iusD4bnKYDPLGy2/5e2rfUhrt0Acw==", "dependencies": { - "@smithy/types": "^3.4.0", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { @@ -14025,13 +17493,12 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.1.0.tgz", - "integrity": "sha512-ilS7/0jcbS2ELdg0fM/4GVvOiuk8/U3bIFXUW25xE1Vh1Ol4DP6vVHQKqM40rCMizCLmJ9UxK+NeJrKlhI3HVA==", - "license": "Apache-2.0", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.1.3.tgz", + "integrity": "sha512-34eACeKov6jZdHqS5hxBMJ4KyWKztTMulhuQ2UdOoP6vVxMLrOKUqIXAwJe/wiWMhXhydLW664B02CNpQBQ4Aw==", "dependencies": { - "@smithy/node-config-provider": "^3.1.5", - "@smithy/types": "^3.4.0", + "@smithy/node-config-provider": "^3.1.8", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { @@ -14039,14 +17506,13 @@ } }, "node_modules/@smithy/util-endpoints/node_modules/@smithy/node-config-provider": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.5.tgz", - "integrity": "sha512-dq/oR3/LxgCgizVk7in7FGTm0w9a3qM4mg3IIXLTCHeW3fV+ipssSvBZ2bvEx1+asfQJTyCnVLeYf7JKfd9v3Q==", - "license": "Apache-2.0", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.8.tgz", + "integrity": "sha512-E0rU0DglpeJn5ge64mk8wTGEXcQwmpUTY5Zr7IzTpDLmHKiIamINERNZYrPQjg58Ck236sEKSwRSHA4CwshU6Q==", "dependencies": { - "@smithy/property-provider": "^3.1.4", - "@smithy/shared-ini-file-loader": "^3.1.5", - "@smithy/types": "^3.4.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": { @@ -14054,12 +17520,11 @@ } }, "node_modules/@smithy/util-endpoints/node_modules/@smithy/shared-ini-file-loader": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.5.tgz", - "integrity": "sha512-6jxsJ4NOmY5Du4FD0enYegNJl4zTSuKLiChIMqIkh+LapxiP7lmz5lYUNLE9/4cvA65mbBmtdzZ8yxmcqM5igg==", - "license": "Apache-2.0", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.8.tgz", + "integrity": "sha512-0NHdQiSkeGl0ICQKcJQ2lCOKH23Nb0EaAa7RDRId6ZqwXkw4LJyIyZ0t3iusD4bnKYDPLGy2/5e2rfUhrt0Acw==", "dependencies": { - "@smithy/types": "^3.4.0", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { @@ -14079,12 +17544,11 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.4.tgz", - "integrity": "sha512-uSXHTBhstb1c4nHdmQEdkNMv9LiRNaJ/lWV2U/GO+5F236YFpdPw+hyWI9Zc0Rp9XKzwD9kVZvhZmEgp0UCVnA==", - "license": "Apache-2.0", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.7.tgz", + "integrity": "sha512-OVA6fv/3o7TMJTpTgOi1H5OTwnuUa8hzRzhSFDtZyNxi6OZ70L/FHattSmhE212I7b6WSOJAAmbYnvcjTHOJCA==", "dependencies": { - "@smithy/types": "^3.4.0", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { @@ -14092,13 +17556,12 @@ } }, "node_modules/@smithy/util-retry": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.4.tgz", - "integrity": "sha512-JJr6g0tO1qO2tCQyK+n3J18r34ZpvatlFN5ULcLranFIBZPxqoivb77EPyNTVwTGMEvvq2qMnyjm4jMIxjdLFg==", - "license": "Apache-2.0", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.7.tgz", + "integrity": "sha512-nh1ZO1vTeo2YX1plFPSe/OXaHkLAHza5jpokNiiKX2M5YpNUv6RxGJZhpfmiR4jSvVHCjIDmILjrxKmP+/Ghug==", "dependencies": { - "@smithy/service-error-classification": "^3.0.4", - "@smithy/types": "^3.4.0", + "@smithy/service-error-classification": "^3.0.7", + "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { @@ -14106,14 +17569,13 @@ } }, "node_modules/@smithy/util-stream": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.1.4.tgz", - "integrity": "sha512-txU3EIDLhrBZdGfon6E9V6sZz/irYnKFMblz4TLVjyq8hObNHNS2n9a2t7GIrl7d85zgEPhwLE0gANpZsvpsKg==", - "license": "Apache-2.0", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.1.9.tgz", + "integrity": "sha512-7YAR0Ub3MwTMjDfjnup4qa6W8gygZMxikBhFMPESi6ASsl/rZJhwLpF/0k9TuezScCojsM0FryGdz4LZtjKPPQ==", "dependencies": { - "@smithy/fetch-http-handler": "^3.2.5", - "@smithy/node-http-handler": "^3.2.0", - "@smithy/types": "^3.4.0", + "@smithy/fetch-http-handler": "^3.2.9", + "@smithy/node-http-handler": "^3.2.4", + "@smithy/types": "^3.5.0", "@smithy/util-base64": "^3.0.0", "@smithy/util-buffer-from": "^3.0.0", "@smithy/util-hex-encoding": "^3.0.0", @@ -14771,21 +18233,19 @@ "license": "ISC" }, "node_modules/@verdaccio/auth": { - "version": "8.0.0-next-8.1", - "resolved": "https://registry.npmjs.org/@verdaccio/auth/-/auth-8.0.0-next-8.1.tgz", - "integrity": "sha512-sPmHdnYuRSMgABCsTJEfz8tb/smONsWVg0g4KK2QycyYZ/A+RwZLV1JLiQb4wzu9zvS0HSloqWqkWlyNHW3mtw==", + "version": "8.0.0-next-8.4", + "resolved": "https://registry.npmjs.org/@verdaccio/auth/-/auth-8.0.0-next-8.4.tgz", + "integrity": "sha512-Bv+du+eIMK2/KU2wIjha2FHQrhBT5RuDOVi1nyRyjEyjmJrUt2RWU0Cb7ASxzQy61nkcJ3bs7kuu9dPHK/Z+jw==", "dev": true, - "license": "MIT", "dependencies": { - "@verdaccio/config": "8.0.0-next-8.1", - "@verdaccio/core": "8.0.0-next-8.1", - "@verdaccio/loaders": "8.0.0-next-8.1", - "@verdaccio/logger": "8.0.0-next-8.1", - "@verdaccio/signature": "8.0.0-next-8.0", - "@verdaccio/utils": "7.0.1-next-8.1", + "@verdaccio/config": "8.0.0-next-8.4", + "@verdaccio/core": "8.0.0-next-8.4", + "@verdaccio/loaders": "8.0.0-next-8.3", + "@verdaccio/signature": "8.0.0-next-8.1", + "@verdaccio/utils": "8.1.0-next-8.4", "debug": "4.3.7", "lodash": "4.17.21", - "verdaccio-htpasswd": "13.0.0-next-8.1" + "verdaccio-htpasswd": "13.0.0-next-8.4" }, "engines": { "node": ">=18" @@ -14795,6 +18255,49 @@ "url": "https://opencollective.com/verdaccio" } }, + "node_modules/@verdaccio/auth/node_modules/@verdaccio/utils": { + "version": "8.1.0-next-8.4", + "resolved": "https://registry.npmjs.org/@verdaccio/utils/-/utils-8.1.0-next-8.4.tgz", + "integrity": "sha512-mAEBWV5zsjtC4e/hfj1Q/eYtMlML5wxedk7mqqmvAydjw+ycSH/D/ksU+B10h4STX2NcBlcLtgLl7OI/wFzrgA==", + "dev": true, + "dependencies": { + "@verdaccio/core": "8.0.0-next-8.4", + "lodash": "4.17.21", + "minimatch": "7.4.6", + "semver": "7.6.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/@verdaccio/auth/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@verdaccio/auth/node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@verdaccio/commons-api": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/@verdaccio/commons-api/-/commons-api-10.2.0.tgz", @@ -14821,21 +18324,39 @@ "license": "MIT" }, "node_modules/@verdaccio/config": { - "version": "8.0.0-next-8.1", - "resolved": "https://registry.npmjs.org/@verdaccio/config/-/config-8.0.0-next-8.1.tgz", - "integrity": "sha512-goDVOH4e8xMUxjHybJpi5HwIecVFqzJ9jeNFrRUgtUUn0PtFuNMHgxOeqDKRVboZhc5HK90yed8URK/1O6VsUw==", + "version": "8.0.0-next-8.4", + "resolved": "https://registry.npmjs.org/@verdaccio/config/-/config-8.0.0-next-8.4.tgz", + "integrity": "sha512-9CTYYhaO4xrGbiYjLqRy8EMFPm0YL4a7P6ae8Zm4Dx85Dd6i8XqZ7tlU/+a6qf1g/qggYloolU8pcjaLWNDKAQ==", "dev": true, - "license": "MIT", "dependencies": { - "@verdaccio/core": "8.0.0-next-8.1", - "@verdaccio/utils": "7.0.1-next-8.1", + "@verdaccio/core": "8.0.0-next-8.4", + "@verdaccio/utils": "8.1.0-next-8.4", "debug": "4.3.7", "js-yaml": "4.1.0", "lodash": "4.17.21", "minimatch": "7.4.6" }, "engines": { - "node": ">=14" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/@verdaccio/config/node_modules/@verdaccio/utils": { + "version": "8.1.0-next-8.4", + "resolved": "https://registry.npmjs.org/@verdaccio/utils/-/utils-8.1.0-next-8.4.tgz", + "integrity": "sha512-mAEBWV5zsjtC4e/hfj1Q/eYtMlML5wxedk7mqqmvAydjw+ycSH/D/ksU+B10h4STX2NcBlcLtgLl7OI/wFzrgA==", + "dev": true, + "dependencies": { + "@verdaccio/core": "8.0.0-next-8.4", + "lodash": "4.17.21", + "minimatch": "7.4.6", + "semver": "7.6.3" + }, + "engines": { + "node": ">=12" }, "funding": { "type": "opencollective", @@ -14846,15 +18367,13 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" + "dev": true }, "node_modules/@verdaccio/config/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -14864,7 +18383,6 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -14877,7 +18395,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -14889,11 +18406,10 @@ } }, "node_modules/@verdaccio/core": { - "version": "8.0.0-next-8.1", - "resolved": "https://registry.npmjs.org/@verdaccio/core/-/core-8.0.0-next-8.1.tgz", - "integrity": "sha512-kQRCB2wgXEh8H88G51eQgAFK9IxmnBtkQ8sY5FbmB6PbBkyHrbGcCp+2mtRqqo36j0W1VAlfM3XzoknMy6qQnw==", + "version": "8.0.0-next-8.4", + "resolved": "https://registry.npmjs.org/@verdaccio/core/-/core-8.0.0-next-8.4.tgz", + "integrity": "sha512-TCaHwIpr97f4YQkU25E6pk1dbfWTQwYPos1tMb9Q7k6IapoxN0c1s+SyF5FBuMOIfJpTHoWJDo/z7QCouQC3lw==", "dev": true, - "license": "MIT", "dependencies": { "ajv": "8.17.1", "core-js": "3.37.1", @@ -14903,7 +18419,7 @@ "semver": "7.6.3" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { "type": "opencollective", @@ -14915,7 +18431,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -14933,7 +18448,6 @@ "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==", "dev": true, "hasInstallScript": true, - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -14943,8 +18457,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@verdaccio/file-locking": { "version": "10.3.1", @@ -14964,13 +18477,11 @@ } }, "node_modules/@verdaccio/loaders": { - "version": "8.0.0-next-8.1", - "resolved": "https://registry.npmjs.org/@verdaccio/loaders/-/loaders-8.0.0-next-8.1.tgz", - "integrity": "sha512-mqGCUBs862g8mICZwX8CG92p1EZ1Un0DJ2DB7+iVu2TYaEeKoHoIdafabVdiYrbOjLcAOOBrMKE1Wnn14eLxpA==", + "version": "8.0.0-next-8.3", + "resolved": "https://registry.npmjs.org/@verdaccio/loaders/-/loaders-8.0.0-next-8.3.tgz", + "integrity": "sha512-7bIOdi+U1xSLRu0s1XxQwrV3zzzFaVaTX7JKFgj2tQvMy9AgzlpjbW1CqaH8OTVEqq03Pwvwj5hQlcvyzCwm1A==", "dev": true, - "license": "MIT", "dependencies": { - "@verdaccio/logger": "8.0.0-next-8.1", "debug": "4.3.7", "lodash": "4.17.21" }, @@ -15039,14 +18550,13 @@ "license": "MIT" }, "node_modules/@verdaccio/logger": { - "version": "8.0.0-next-8.1", - "resolved": "https://registry.npmjs.org/@verdaccio/logger/-/logger-8.0.0-next-8.1.tgz", - "integrity": "sha512-w5kR0/umQkfH2F4PK5Fz9T6z3xz+twewawKLPTUfAgrVAOiWxcikGhhcHWhSGiJ0lPqIa+T0VYuLWMeVeDirGw==", + "version": "8.0.0-next-8.4", + "resolved": "https://registry.npmjs.org/@verdaccio/logger/-/logger-8.0.0-next-8.4.tgz", + "integrity": "sha512-zJIFpKYNR/api/mxj5HqJSlEMFh9J4sVKk+3QYlPmppW68beZLLzqwchb5+c/V559lnSrGy5HvDUEGLXvp6reA==", "dev": true, - "license": "MIT", "dependencies": { - "@verdaccio/logger-commons": "8.0.0-next-8.1", - "pino": "8.17.2" + "@verdaccio/logger-commons": "8.0.0-next-8.4", + "pino": "9.4.0" }, "engines": { "node": ">=18" @@ -15056,188 +18566,80 @@ "url": "https://opencollective.com/verdaccio" } }, - "node_modules/@verdaccio/logger-7": { - "version": "8.0.0-next-8.1", - "resolved": "https://registry.npmjs.org/@verdaccio/logger-7/-/logger-7-8.0.0-next-8.1.tgz", - "integrity": "sha512-V+/B1Wnct3IZ90q6HkI1a3dqbS0ds7s/5WPrS5cmBeLEw78/OGgF76XkhI2+lett7Un1CjVow7mcebOWcZ/Sqw==", + "node_modules/@verdaccio/logger-commons": { + "version": "8.0.0-next-8.4", + "resolved": "https://registry.npmjs.org/@verdaccio/logger-commons/-/logger-commons-8.0.0-next-8.4.tgz", + "integrity": "sha512-neDbq5IIRoidFT4Rv3zH9YydICDCJEybb06BzCGVOzlhZ7F+fBzJH1qlBhAEISfbONugDgfuUQ2jbRCKEkHezQ==", "dev": true, - "license": "MIT", "dependencies": { - "@verdaccio/logger-commons": "8.0.0-next-8.1", - "pino": "7.11.0" + "@verdaccio/core": "8.0.0-next-8.4", + "@verdaccio/logger-prettify": "8.0.0-next-8.1", + "colorette": "2.0.20", + "debug": "4.3.7" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/verdaccio" } }, - "node_modules/@verdaccio/logger-7/node_modules/duplexify": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", - "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.2" - } - }, - "node_modules/@verdaccio/logger-7/node_modules/on-exit-leak-free": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz", - "integrity": "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@verdaccio/logger-7/node_modules/pino": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-7.11.0.tgz", - "integrity": "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0", - "fast-redact": "^3.0.0", - "on-exit-leak-free": "^0.2.0", - "pino-abstract-transport": "v0.5.0", - "pino-std-serializers": "^4.0.0", - "process-warning": "^1.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.1.0", - "safe-stable-stringify": "^2.1.0", - "sonic-boom": "^2.2.1", - "thread-stream": "^0.15.1" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/@verdaccio/logger-7/node_modules/pino-abstract-transport": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz", - "integrity": "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "duplexify": "^4.1.2", - "split2": "^4.0.0" - } - }, - "node_modules/@verdaccio/logger-7/node_modules/pino-std-serializers": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", - "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@verdaccio/logger-7/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@verdaccio/logger-7/node_modules/real-require": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.1.0.tgz", - "integrity": "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/@verdaccio/logger-7/node_modules/sonic-boom": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz", - "integrity": "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, - "node_modules/@verdaccio/logger-7/node_modules/thread-stream": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-0.15.2.tgz", - "integrity": "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "real-require": "^0.1.0" - } - }, - "node_modules/@verdaccio/logger-commons": { + "node_modules/@verdaccio/logger-prettify": { "version": "8.0.0-next-8.1", - "resolved": "https://registry.npmjs.org/@verdaccio/logger-commons/-/logger-commons-8.0.0-next-8.1.tgz", - "integrity": "sha512-jCge//RT4uaK7MarhpzcJeJ5Uvtu/DbJ1wvJQyGiFe+9AvxDGm3EUFXvawLFZ0lzYhmLt1nvm7kevcc3vOm2ZQ==", + "resolved": "https://registry.npmjs.org/@verdaccio/logger-prettify/-/logger-prettify-8.0.0-next-8.1.tgz", + "integrity": "sha512-vLhaGq0q7wtMCcqa0aQY6QOsMNarhTu/l4e6Z8mG/5LUH95GGLsBwpXLnKS94P3deIjsHhc9ycnEmG39txbQ1w==", "dev": true, - "license": "MIT", "dependencies": { - "@verdaccio/core": "8.0.0-next-8.1", - "@verdaccio/logger-prettify": "8.0.0-next-8.0", "colorette": "2.0.20", - "debug": "4.3.7" + "dayjs": "1.11.13", + "lodash": "4.17.21", + "pino-abstract-transport": "1.2.0", + "sonic-boom": "3.8.1" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/verdaccio" } }, - "node_modules/@verdaccio/logger-prettify": { - "version": "8.0.0-next-8.0", - "resolved": "https://registry.npmjs.org/@verdaccio/logger-prettify/-/logger-prettify-8.0.0-next-8.0.tgz", - "integrity": "sha512-7mAFHZF2NPTubrOXYp2+fbMjRW5MMWXMeS3LcpupMAn5uPp6jkKEM8NC4IVJEevC5Ph4vPVZqpoPDpgXHEaV3Q==", + "node_modules/@verdaccio/middleware": { + "version": "8.0.0-next-8.4", + "resolved": "https://registry.npmjs.org/@verdaccio/middleware/-/middleware-8.0.0-next-8.4.tgz", + "integrity": "sha512-tzpfSpeLKUeyTsQ+fvUsokgdh1NrjDJX/oz2ya8wTYSInKAt1Ld9MRzRVSHJwIQc7wfg46zSjpcKZVLA/YkJ6w==", "dev": true, - "license": "MIT", "dependencies": { - "colorette": "2.0.20", - "dayjs": "1.11.13", + "@verdaccio/config": "8.0.0-next-8.4", + "@verdaccio/core": "8.0.0-next-8.4", + "@verdaccio/url": "13.0.0-next-8.4", + "@verdaccio/utils": "8.1.0-next-8.4", + "debug": "4.3.7", + "express": "4.21.1", + "express-rate-limit": "5.5.1", "lodash": "4.17.21", - "pino-abstract-transport": "1.1.0", - "sonic-boom": "3.8.0" + "lru-cache": "7.18.3", + "mime": "2.6.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/verdaccio" } }, - "node_modules/@verdaccio/middleware": { - "version": "8.0.0-next-8.1", - "resolved": "https://registry.npmjs.org/@verdaccio/middleware/-/middleware-8.0.0-next-8.1.tgz", - "integrity": "sha512-GpAdJYky1WmOERpxPoCkVSwTTJIsVAjqf2a2uQNvi7R3UZhs059JKhWcZjJMVCGV0uz9xgQvtb3DEuYGHqyaOg==", + "node_modules/@verdaccio/middleware/node_modules/@verdaccio/utils": { + "version": "8.1.0-next-8.4", + "resolved": "https://registry.npmjs.org/@verdaccio/utils/-/utils-8.1.0-next-8.4.tgz", + "integrity": "sha512-mAEBWV5zsjtC4e/hfj1Q/eYtMlML5wxedk7mqqmvAydjw+ycSH/D/ksU+B10h4STX2NcBlcLtgLl7OI/wFzrgA==", "dev": true, - "license": "MIT", "dependencies": { - "@verdaccio/config": "8.0.0-next-8.1", - "@verdaccio/core": "8.0.0-next-8.1", - "@verdaccio/url": "13.0.0-next-8.1", - "@verdaccio/utils": "7.0.1-next-8.1", - "debug": "4.3.7", - "express": "4.21.0", - "express-rate-limit": "5.5.1", + "@verdaccio/core": "8.0.0-next-8.4", "lodash": "4.17.21", - "lru-cache": "7.18.3", - "mime": "2.6.0" + "minimatch": "7.4.6", + "semver": "7.6.3" }, "engines": { "node": ">=12" @@ -15247,12 +18649,20 @@ "url": "https://opencollective.com/verdaccio" } }, + "node_modules/@verdaccio/middleware/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/@verdaccio/middleware/node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", "dev": true, - "license": "ISC", "engines": { "node": ">=12" } @@ -15262,22 +18672,35 @@ "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/@verdaccio/middleware/node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=4.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@verdaccio/search-indexer": { - "version": "8.0.0-next-8.0", - "resolved": "https://registry.npmjs.org/@verdaccio/search-indexer/-/search-indexer-8.0.0-next-8.0.tgz", - "integrity": "sha512-VS9axVt8XAueiPceVCgaj9nlvYj5s/T4MkAILSf2rVZeFFOMUyxU3mddUCajSHzL+YpqCuzLLL9865sRRzOJ9w==", + "version": "8.0.0-next-8.2", + "resolved": "https://registry.npmjs.org/@verdaccio/search-indexer/-/search-indexer-8.0.0-next-8.2.tgz", + "integrity": "sha512-sWliVN5BkAGbZ3e/GD0CsZMfPJdRMRuN0tEKQFsvEJifxToq5UkfCw6vKaVvhezsTWqb+Rp5y+2d4n5BDOA49w==", "dev": true, - "license": "MIT", "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "type": "opencollective", @@ -15285,17 +18708,16 @@ } }, "node_modules/@verdaccio/signature": { - "version": "8.0.0-next-8.0", - "resolved": "https://registry.npmjs.org/@verdaccio/signature/-/signature-8.0.0-next-8.0.tgz", - "integrity": "sha512-klcc2UlCvQxXDV65Qewo2rZOfv7S1y8NekS/8uurSaCTjU35T+fz+Pbqz1S9XK9oQlMp4vCQ7w3iMPWQbvphEQ==", + "version": "8.0.0-next-8.1", + "resolved": "https://registry.npmjs.org/@verdaccio/signature/-/signature-8.0.0-next-8.1.tgz", + "integrity": "sha512-lHD/Z2FoPQTtDYz6ZlXhj/lrg0SFirHrwCGt/cibl1GlePpx78WPdo03tgAyl0Qf+I35n484/gR1l9eixBQqYw==", "dev": true, - "license": "MIT", "dependencies": { "debug": "4.3.7", "jsonwebtoken": "9.0.2" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { "type": "opencollective", @@ -15318,49 +18740,89 @@ } }, "node_modules/@verdaccio/tarball": { - "version": "13.0.0-next-8.1", - "resolved": "https://registry.npmjs.org/@verdaccio/tarball/-/tarball-13.0.0-next-8.1.tgz", - "integrity": "sha512-58uimU2Bqt9+s+9ixy7wK/nPCqbOXhhhr/MQjl+otIlsUhSeATndhFzEctz/W+4MhUDg0tUnE9HC2yeNHHAo1Q==", + "version": "13.0.0-next-8.4", + "resolved": "https://registry.npmjs.org/@verdaccio/tarball/-/tarball-13.0.0-next-8.4.tgz", + "integrity": "sha512-zizQwACK+P9sHtArbuW5MJluRpc3lC6bilGTFNc0TLkHbwL73F8wxkKr5VLzWV7H54+sVKMDs1lCnaoHa0ygmw==", "dev": true, - "license": "MIT", "dependencies": { - "@verdaccio/core": "8.0.0-next-8.1", - "@verdaccio/url": "13.0.0-next-8.1", - "@verdaccio/utils": "7.0.1-next-8.1", + "@verdaccio/core": "8.0.0-next-8.4", + "@verdaccio/url": "13.0.0-next-8.4", + "@verdaccio/utils": "8.1.0-next-8.4", "debug": "4.3.7", "gunzip-maybe": "^1.4.2", "lodash": "4.17.21", "tar-stream": "^3.1.7" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/verdaccio" } }, - "node_modules/@verdaccio/ui-theme": { - "version": "8.0.0-next-8.1", - "resolved": "https://registry.npmjs.org/@verdaccio/ui-theme/-/ui-theme-8.0.0-next-8.1.tgz", - "integrity": "sha512-9PxV8+jE2Tr+iy9DQW/bzny4YqOlW0mCZ9ct6jhcUW4GdfzU//gY2fBN/DDtQVmfbTy8smuj4Enyv5f0wCsnYg==", + "node_modules/@verdaccio/tarball/node_modules/@verdaccio/utils": { + "version": "8.1.0-next-8.4", + "resolved": "https://registry.npmjs.org/@verdaccio/utils/-/utils-8.1.0-next-8.4.tgz", + "integrity": "sha512-mAEBWV5zsjtC4e/hfj1Q/eYtMlML5wxedk7mqqmvAydjw+ycSH/D/ksU+B10h4STX2NcBlcLtgLl7OI/wFzrgA==", "dev": true, - "license": "MIT" + "dependencies": { + "@verdaccio/core": "8.0.0-next-8.4", + "lodash": "4.17.21", + "minimatch": "7.4.6", + "semver": "7.6.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/@verdaccio/tarball/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@verdaccio/tarball/node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@verdaccio/ui-theme": { + "version": "8.0.0-next-8.4", + "resolved": "https://registry.npmjs.org/@verdaccio/ui-theme/-/ui-theme-8.0.0-next-8.4.tgz", + "integrity": "sha512-j3STxUuIgvn058LqfXWv+SwRi1fQ7HapMSfVKXAhi09+f4zlD2mtvKLth0WbuzU1NqJPTGLAP9ueBf1210C1Hw==", + "dev": true }, "node_modules/@verdaccio/url": { - "version": "13.0.0-next-8.1", - "resolved": "https://registry.npmjs.org/@verdaccio/url/-/url-13.0.0-next-8.1.tgz", - "integrity": "sha512-h6pkJf+YtogImKgOrmPP9UVG3p3gtb67gqkQU0bZnK+SEKQt6Rkek/QvtJ8MbmciagYS18bDhpI8DxqLHjDfZQ==", + "version": "13.0.0-next-8.4", + "resolved": "https://registry.npmjs.org/@verdaccio/url/-/url-13.0.0-next-8.4.tgz", + "integrity": "sha512-Xo+9DUcwYTBV6d0n4vjLAN2k92J33XM/9JNltWM6140oI8lz+VJKiajtejG/hRBi82RioRdWJ0RZDDY6FsbS3Q==", "dev": true, - "license": "MIT", "dependencies": { - "@verdaccio/core": "8.0.0-next-8.1", + "@verdaccio/core": "8.0.0-next-8.4", "debug": "4.3.7", "lodash": "4.17.21", "validator": "13.12.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "type": "opencollective", @@ -15387,6 +18849,45 @@ "url": "https://opencollective.com/verdaccio" } }, + "node_modules/@verdaccio/utils/node_modules/@verdaccio/core": { + "version": "8.0.0-next-8.1", + "resolved": "https://registry.npmjs.org/@verdaccio/core/-/core-8.0.0-next-8.1.tgz", + "integrity": "sha512-kQRCB2wgXEh8H88G51eQgAFK9IxmnBtkQ8sY5FbmB6PbBkyHrbGcCp+2mtRqqo36j0W1VAlfM3XzoknMy6qQnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "core-js": "3.37.1", + "http-errors": "2.0.0", + "http-status-codes": "2.3.0", + "process-warning": "1.0.0", + "semver": "7.6.3" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, + "node_modules/@verdaccio/utils/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@verdaccio/utils/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -15397,6 +18898,25 @@ "balanced-match": "^1.0.0" } }, + "node_modules/@verdaccio/utils/node_modules/core-js": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz", + "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/@verdaccio/utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/@verdaccio/utils/node_modules/minimatch": { "version": "7.4.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", @@ -15493,6 +19013,17 @@ "node": ">=8" } }, + "node_modules/@zip.js/zip.js": { + "version": "2.7.52", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.52.tgz", + "integrity": "sha512-+5g7FQswvrCHwYKNMd/KFxZSObctLSsQOgqBSi0LzwHo3li9Eh1w5cF5ndjQw9Zbr3ajVnd2+XyiX85gAetx1Q==", + "dev": true, + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=16.5.0" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -15627,16 +19158,16 @@ } }, "node_modules/ansi-escapes": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", - "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", "dev": true, "license": "MIT", "dependencies": { - "type-fest": "^1.0.2" + "environment": "^1.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -15918,9 +19449,9 @@ "license": "MIT" }, "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "dev": true, "license": "MIT" }, @@ -16050,9 +19581,9 @@ "license": "0BSD" }, "node_modules/aws-cdk": { - "version": "2.158.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.158.0.tgz", - "integrity": "sha512-UcrxBG02RACrnTvfuyZiTuOz8gqOpnqjCMTdVmdpExv5qk9hddhtRAubNaC4xleHuNJnvskYqqVW+Y3Abh6zGQ==", + "version": "2.164.1", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.164.1.tgz", + "integrity": "sha512-dWRViQgHLe7GHkPIQGA+8EQSm8TBcxemyCC3HHW3wbLMWUDbspio9Dktmw5EmWxlFjjWh86Dk1JWf1zKQo8C5g==", "license": "Apache-2.0", "peer": true, "bin": { @@ -16066,9 +19597,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.158.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.158.0.tgz", - "integrity": "sha512-Pl9CCLM+XRTy6nyyRJM1INEMtwIlZOib0FWyq9i9E388vurw7sNVJ6tAsfLpGIOLHsFQCbF4f6OZ0KSVxmMaiA==", + "version": "2.164.1", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.164.1.tgz", + "integrity": "sha512-jNvVmfZJbZoAYU94b5dzTlF2z6JXJ204NgcYY5haOa6mq3m2bzdYPXnPtB5kpAX3oBi++yoRdmLhqgckdEhUZA==", "bundleDependencies": [ "@balena/dockerignore", "case", @@ -16087,7 +19618,7 @@ "@aws-cdk/asset-awscli-v1": "^2.2.202", "@aws-cdk/asset-kubectl-v20": "^2.1.2", "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", - "@aws-cdk/cloud-assembly-schema": "^36.0.24", + "@aws-cdk/cloud-assembly-schema": "^38.0.0", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", "fs-extra": "^11.2.0", @@ -16527,9 +20058,9 @@ } }, "node_modules/b4a": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", - "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", "dev": true, "license": "Apache-2.0" }, @@ -16675,9 +20206,9 @@ "license": "MIT" }, "node_modules/bare-events": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", - "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz", + "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==", "dev": true, "license": "Apache-2.0", "optional": true @@ -16732,7 +20263,6 @@ "resolved": "https://registry.npmjs.org/better-path-resolve/-/better-path-resolve-1.0.0.tgz", "integrity": "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==", "dev": true, - "license": "MIT", "dependencies": { "is-windows": "^1.0.0" }, @@ -16774,16 +20304,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/body-parser/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/body-parser/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -16801,22 +20321,6 @@ "dev": true, "license": "MIT" }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/bowser": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", @@ -16983,11 +20487,10 @@ } }, "node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -17445,16 +20948,16 @@ } }, "node_modules/cli-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", - "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, "license": "MIT", "dependencies": { - "restore-cursor": "^4.0.0" + "restore-cursor": "^5.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -17473,22 +20976,76 @@ } }, "node_modules/cli-truncate": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", - "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", "dev": true, "license": "MIT", "dependencies": { "slice-ansi": "^5.0.0", - "string-width": "^5.0.0" + "string-width": "^7.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/cli-width": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", @@ -17499,9 +21056,9 @@ } }, "node_modules/clipanion": { - "version": "4.0.0-rc.3", - "resolved": "https://registry.npmjs.org/clipanion/-/clipanion-4.0.0-rc.3.tgz", - "integrity": "sha512-+rJOJMt2N6Oikgtfqmo/Duvme7uz3SIedL2b6ycgCztQMiTfr3aQh2DDyLHl+QUPClKMNpSg3gDJFvNQYIcq1g==", + "version": "4.0.0-rc.4", + "resolved": "https://registry.npmjs.org/clipanion/-/clipanion-4.0.0-rc.4.tgz", + "integrity": "sha512-CXkMQxU6s9GklO/1f714dkKBMu1lopS1WFF0B8o4AxPykR1hpozxSiUZ5ZUeBjfPgCWqbcNOtZVFhB8Lkfp1+Q==", "dev": true, "license": "MIT", "workspaces": [ @@ -17616,13 +21173,13 @@ } }, "node_modules/commander": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", - "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/comment-parser": { @@ -17658,18 +21215,17 @@ } }, "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz", + "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==", "dev": true, - "license": "MIT", "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", + "bytes": "3.1.2", + "compressible": "~2.0.18", "debug": "2.6.9", + "negotiator": "~0.6.4", "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", + "safe-buffer": "5.2.1", "vary": "~1.1.2" }, "engines": { @@ -17693,12 +21249,14 @@ "dev": true, "license": "MIT" }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "dev": true, - "license": "MIT" + "engines": { + "node": ">= 0.6" + } }, "node_modules/concat-map": { "version": "0.0.1", @@ -17718,12 +21276,11 @@ } }, "node_modules/constructs": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.3.0.tgz", - "integrity": "sha512-vbK8i3rIb/xwZxSpTjz3SagHn1qq9BChLEfy5Hf6fB3/2eFbrwt2n9kHwQcS0CPTRBesreeAcsJfMq2229FnbQ==", - "license": "Apache-2.0", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.3.2.tgz", + "integrity": "sha512-odjsmhoBKRWa2F/Z3edOSZCb7IgxAL5usXQMRKoINMJzcFfC1GvcbO6Dd/xMGLRv4J/tEsjSLwqLxRfJrjPsQw==", "engines": { - "node": ">= 16.14.0" + "node": ">= 18.12.0" } }, "node_modules/content-disposition": { @@ -17756,9 +21313,9 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, "license": "MIT", "engines": { @@ -17865,10 +21422,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "license": "MIT", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -18261,7 +21817,6 @@ "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } @@ -18437,6 +21992,19 @@ "node": ">=4" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -19862,9 +23430,9 @@ } }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dev": true, "license": "MIT", "dependencies": { @@ -19873,7 +23441,7 @@ "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -19928,22 +23496,6 @@ "dev": true, "license": "MIT" }, - "node_modules/express/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -20372,18 +23924,18 @@ } }, "node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", + "combined-stream": "^1.0.8", "mime-types": "^2.1.12" }, "engines": { - "node": ">= 0.12" + "node": ">= 6" } }, "node_modules/formdata-polyfill": { @@ -20422,6 +23974,7 @@ "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -20515,6 +24068,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -20980,15 +24546,15 @@ } }, "node_modules/http-signature": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", - "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", "dev": true, "license": "MIT", "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^2.0.2", - "sshpk": "^1.14.1" + "sshpk": "^1.18.0" }, "engines": { "node": ">=0.10" @@ -21096,6 +24662,7 @@ "version": "9.0.6", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.6.tgz", "integrity": "sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ==", + "dev": true, "license": "MIT", "funding": { "type": "opencollective", @@ -21732,7 +25299,6 @@ "resolved": "https://registry.npmjs.org/is-subdir/-/is-subdir-1.2.0.tgz", "integrity": "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==", "dev": true, - "license": "MIT", "dependencies": { "better-path-resolve": "1.0.0" }, @@ -22073,6 +25639,18 @@ "dev": true, "license": "(AFL-2.1 OR BSD-3-Clause)" }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -22376,13 +25954,16 @@ } }, "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/lines-and-columns": { @@ -22393,28 +25974,28 @@ "license": "MIT" }, "node_modules/lint-staged": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.3.0.tgz", - "integrity": "sha512-mPRtrYnipYYv1FEE134ufbWpeggNTo+O/UPzngoaKzbzHAthvR55am+8GfHTnqNRQVRRrYQLGW9ZyUoD7DsBHQ==", + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.10.tgz", + "integrity": "sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "5.3.0", - "commander": "11.0.0", - "debug": "4.3.4", - "execa": "7.2.0", - "lilconfig": "2.1.0", - "listr2": "6.6.1", - "micromatch": "4.0.5", - "pidtree": "0.6.0", - "string-argv": "0.3.2", - "yaml": "2.3.1" + "chalk": "~5.3.0", + "commander": "~12.1.0", + "debug": "~4.3.6", + "execa": "~8.0.1", + "lilconfig": "~3.1.2", + "listr2": "~8.2.4", + "micromatch": "~4.0.8", + "pidtree": "~0.6.0", + "string-argv": "~0.3.2", + "yaml": "~2.5.0" }, "bin": { "lint-staged": "bin/lint-staged.js" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">=18.12.0" }, "funding": { "url": "https://opencollective.com/lint-staged" @@ -22433,123 +26014,107 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/lint-staged/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "node_modules/listr2": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", + "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", "dev": true, "license": "MIT", "dependencies": { - "ms": "2.1.2" + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=18.0.0" } }, - "node_modules/lint-staged/node_modules/execa": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", - "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^3.0.7", - "strip-final-newline": "^3.0.0" - }, "engines": { - "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + "node": ">=12" }, "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/lint-staged/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/lint-staged/node_modules/human-signals": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", - "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=14.18.0" - } + "license": "MIT" }, - "node_modules/lint-staged/node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8.6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lint-staged/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/lint-staged/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } }, - "node_modules/listr2": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-6.6.1.tgz", - "integrity": "sha512-+rAXGHh0fkEWdXBmX+L6mmfmXmXvDGEKzkjxO+8mP3+nI/r/CWznVBvsibXdxda9Zz0OW2e2ikphN3OwCT/jSg==", + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "dev": true, "license": "MIT", "dependencies": { - "cli-truncate": "^3.1.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^5.0.1", - "rfdc": "^1.3.0", - "wrap-ansi": "^8.1.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "enquirer": ">= 2.3.0 < 3" + "node": ">=18" }, - "peerDependenciesMeta": { - "enquirer": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/locate-path": { @@ -22678,8 +26243,7 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/log-symbols": { "version": "3.0.0", @@ -22765,20 +26329,20 @@ } }, "node_modules/log-update": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-5.0.1.tgz", - "integrity": "sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-escapes": "^5.0.0", - "cli-cursor": "^4.0.0", - "slice-ansi": "^5.0.0", - "strip-ansi": "^7.0.1", - "wrap-ansi": "^8.0.1" + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -22797,6 +26361,77 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/log-update/node_modules/strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", @@ -22813,6 +26448,24 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/long": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", @@ -23026,6 +26679,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -23038,6 +26692,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -23055,6 +26710,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -23142,66 +26810,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/mv": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", - "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mkdirp": "~0.5.1", - "ncp": "~2.0.0", - "rimraf": "~2.4.0" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/mv/node_modules/glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mv/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/mv/node_modules/rimraf": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", - "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^6.0.1" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/mysql2": { "version": "3.9.9", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.9.tgz", @@ -23283,16 +26891,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "license": "MIT" }, - "node_modules/ncp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", - "dev": true, - "license": "MIT", - "bin": { - "ncp": "bin/ncp" - } - }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -23823,15 +27421,13 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz", "integrity": "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/p-filter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz", "integrity": "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==", "dev": true, - "license": "MIT", "dependencies": { "p-map": "^2.0.0" }, @@ -23871,7 +27467,6 @@ "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } @@ -24247,32 +27842,32 @@ } }, "node_modules/pino": { - "version": "8.17.2", - "resolved": "https://registry.npmjs.org/pino/-/pino-8.17.2.tgz", - "integrity": "sha512-LA6qKgeDMLr2ux2y/YiUt47EfgQ+S9LznBWOJdN3q1dx2sv0ziDLUBeVpyVv17TEcGCBuWf0zNtg3M5m1NhhWQ==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.4.0.tgz", + "integrity": "sha512-nbkQb5+9YPhQRz/BeQmrWpEknAaqjpAqRK8NwJpmrX/JHu7JuZC5G1CeAwJDJfGes4h+YihC6in3Q2nGb+Y09w==", "dev": true, "license": "MIT", "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "v1.1.0", - "pino-std-serializers": "^6.0.0", - "process-warning": "^3.0.0", + "pino-abstract-transport": "^1.2.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^4.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^3.7.0", - "thread-stream": "^2.0.0" + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "node_modules/pino-abstract-transport": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", - "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", "dev": true, "license": "MIT", "dependencies": { @@ -24364,19 +27959,29 @@ } }, "node_modules/pino-std-serializers": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", - "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", "dev": true, "license": "MIT" }, "node_modules/pino/node_modules/process-warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", - "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", + "integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==", "dev": true, "license": "MIT" }, + "node_modules/pino/node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "dev": true, + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/pkg-dir": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", @@ -24624,13 +28229,6 @@ "node": ">= 0.10" } }, - "node_modules/pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", - "dev": true, - "license": "ISC" - }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -24671,13 +28269,13 @@ } }, "node_modules/qs": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", - "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -24763,16 +28361,6 @@ "node": ">= 0.8" } }, - "node_modules/raw-body/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -25133,55 +28721,38 @@ } }, "node_modules/restore-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", - "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, "license": "MIT", "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/restore-cursor/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/restore-cursor/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, "license": "MIT", "dependencies": { - "mimic-fn": "^2.1.0" + "mimic-function": "^5.0.0" }, "engines": { - "node": ">=6" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -25795,9 +29366,9 @@ } }, "node_modules/sonic-boom": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.0.tgz", - "integrity": "sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz", + "integrity": "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==", "dev": true, "license": "MIT", "dependencies": { @@ -25833,89 +29404,15 @@ } }, "node_modules/spawndamnit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/spawndamnit/-/spawndamnit-2.0.0.tgz", - "integrity": "sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/spawndamnit/node_modules/cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "node_modules/spawndamnit/node_modules/lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "license": "ISC", - "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "node_modules/spawndamnit/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/spawndamnit/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/spawndamnit/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/spawndamnit/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spawndamnit/-/spawndamnit-3.0.1.tgz", + "integrity": "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==", "dev": true, - "license": "ISC", "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" + "cross-spawn": "^7.0.5", + "signal-exit": "^4.0.1" } }, - "node_modules/spawndamnit/node_modules/yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", - "dev": true, - "license": "ISC" - }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -26518,14 +30015,11 @@ } }, "node_modules/text-decoder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.0.tgz", - "integrity": "sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.1.tgz", + "integrity": "sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } + "license": "Apache-2.0" }, "node_modules/text-table": { "version": "0.2.0", @@ -26534,9 +30028,9 @@ "license": "MIT" }, "node_modules/thread-stream": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz", - "integrity": "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", "dev": true, "license": "MIT", "dependencies": { @@ -26681,6 +30175,11 @@ "node": ">=0.10.0" } }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==" + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -26851,19 +30350,6 @@ "node": ">=4" } }, - "node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -27332,33 +30818,32 @@ } }, "node_modules/verdaccio": { - "version": "5.32.2", - "resolved": "https://registry.npmjs.org/verdaccio/-/verdaccio-5.32.2.tgz", - "integrity": "sha512-QnVYIUvwB884fwVcA/D+x7AabsRPlTPyYAKMtExm8kJjiH+s2LGK2qX2o3I4VmYXqBR3W9b8gEnyQnGwQhUPsw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/verdaccio/-/verdaccio-6.0.2.tgz", + "integrity": "sha512-XthgJlF1hGW+GR/apRLZ7DQw26XpLI+xjMGb7dhJKxI4Pz2gSiEY1RXP9T9I/rlIBr9Zx6rYOgRk7A9Aeq/kpg==", "dev": true, - "license": "MIT", "dependencies": { - "@cypress/request": "3.0.1", - "@verdaccio/auth": "8.0.0-next-8.1", - "@verdaccio/config": "8.0.0-next-8.1", - "@verdaccio/core": "8.0.0-next-8.1", + "@cypress/request": "3.0.5", + "@verdaccio/auth": "8.0.0-next-8.4", + "@verdaccio/config": "8.0.0-next-8.4", + "@verdaccio/core": "8.0.0-next-8.4", "@verdaccio/local-storage-legacy": "11.0.2", - "@verdaccio/logger-7": "8.0.0-next-8.1", - "@verdaccio/middleware": "8.0.0-next-8.1", - "@verdaccio/search-indexer": "8.0.0-next-8.0", - "@verdaccio/signature": "8.0.0-next-8.0", + "@verdaccio/logger": "8.0.0-next-8.4", + "@verdaccio/middleware": "8.0.0-next-8.4", + "@verdaccio/search-indexer": "8.0.0-next-8.2", + "@verdaccio/signature": "8.0.0-next-8.1", "@verdaccio/streams": "10.2.1", - "@verdaccio/tarball": "13.0.0-next-8.1", - "@verdaccio/ui-theme": "8.0.0-next-8.1", - "@verdaccio/url": "13.0.0-next-8.1", + "@verdaccio/tarball": "13.0.0-next-8.4", + "@verdaccio/ui-theme": "8.0.0-next-8.4", + "@verdaccio/url": "13.0.0-next-8.4", "@verdaccio/utils": "7.0.1-next-8.1", - "async": "3.2.5", - "clipanion": "4.0.0-rc.3", - "compression": "1.7.4", + "async": "3.2.6", + "clipanion": "4.0.0-rc.4", + "compression": "1.7.5", "cors": "2.8.5", - "debug": "^4.3.5", - "envinfo": "7.13.0", - "express": "4.21.0", + "debug": "4.3.7", + "envinfo": "7.14.0", + "express": "4.21.1", "express-rate-limit": "5.5.1", "fast-safe-stringify": "2.1.1", "handlebars": "4.7.8", @@ -27370,18 +30855,17 @@ "lru-cache": "7.18.3", "mime": "3.0.0", "mkdirp": "1.0.4", - "mv": "2.1.1", "pkginfo": "0.4.1", "semver": "7.6.3", "validator": "13.12.0", - "verdaccio-audit": "13.0.0-next-8.1", - "verdaccio-htpasswd": "13.0.0-next-8.1" + "verdaccio-audit": "13.0.0-next-8.4", + "verdaccio-htpasswd": "13.0.0-next-8.4" }, "bin": { "verdaccio": "bin/verdaccio" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { "type": "opencollective", @@ -27389,20 +30873,19 @@ } }, "node_modules/verdaccio-audit": { - "version": "13.0.0-next-8.1", - "resolved": "https://registry.npmjs.org/verdaccio-audit/-/verdaccio-audit-13.0.0-next-8.1.tgz", - "integrity": "sha512-EEfUeC1kHuErtwF9FC670W+EXHhcl+iuigONkcprwRfkPxmdBs+Hx36745hgAMZ9SCqedNECaycnGF3tZ3VYfw==", + "version": "13.0.0-next-8.4", + "resolved": "https://registry.npmjs.org/verdaccio-audit/-/verdaccio-audit-13.0.0-next-8.4.tgz", + "integrity": "sha512-T4yi/46fLngllx5mvFtXsGcW3MxGZZ9IkHYPK1OQw9+Xj9aOuMec2eFdztTRo9SgqZfgblGSY1ESZYy19sQLvw==", "dev": true, - "license": "MIT", "dependencies": { - "@verdaccio/config": "8.0.0-next-8.1", - "@verdaccio/core": "8.0.0-next-8.1", - "express": "4.21.0", + "@verdaccio/config": "8.0.0-next-8.4", + "@verdaccio/core": "8.0.0-next-8.4", + "express": "4.21.1", "https-proxy-agent": "5.0.1", "node-fetch": "cjs" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "type": "opencollective", @@ -27410,14 +30893,13 @@ } }, "node_modules/verdaccio-htpasswd": { - "version": "13.0.0-next-8.1", - "resolved": "https://registry.npmjs.org/verdaccio-htpasswd/-/verdaccio-htpasswd-13.0.0-next-8.1.tgz", - "integrity": "sha512-BfvmO+ZdbwfttOwrdTPD6Bccr1ZfZ9Tk/9wpXamxdWB/XPWlk3FtyGsvqCmxsInRLPhQ/FSk9c3zRCGvICTFYg==", + "version": "13.0.0-next-8.4", + "resolved": "https://registry.npmjs.org/verdaccio-htpasswd/-/verdaccio-htpasswd-13.0.0-next-8.4.tgz", + "integrity": "sha512-w0knjKz8SdBGSv0Kt61LoUOCYBZ2/iig3bVbGFWTj4MwCUG6eNewMoQ6nbrk+kyHNFVq75IzT1eIhXDEysx15Q==", "dev": true, - "license": "MIT", "dependencies": { - "@verdaccio/core": "8.0.0-next-8.1", - "@verdaccio/file-locking": "13.0.0-next-8.0", + "@verdaccio/core": "8.0.0-next-8.4", + "@verdaccio/file-locking": "13.0.0-next-8.2", "apache-md5": "1.1.8", "bcryptjs": "2.4.3", "core-js": "3.37.1", @@ -27426,7 +30908,7 @@ "unix-crypt-td-js": "1.1.4" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "type": "opencollective", @@ -27434,16 +30916,15 @@ } }, "node_modules/verdaccio-htpasswd/node_modules/@verdaccio/file-locking": { - "version": "13.0.0-next-8.0", - "resolved": "https://registry.npmjs.org/@verdaccio/file-locking/-/file-locking-13.0.0-next-8.0.tgz", - "integrity": "sha512-28XRwpKiE3Z6KsnwE7o8dEM+zGWOT+Vef7RVJyUlG176JVDbGGip3HfCmFioE1a9BklLyGEFTu6D69BzfbRkzA==", + "version": "13.0.0-next-8.2", + "resolved": "https://registry.npmjs.org/@verdaccio/file-locking/-/file-locking-13.0.0-next-8.2.tgz", + "integrity": "sha512-TcHgN3I/N28WBSvtukpGrJhBljl4jyIXq0vEv94vXAG6nUE3saK+vtgo8PfYA3Ueo88v/1zyAbiZM4uxwojCmQ==", "dev": true, - "license": "MIT", "dependencies": { "lockfile": "1.0.4" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "type": "opencollective", @@ -27456,7 +30937,6 @@ "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==", "dev": true, "hasInstallScript": true, - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -27469,19 +30949,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/verdaccio/node_modules/envinfo": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz", - "integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==", - "dev": true, - "license": "MIT", - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/verdaccio/node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -27900,10 +31367,14 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", - "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "dev": true, "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, "engines": { "node": ">= 14" } @@ -28039,51 +31510,61 @@ } }, "packages/ai-constructs": { - "version": "0.1.4", + "name": "@aws-amplify/ai-constructs", + "version": "1.0.0", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/plugin-types": "^1.0.1", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/platform-core": "^1.2.0", + "@aws-amplify/plugin-types": "^1.3.1", "@aws-sdk/client-bedrock-runtime": "^3.622.0", - "@smithy/types": "^3.3.0" + "@smithy/types": "^3.3.0", + "json-schema-to-ts": "^3.1.1" + }, + "devDependencies": { + "@aws-amplify/backend-output-storage": "^1.1.3", + "typescript": "^5.0.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0" } }, "packages/ampx": { - "version": "0.2.1", + "version": "0.2.2", "license": "Apache-2.0" }, "packages/auth-construct": { - "version": "1.3.0", + "name": "@aws-amplify/auth-construct", + "version": "1.5.0", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/backend-output-schemas": "^1.1.0", - "@aws-amplify/backend-output-storage": "^1.1.1", - "@aws-amplify/plugin-types": "^1.2.1", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/backend-output-storage": "^1.1.3", + "@aws-amplify/plugin-types": "^1.4.0", "@aws-sdk/util-arn-parser": "^3.568.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0" } }, "packages/backend": { - "version": "1.2.1", + "name": "@aws-amplify/backend", + "version": "1.8.0", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/backend-auth": "^1.1.2", - "@aws-amplify/backend-data": "^1.1.3", - "@aws-amplify/backend-function": "^1.4.0", - "@aws-amplify/backend-output-schemas": "^1.2.0", - "@aws-amplify/backend-output-storage": "^1.1.1", - "@aws-amplify/backend-secret": "^1.0.1", - "@aws-amplify/backend-storage": "^1.1.1", - "@aws-amplify/client-config": "^1.3.0", + "@aws-amplify/backend-auth": "^1.4.1", + "@aws-amplify/backend-data": "^1.2.1", + "@aws-amplify/backend-function": "^1.8.0", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/backend-output-storage": "^1.1.3", + "@aws-amplify/backend-secret": "^1.1.4", + "@aws-amplify/backend-storage": "^1.2.3", + "@aws-amplify/client-config": "^1.5.2", "@aws-amplify/data-schema": "^1.0.0", - "@aws-amplify/platform-core": "^1.1.0", - "@aws-amplify/plugin-types": "^1.2.1", + "@aws-amplify/platform-core": "^1.2.1", + "@aws-amplify/plugin-types": "^1.5.0", "@aws-sdk/client-amplify": "^3.624.0", "lodash.snakecase": "^4.1.1" }, @@ -28093,95 +31574,105 @@ "aws-lambda": "^1.0.7" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0" } }, "packages/backend-ai": { - "version": "0.1.1", + "name": "@aws-amplify/backend-ai", + "version": "1.0.1", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/ai-constructs": "^0.1.4", - "@aws-amplify/backend-output-schemas": "^1.1.0", - "@aws-amplify/backend-output-storage": "^1.0.2", - "@aws-amplify/platform-core": "^1.1.0", - "@aws-amplify/plugin-types": "^1.0.1" + "@aws-amplify/ai-constructs": "^1.0.0", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/backend-output-storage": "^1.1.3", + "@aws-amplify/data-schema-types": "^1.2.0", + "@aws-amplify/platform-core": "^1.2.1", + "@aws-amplify/plugin-types": "^1.5.0" }, "peerDependencies": { - "@smithy/types": "^3.3.0", - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0" } }, "packages/backend-auth": { - "version": "1.1.4", + "name": "@aws-amplify/backend-auth", + "version": "1.4.1", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/auth-construct": "^1.3.0", - "@aws-amplify/backend-output-storage": "^1.1.1", - "@aws-amplify/plugin-types": "^1.2.1" + "@aws-amplify/auth-construct": "^1.5.0", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/backend-output-storage": "^1.1.3", + "@aws-amplify/plugin-types": "^1.5.0" }, "devDependencies": { - "@aws-amplify/backend-platform-test-stubs": "^0.3.4", - "@aws-amplify/platform-core": "^1.0.6" + "@aws-amplify/backend-platform-test-stubs": "^0.3.6", + "@aws-amplify/platform-core": "^1.2.1", + "@aws-sdk/client-cognito-identity": "^3.624.0", + "@aws-sdk/client-cognito-identity-provider": "^3.624.0", + "@types/aws-lambda": "^8.10.119", + "aws-lambda": "^1.0.7" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0" } }, "packages/backend-data": { - "version": "1.1.3", + "name": "@aws-amplify/backend-data", + "version": "1.2.1", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/backend-output-schemas": "^1.1.0", - "@aws-amplify/backend-output-storage": "^1.1.1", - "@aws-amplify/data-construct": "^1.9.6", - "@aws-amplify/data-schema-types": "^1.1.1", - "@aws-amplify/plugin-types": "^1.2.1" + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/backend-output-storage": "^1.1.3", + "@aws-amplify/data-construct": "^1.10.1", + "@aws-amplify/data-schema-types": "^1.2.0", + "@aws-amplify/plugin-types": "^1.5.0" }, "devDependencies": { - "@aws-amplify/backend-platform-test-stubs": "^0.3.4", + "@aws-amplify/backend-platform-test-stubs": "^0.3.6", "@aws-amplify/data-schema": "^1.0.0", - "@aws-amplify/platform-core": "^1.0.7" + "@aws-amplify/platform-core": "^1.2.1" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0" } }, "packages/backend-deployer": { - "version": "1.1.2", + "name": "@aws-amplify/backend-deployer", + "version": "1.1.9", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/platform-core": "^1.0.6", - "@aws-amplify/plugin-types": "^1.2.1", + "@aws-amplify/platform-core": "^1.2.0", + "@aws-amplify/plugin-types": "^1.4.0", "execa": "^8.0.1", "tsx": "^4.6.1" }, "peerDependencies": { - "aws-cdk": "^2.152.0", + "aws-cdk": "^2.158.0", "typescript": "^5.0.0" } }, "packages/backend-function": { - "version": "1.4.0", + "name": "@aws-amplify/backend-function", + "version": "1.8.0", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/backend-output-schemas": "^1.1.0", - "@aws-amplify/backend-output-storage": "^1.1.1", - "@aws-amplify/plugin-types": "^1.2.1", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/backend-output-storage": "^1.1.3", + "@aws-amplify/plugin-types": "^1.5.0", "execa": "^8.0.1" }, "devDependencies": { - "@aws-amplify/backend-platform-test-stubs": "^0.3.4", - "@aws-amplify/platform-core": "^1.1.0", + "@aws-amplify/backend-platform-test-stubs": "^0.3.6", + "@aws-amplify/platform-core": "^1.2.1", "@aws-sdk/client-ssm": "^3.624.0", "aws-sdk": "^2.1550.0", "uuid": "^9.0.1" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0" } }, @@ -28200,7 +31691,8 @@ } }, "packages/backend-output-schemas": { - "version": "1.2.0", + "name": "@aws-amplify/backend-output-schemas", + "version": "1.4.0", "license": "Apache-2.0", "devDependencies": { "@aws-amplify/plugin-types": "^1.2.0" @@ -28210,30 +31702,35 @@ } }, "packages/backend-output-storage": { - "version": "1.1.1", + "name": "@aws-amplify/backend-output-storage", + "version": "1.1.3", "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^1.2.0", - "@aws-amplify/platform-core": "^1.0.6" + "@aws-amplify/platform-core": "^1.0.6", + "@aws-amplify/plugin-types": "^1.3.1" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0" + "aws-cdk-lib": "^2.158.0" } }, "packages/backend-platform-test-stubs": { - "version": "0.3.4", + "name": "@aws-amplify/backend-platform-test-stubs", + "version": "0.3.6", "license": "Apache-2.0", "dependencies": { - "aws-cdk-lib": "^2.152.0", + "@aws-amplify/plugin-types": "^1.3.1", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0" } }, "packages/backend-secret": { - "version": "1.1.1", + "name": "@aws-amplify/backend-secret", + "version": "1.1.5", "license": "Apache-2.0", "dependencies": { "@aws-amplify/platform-core": "^1.0.5", - "@aws-amplify/plugin-types": "^1.1.1", + "@aws-amplify/plugin-types": "^1.2.2", "@aws-sdk/client-ssm": "^3.624.0" }, "devDependencies": { @@ -28241,37 +31738,40 @@ } }, "packages/backend-storage": { - "version": "1.1.2", + "name": "@aws-amplify/backend-storage", + "version": "1.2.3", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/backend-output-schemas": "^1.2.0", - "@aws-amplify/backend-output-storage": "^1.1.1", - "@aws-amplify/plugin-types": "^1.2.1" + "@aws-amplify/backend-output-schemas": "^1.2.1", + "@aws-amplify/backend-output-storage": "^1.1.3", + "@aws-amplify/plugin-types": "^1.5.0" }, "devDependencies": { - "@aws-amplify/backend-platform-test-stubs": "^0.3.4", - "@aws-amplify/platform-core": "^1.0.6" + "@aws-amplify/backend-platform-test-stubs": "^0.3.6", + "@aws-amplify/platform-core": "^1.2.1" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0" } }, "packages/cli": { - "version": "1.2.6", + "name": "@aws-amplify/backend-cli", + "version": "1.4.2", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/backend-deployer": "^1.1.1", - "@aws-amplify/backend-output-schemas": "^1.1.0", - "@aws-amplify/backend-secret": "^1.1.0", - "@aws-amplify/cli-core": "^1.1.2", - "@aws-amplify/client-config": "^1.2.1", - "@aws-amplify/deployed-backend-client": "^1.3.0", - "@aws-amplify/form-generator": "^1.0.1", - "@aws-amplify/model-generator": "^1.0.5", - "@aws-amplify/platform-core": "^1.0.5", - "@aws-amplify/sandbox": "^1.2.0", - "@aws-amplify/schema-generator": "^1.2.1", + "@aws-amplify/backend-deployer": "^1.1.9", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/backend-secret": "^1.1.2", + "@aws-amplify/cli-core": "^1.2.0", + "@aws-amplify/client-config": "^1.5.1", + "@aws-amplify/deployed-backend-client": "^1.4.1", + "@aws-amplify/form-generator": "^1.0.3", + "@aws-amplify/model-generator": "^1.0.9", + "@aws-amplify/platform-core": "^1.2.0", + "@aws-amplify/plugin-types": "^1.4.0", + "@aws-amplify/sandbox": "^1.2.5", + "@aws-amplify/schema-generator": "^1.2.5", "@aws-sdk/client-amplify": "^3.624.0", "@aws-sdk/client-cloudformation": "^3.624.0", "@aws-sdk/client-s3": "^3.624.0", @@ -28303,7 +31803,8 @@ } }, "packages/cli-core": { - "version": "1.1.2", + "name": "@aws-amplify/cli-core", + "version": "1.2.0", "license": "Apache-2.0", "dependencies": { "@aws-amplify/platform-core": "^1.0.5", @@ -28409,13 +31910,15 @@ } }, "packages/client-config": { - "version": "1.3.0", + "name": "@aws-amplify/client-config", + "version": "1.5.2", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/backend-output-schemas": "^1.2.0", - "@aws-amplify/deployed-backend-client": "^1.4.0", - "@aws-amplify/model-generator": "^1.0.5", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/deployed-backend-client": "^1.4.1", + "@aws-amplify/model-generator": "^1.0.7", "@aws-amplify/platform-core": "^1.0.7", + "@aws-amplify/plugin-types": "^1.3.1", "zod": "^3.22.2" }, "devDependencies": { @@ -28430,12 +31933,12 @@ } }, "packages/create-amplify": { - "version": "1.0.5", + "version": "1.0.6", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/cli-core": "^1.1.1", + "@aws-amplify/cli-core": "^1.1.3", "@aws-amplify/platform-core": "^1.0.3", - "@aws-amplify/plugin-types": "^1.1.0", + "@aws-amplify/plugin-types": "^1.2.2", "execa": "^8.0.1", "kleur": "^4.1.5", "yargs": "^17.7.2" @@ -28544,11 +32047,13 @@ } }, "packages/deployed-backend-client": { - "version": "1.4.0", + "name": "@aws-amplify/deployed-backend-client", + "version": "1.4.2", "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^1.2.0", "@aws-amplify/platform-core": "^1.0.5", + "@aws-amplify/plugin-types": "^1.2.2", "zod": "^3.22.2" }, "peerDependencies": { @@ -28559,6 +32064,7 @@ } }, "packages/eslint-rules": { + "name": "eslint-plugin-amplify-backend-rules", "version": "0.0.1", "license": "Apache-2.0", "dependencies": { @@ -28569,7 +32075,8 @@ } }, "packages/form-generator": { - "version": "1.0.1", + "name": "@aws-amplify/form-generator", + "version": "1.0.3", "license": "Apache-2.0", "dependencies": { "@aws-amplify/appsync-modelgen-plugin": "^2.11.0", @@ -28588,23 +32095,26 @@ } }, "packages/integration-tests": { - "version": "0.5.8", + "name": "@aws-amplify/integration-tests", + "version": "0.6.0", "license": "Apache-2.0", "devDependencies": { "@apollo/client": "^3.10.1", - "@aws-amplify/ai-constructs": "^0.1.0", - "@aws-amplify/auth-construct": "^1.2.2", - "@aws-amplify/backend": "^1.2.1", - "@aws-amplify/backend-ai": "^0.1.0", - "@aws-amplify/backend-secret": "^1.0.1", - "@aws-amplify/client-config": "^1.1.3", + "@aws-amplify/ai-constructs": "^1.0.0", + "@aws-amplify/auth-construct": "^1.4.0", + "@aws-amplify/backend": "^1.6.0", + "@aws-amplify/backend-ai": "^1.0.0", + "@aws-amplify/backend-secret": "^1.1.4", + "@aws-amplify/client-config": "^1.5.1", "@aws-amplify/data-schema": "^1.0.0", - "@aws-amplify/deployed-backend-client": "^1.3.0", + "@aws-amplify/deployed-backend-client": "^1.4.1", "@aws-amplify/platform-core": "^1.1.0", + "@aws-amplify/plugin-types": "^1.3.1", "@aws-sdk/client-accessanalyzer": "^3.624.0", "@aws-sdk/client-amplify": "^3.624.0", "@aws-sdk/client-bedrock-runtime": "^3.622.0", "@aws-sdk/client-cloudformation": "^3.624.0", + "@aws-sdk/client-cloudtrail": "^3.624.0", "@aws-sdk/client-cognito-identity": "^3.624.0", "@aws-sdk/client-cognito-identity-provider": "^3.624.0", "@aws-sdk/client-iam": "^3.624.0", @@ -28615,9 +32125,10 @@ "@aws-sdk/credential-providers": "^3.624.0", "@smithy/shared-ini-file-loader": "^2.2.5", "@types/lodash.ismatch": "^4.4.9", + "@zip.js/zip.js": "^2.7.52", "aws-amplify": "^6.0.16", "aws-appsync-auth-link": "^3.0.7", - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0", "execa": "^8.0.1", "fs-extra": "^11.1.1", @@ -28645,14 +32156,16 @@ } }, "packages/model-generator": { - "version": "1.0.6", + "name": "@aws-amplify/model-generator", + "version": "1.0.9", "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^1.1.0", - "@aws-amplify/deployed-backend-client": "^1.3.0", - "@aws-amplify/graphql-generator": "^0.4.0", + "@aws-amplify/deployed-backend-client": "^1.4.1", + "@aws-amplify/graphql-generator": "^0.5.1", "@aws-amplify/graphql-types-generator": "^3.6.0", "@aws-amplify/platform-core": "^1.0.5", + "@aws-amplify/plugin-types": "^1.4.0", "@aws-sdk/client-appsync": "^3.624.0", "@aws-sdk/client-s3": "^3.624.0", "@aws-sdk/credential-providers": "^3.624.0", @@ -28665,10 +32178,11 @@ } }, "packages/platform-core": { - "version": "1.1.0", + "name": "@aws-amplify/platform-core", + "version": "1.2.1", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/plugin-types": "^1.2.1", + "@aws-amplify/plugin-types": "^1.5.0", "@aws-sdk/client-sts": "^3.624.0", "is-ci": "^3.0.1", "lodash.mergewith": "^4.6.2", @@ -28696,14 +32210,15 @@ } }, "packages/plugin-types": { - "version": "1.2.1", + "name": "@aws-amplify/plugin-types", + "version": "1.5.0", "license": "Apache-2.0", "devDependencies": { "execa": "^5.1.1" }, "peerDependencies": { "@aws-sdk/types": "^3.609.0", - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0" } }, @@ -28824,22 +32339,22 @@ } }, "packages/sandbox": { - "version": "1.2.1", + "name": "@aws-amplify/sandbox", + "version": "1.2.6", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/backend-deployer": "^1.1.0", - "@aws-amplify/backend-secret": "^1.1.1", - "@aws-amplify/cli-core": "^1.1.2", - "@aws-amplify/client-config": "^1.1.3", - "@aws-amplify/deployed-backend-client": "^1.3.0", - "@aws-amplify/platform-core": "^1.0.6", - "@aws-sdk/client-cloudformation": "^3.624.0", + "@aws-amplify/backend-deployer": "^1.1.8", + "@aws-amplify/backend-secret": "^1.1.2", + "@aws-amplify/cli-core": "^1.2.0", + "@aws-amplify/client-config": "^1.5.1", + "@aws-amplify/deployed-backend-client": "^1.4.1", + "@aws-amplify/platform-core": "^1.2.1", + "@aws-amplify/plugin-types": "^1.5.0", "@aws-sdk/client-cloudwatch-logs": "^3.624.0", "@aws-sdk/client-lambda": "^3.624.0", "@aws-sdk/client-ssm": "^3.624.0", "@aws-sdk/credential-providers": "^3.624.0", "@aws-sdk/types": "^3.609.0", - "@aws-sdk/util-arn-parser": "^3.568.0", "@parcel/watcher": "^2.4.1", "debounce-promise": "^3.1.2", "glob": "^10.2.7", @@ -28851,14 +32366,15 @@ "@types/parse-gitignore": "^1.0.0" }, "peerDependencies": { - "aws-cdk": "^2.152.0" + "aws-cdk": "^2.158.0" } }, "packages/schema-generator": { - "version": "1.2.2", + "name": "@aws-amplify/schema-generator", + "version": "1.2.5", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-schema-generator": "^0.9.4", + "@aws-amplify/graphql-schema-generator": "^0.11.0", "@aws-amplify/platform-core": "^1.0.5" } } diff --git a/package.json b/package.json index ff4526d10a..56f0ad37f5 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@actions/github": "^6.0.0", "@aws-sdk/client-amplify": "^3.624.0", "@aws-sdk/client-cloudformation": "^3.624.0", + "@aws-sdk/client-cloudwatch-logs": "^3.624.0", "@aws-sdk/client-cognito-identity-provider": "^3.624.0", "@aws-sdk/client-dynamodb": "^3.624.0", "@aws-sdk/client-iam": "^3.624.0", @@ -64,6 +65,7 @@ "@aws-sdk/client-ssm": "^3.624.0", "@changesets/cli": "^2.26.1", "@changesets/get-release-plan": "^4.0.0", + "@changesets/types": "^6.0.0", "@microsoft/api-extractor": "7.43.8", "@octokit/webhooks-types": "^7.5.1", "@shopify/eslint-plugin": "^43.0.0", @@ -88,14 +90,14 @@ "fs-extra": "^11.1.1", "glob": "^10.1.0", "husky": "^8.0.3", - "lint-staged": "^13.2.1", + "lint-staged": "^15.2.10", "prettier": "^2.8.7", "rimraf": "^5.0.0", "semver": "^7.5.4", "tsx": "^4.6.1", "typedoc": "^0.25.3", "typescript": "~5.2.0", - "verdaccio": "^5.24.1" + "verdaccio": "^6.0.1" }, "workspaces": [ "packages/*" diff --git a/packages/ai-constructs/API.md b/packages/ai-constructs/API.md index 95ab27f8e1..a5621d37df 100644 --- a/packages/ai-constructs/API.md +++ b/packages/ai-constructs/API.md @@ -6,29 +6,32 @@ /// +import { AIConversationOutput } from '@aws-amplify/backend-output-schemas'; +import { BackendOutputStorageStrategy } from '@aws-amplify/plugin-types'; import * as bedrock from '@aws-sdk/client-bedrock-runtime'; import { Construct } from 'constructs'; import { FunctionResources } from '@aws-amplify/plugin-types'; +import * as jsonSchemaToTypeScript from 'json-schema-to-ts'; import { ResourceProvider } from '@aws-amplify/plugin-types'; -import * as smithy from '@smithy/types'; declare namespace __export__conversation { export { ConversationHandlerFunction, - ConversationHandlerFunctionProps + ConversationHandlerFunctionProps, + ConversationTurnEventVersion } } export { __export__conversation } declare namespace __export__conversation__runtime { export { - ConversationMessage, - ConversationMessageContentBlock, ConversationTurnEvent, + createExecutableTool, ExecutableTool, + FromJSONSchema, + JSONSchema, handleConversationTurnEvent, ToolDefinition, - ToolExecutionInput, ToolInputSchema, ToolResultContentBlock } @@ -39,6 +42,8 @@ export { __export__conversation__runtime } class ConversationHandlerFunction extends Construct implements ResourceProvider { constructor(scope: Construct, id: string, props: ConversationHandlerFunctionProps); // (undocumented) + static readonly eventVersion: ConversationTurnEventVersion; + // (undocumented) resources: FunctionResources; } @@ -49,27 +54,15 @@ type ConversationHandlerFunctionProps = { modelId: string; region?: string; }>; -}; - -// @public (undocumented) -type ConversationMessage = { - role: 'user' | 'assistant'; - content: Array; -}; - -// @public (undocumented) -type ConversationMessageContentBlock = bedrock.ContentBlock | { - image: Omit & { - source: { - bytes: string; - }; - }; + memoryMB?: number; + outputStorageStrategy?: BackendOutputStorageStrategy; }; // @public (undocumented) type ConversationTurnEvent = { conversationId: string; currentMessageId: string; + streamResponse?: boolean; responseMutation: { name: string; inputTypeName: string; @@ -87,11 +80,15 @@ type ConversationTurnEvent = { }; }; request: { - headers: { - authorization: string; - }; + headers: Record; + }; + messageHistoryQuery: { + getQueryName: string; + getQueryInputTypeName: string; + listQueryName: string; + listQueryInputTypeName: string; + listQueryLimit?: number; }; - messages: Array; toolsConfiguration?: { dataTools?: Array Promise; +type ConversationTurnEventVersion = `1.${number}`; + +// @public +const createExecutableTool: >(name: string, description: string, inputSchema: ToolInputSchema, handler: (input: TToolInput) => Promise) => ExecutableTool; + +// @public (undocumented) +type ExecutableTool> = ToolDefinition & { + execute: (input: TToolInput) => Promise; }; +// @public (undocumented) +type FromJSONSchema = jsonSchemaToTypeScript.FromSchema; + // @public const handleConversationTurnEvent: (event: ConversationTurnEvent, props?: { - tools?: Array; + tools?: Array>; }) => Promise; // @public (undocumented) -type ToolDefinition = { +type JSONSchema = jsonSchemaToTypeScript.JSONSchema; + +// @public (undocumented) +type ToolDefinition = { name: string; description: string; - inputSchema: ToolInputSchema; + inputSchema: ToolInputSchema; }; // @public (undocumented) -type ToolExecutionInput = smithy.DocumentType; - -// @public (undocumented) -type ToolInputSchema = bedrock.ToolInputSchema; +type ToolInputSchema = { + json: TJSONSchema; +}; // @public (undocumented) type ToolResultContentBlock = bedrock.ToolResultContentBlock; diff --git a/packages/ai-constructs/CHANGELOG.md b/packages/ai-constructs/CHANGELOG.md index fe7736ec0e..757855305c 100644 --- a/packages/ai-constructs/CHANGELOG.md +++ b/packages/ai-constructs/CHANGELOG.md @@ -1,5 +1,107 @@ # @aws-amplify/ai-constructs +## 1.0.0 + +### Major Changes + +- bbd6add: GA release of backend AI features + +### Patch Changes + +- fd8759d: Fix a case when Bedrock throws validation error if tool input is not persisted in history + +## 0.8.2 + +### Patch Changes + +- bc6dc69: Fix case where tool use does not have input while streaming + +## 0.8.1 + +### Patch Changes + +- 1af5060: Add metadata to user agent in conversation handler runtime. +- Updated dependencies [583a3f2] + - @aws-amplify/platform-core@1.2.0 + +## 0.8.0 + +### Minor Changes + +- 37dd87c: Propagate errors to AppSync + +### Patch Changes + +- 613bca9: Remove tool usage for non current turns when looking up message history +- b56d344: update aws-cdk lib to ^2.158.0 +- Updated dependencies [b56d344] + - @aws-amplify/plugin-types@1.3.1 + +## 0.7.0 + +### Minor Changes + +- 63fb254: Include accumulated turn content in chunk mutation + +## 0.6.2 + +### Patch Changes + +- bd4ff4d: Add memory setting to conversation handler +- Updated dependencies [5f46d8d] + - @aws-amplify/backend-output-schemas@1.4.0 + +## 0.6.1 + +### Patch Changes + +- 91e7f3c: Parse client side tool json elements + +## 0.6.0 + +### Minor Changes + +- b6761b0: Stream Bedrock responses + +## 0.5.0 + +### Minor Changes + +- 46a0e85: Remove deprecated messages field from event + +### Patch Changes + +- faacd1b: Fix case where bedrock content blocks would be populated with 'null' instead of 'undefined. + +## 0.4.0 + +### Minor Changes + +- 4781704: Add information about event version to conversation components +- 3a29d43: Pass user agent in conversation handler lambda + +### Patch Changes + +- 6e4a62f: Fix multi tool usage in single turn. + +## 0.3.0 + +### Minor Changes + +- 300a72d: Infer executable tool input type from input schema +- 0a5e51c: Stream conversation logs in sandbox + +### Patch Changes + +- Updated dependencies [0a5e51c] + - @aws-amplify/backend-output-schemas@1.3.0 + +## 0.2.0 + +### Minor Changes + +- d0a90b1: Use message history instead of event payload for conversational route + ## 0.1.4 ### Patch Changes diff --git a/packages/ai-constructs/package.json b/packages/ai-constructs/package.json index 693bcf24cd..73fdf04e27 100644 --- a/packages/ai-constructs/package.json +++ b/packages/ai-constructs/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/ai-constructs", - "version": "0.1.4", + "version": "1.0.0", "type": "commonjs", "publishConfig": { "access": "public" @@ -26,12 +26,19 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/plugin-types": "^1.0.1", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/platform-core": "^1.2.0", + "@aws-amplify/plugin-types": "^1.3.1", "@aws-sdk/client-bedrock-runtime": "^3.622.0", - "@smithy/types": "^3.3.0" + "@smithy/types": "^3.3.0", + "json-schema-to-ts": "^3.1.1" + }, + "devDependencies": { + "@aws-amplify/backend-output-storage": "^1.1.3", + "typescript": "^5.0.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0" } } diff --git a/packages/ai-constructs/src/conversation/conversation_handler_construct.test.ts b/packages/ai-constructs/src/conversation/conversation_handler_construct.test.ts index 07cfa8404a..b0130e711f 100644 --- a/packages/ai-constructs/src/conversation/conversation_handler_construct.test.ts +++ b/packages/ai-constructs/src/conversation/conversation_handler_construct.test.ts @@ -4,6 +4,7 @@ import { App, Stack } from 'aws-cdk-lib'; import { ConversationHandlerFunction } from './conversation_handler_construct'; import { Template } from 'aws-cdk-lib/assertions'; import path from 'path'; +import { StackMetadataBackendOutputStorageStrategy } from '@aws-amplify/backend-output-storage'; void describe('Conversation Handler Function construct', () => { void it('creates handler with log group with JWT token redacting policy', () => { @@ -81,7 +82,10 @@ void describe('Conversation Handler Function construct', () => { PolicyDocument: { Statement: [ { - Action: 'bedrock:InvokeModel', + Action: [ + 'bedrock:InvokeModel', + 'bedrock:InvokeModelWithResponseStream', + ], Effect: 'Allow', Resource: [ 'arn:aws:bedrock:region1::foundation-model/model1', @@ -114,7 +118,10 @@ void describe('Conversation Handler Function construct', () => { PolicyDocument: { Statement: [ { - Action: 'bedrock:InvokeModel', + Action: [ + 'bedrock:InvokeModel', + 'bedrock:InvokeModelWithResponseStream', + ], Effect: 'Allow', Resource: { 'Fn::Join': [ @@ -140,6 +147,56 @@ void describe('Conversation Handler Function construct', () => { }); }); + void it('does not store output if output strategy is absent', () => { + const app = new App(); + const stack = new Stack(app); + new ConversationHandlerFunction(stack, 'conversationHandler', { + models: [ + { + modelId: 'testModelId', + }, + ], + outputStorageStrategy: undefined, + }); + const template = Template.fromStack(stack); + const output = template.findOutputs( + 'definedConversationHandlers' + ).definedConversationHandlers; + assert.ok(!output); + }); + + void it('stores output if output strategy is present', () => { + const app = new App(); + const stack = new Stack(app); + new ConversationHandlerFunction(stack, 'conversationHandler', { + models: [ + { + modelId: 'testModelId', + }, + ], + outputStorageStrategy: new StackMetadataBackendOutputStorageStrategy( + stack + ), + }); + const template = Template.fromStack(stack); + const outputValue = template.findOutputs('definedConversationHandlers') + .definedConversationHandlers.Value; + assert.deepStrictEqual(outputValue, { + 'Fn::Join': [ + '', + [ + '["', + { + /* eslint-disable spellcheck/spell-checker */ + Ref: 'conversationHandlerconversationHandlerFunction45BC2E1F', + /* eslint-enable spellcheck/spell-checker */ + }, + '"]', + ], + ], + }); + }); + void it('throws if entry is not absolute', () => { const app = new App(); const stack = new Stack(app); @@ -165,4 +222,66 @@ void describe('Conversation Handler Function construct', () => { Handler: 'index.handler', }); }); + + void describe('memory property', () => { + void it('sets valid memory', () => { + const app = new App(); + const stack = new Stack(app); + new ConversationHandlerFunction(stack, 'conversationHandler', { + models: [], + memoryMB: 234, + }); + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::Lambda::Function', { + MemorySize: 234, + }); + }); + + void it('sets default memory', () => { + const app = new App(); + const stack = new Stack(app); + new ConversationHandlerFunction(stack, 'conversationHandler', { + models: [], + }); + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::Lambda::Function', { + MemorySize: 512, + }); + }); + + void it('throws on memory below 128 MB', () => { + assert.throws(() => { + const app = new App(); + const stack = new Stack(app); + new ConversationHandlerFunction(stack, 'conversationHandler', { + models: [], + memoryMB: 127, + }); + }, new Error('memoryMB must be a whole number between 128 and 10240 inclusive')); + }); + + void it('throws on memory above 10240 MB', () => { + assert.throws(() => { + const app = new App(); + const stack = new Stack(app); + new ConversationHandlerFunction(stack, 'conversationHandler', { + models: [], + memoryMB: 10241, + }); + }, new Error('memoryMB must be a whole number between 128 and 10240 inclusive')); + }); + + void it('throws on fractional memory', () => { + assert.throws(() => { + const app = new App(); + const stack = new Stack(app); + new ConversationHandlerFunction(stack, 'conversationHandler', { + models: [], + memoryMB: 256.2, + }); + }, new Error('memoryMB must be a whole number between 128 and 10240 inclusive')); + }); + }); }); diff --git a/packages/ai-constructs/src/conversation/conversation_handler_construct.ts b/packages/ai-constructs/src/conversation/conversation_handler_construct.ts index 57b33f779f..995b92fed6 100644 --- a/packages/ai-constructs/src/conversation/conversation_handler_construct.ts +++ b/packages/ai-constructs/src/conversation/conversation_handler_construct.ts @@ -1,7 +1,15 @@ -import { FunctionResources, ResourceProvider } from '@aws-amplify/plugin-types'; -import { Duration, Stack } from 'aws-cdk-lib'; +import { + BackendOutputStorageStrategy, + FunctionResources, + ResourceProvider, +} from '@aws-amplify/plugin-types'; +import { Duration, Stack, Tags } from 'aws-cdk-lib'; import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; -import { CfnFunction, Runtime as LambdaRuntime } from 'aws-cdk-lib/aws-lambda'; +import { + CfnFunction, + Runtime as LambdaRuntime, + LoggingFormat, +} from 'aws-cdk-lib/aws-lambda'; import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; import { CustomDataIdentifier, @@ -11,6 +19,11 @@ import { } from 'aws-cdk-lib/aws-logs'; import { Construct } from 'constructs'; import path from 'path'; +import { TagName } from '@aws-amplify/platform-core'; +import { + AIConversationOutput, + aiConversationOutputKey, +} from '@aws-amplify/backend-output-schemas'; const resourcesRoot = path.normalize(path.join(__dirname, 'runtime')); const defaultHandlerFilePath = path.join(resourcesRoot, 'default_handler.js'); @@ -21,8 +34,23 @@ export type ConversationHandlerFunctionProps = { modelId: string; region?: string; }>; + /** + * An amount of memory (RAM) to allocate to the function between 128 and 10240 MB. + * Must be a whole number. + * Default is 512MB. + */ + memoryMB?: number; + /** + * @internal + */ + outputStorageStrategy?: BackendOutputStorageStrategy; }; +// Event is a protocol between AppSync and Lambda handler. Therefore, X.Y subset of semver is enough. +// Typing this as 1.X so that major version changes are caught by compiler if consumer of this construct inspects +// event version. +export type ConversationTurnEventVersion = `1.${number}`; + /** * Conversation Handler Function CDK construct. * This construct deploys resources that integrate conversation routes @@ -37,6 +65,7 @@ export class ConversationHandlerFunction extends Construct implements ResourceProvider { + static readonly eventVersion: ConversationTurnEventVersion = '1.0'; resources: FunctionResources; /** @@ -53,6 +82,8 @@ export class ConversationHandlerFunction throw new Error('Entry must be absolute path'); } + Tags.of(this).add(TagName.FRIENDLY_NAME, id); + const conversationHandler = new NodejsFunction( this, `conversationHandlerFunction`, @@ -61,12 +92,14 @@ export class ConversationHandlerFunction timeout: Duration.seconds(60), entry: this.props.entry ?? defaultHandlerFilePath, handler: 'handler', + memorySize: this.resolveMemory(), bundling: { // Do not bundle SDK if conversation handler is using our default implementation which is // compatible with Lambda provided SDK. // For custom entry we do bundle SDK as we can't control version customer is coding against. bundleAwsSDK: !!this.props.entry, }, + loggingFormat: LoggingFormat.JSON, logGroup: new LogGroup(this, 'conversationHandlerFunctionLogGroup', { retention: RetentionDays.INFINITE, dataProtectionPolicy: new DataProtectionPolicy({ @@ -91,7 +124,10 @@ export class ConversationHandlerFunction conversationHandler.addToRolePolicy( new PolicyStatement({ effect: Effect.ALLOW, - actions: ['bedrock:InvokeModel'], + actions: [ + 'bedrock:InvokeModel', + 'bedrock:InvokeModelWithResponseStream', + ], resources, }) ); @@ -105,5 +141,46 @@ export class ConversationHandlerFunction ) as CfnFunction, }, }; + + this.storeOutput(this.props.outputStorageStrategy); } + + /** + * Append conversation handler to defined functions. + */ + private storeOutput = ( + outputStorageStrategy: + | BackendOutputStorageStrategy + | undefined + ): void => { + outputStorageStrategy?.appendToBackendOutputList(aiConversationOutputKey, { + version: '1', + payload: { + definedConversationHandlers: this.resources.lambda.functionName, + }, + }); + }; + + private resolveMemory = () => { + const memoryMin = 128; + const memoryMax = 10240; + const memoryDefault = 512; + if (this.props.memoryMB === undefined) { + return memoryDefault; + } + if ( + !isWholeNumberBetweenInclusive(this.props.memoryMB, memoryMin, memoryMax) + ) { + throw new Error( + `memoryMB must be a whole number between ${memoryMin} and ${memoryMax} inclusive` + ); + } + return this.props.memoryMB; + }; } + +const isWholeNumberBetweenInclusive = ( + test: number, + min: number, + max: number +) => min <= test && test <= max && test % 1 === 0; diff --git a/packages/ai-constructs/src/conversation/index.ts b/packages/ai-constructs/src/conversation/index.ts index 439dd4067c..fa1330a494 100644 --- a/packages/ai-constructs/src/conversation/index.ts +++ b/packages/ai-constructs/src/conversation/index.ts @@ -1,6 +1,11 @@ import { ConversationHandlerFunction, ConversationHandlerFunctionProps, + ConversationTurnEventVersion, } from './conversation_handler_construct.js'; -export { ConversationHandlerFunction, ConversationHandlerFunctionProps }; +export { + ConversationHandlerFunction, + ConversationHandlerFunctionProps, + ConversationTurnEventVersion, +}; diff --git a/packages/ai-constructs/src/conversation/runtime/bedrock_converse_adapter.test.ts b/packages/ai-constructs/src/conversation/runtime/bedrock_converse_adapter.test.ts index 12c08d5d0d..b3a99ed1a0 100644 --- a/packages/ai-constructs/src/conversation/runtime/bedrock_converse_adapter.test.ts +++ b/packages/ai-constructs/src/conversation/runtime/bedrock_converse_adapter.test.ts @@ -1,34 +1,43 @@ import { describe, it, mock } from 'node:test'; import assert from 'node:assert'; -import { ConversationTurnEvent, ExecutableTool, ToolDefinition } from './types'; +import { + ConversationMessage, + ConversationTurnEvent, + ExecutableTool, + StreamingResponseChunk, + ToolDefinition, +} from './types'; import { BedrockConverseAdapter } from './bedrock_converse_adapter'; import { BedrockRuntimeClient, + ContentBlock, ConverseCommand, ConverseCommandInput, ConverseCommandOutput, + ConverseStreamCommandOutput, + ConverseStreamOutput, Message, + StopReason, ToolConfiguration, + ToolInputSchema, ToolResultContentBlock, } from '@aws-sdk/client-bedrock-runtime'; import { ConversationTurnEventToolsProvider } from './event-tools-provider'; import { randomBytes, randomUUID } from 'node:crypto'; +import { ConversationMessageHistoryRetriever } from './conversation_message_history_retriever'; +import { UserAgentProvider } from './user_agent_provider'; void describe('Bedrock converse adapter', () => { const commonEvent: Readonly = { - conversationId: '', - currentMessageId: '', + conversationId: 'testConversationId', + currentMessageId: 'testCurrentMessageId', graphqlApiEndpoint: '', - messages: [ - { - role: 'user', - content: [ - { - text: 'event message', - }, - ], - }, - ], + messageHistoryQuery: { + getQueryName: '', + getQueryInputTypeName: '', + listQueryName: '', + listQueryInputTypeName: '', + }, modelConfiguration: { modelId: 'testModelId', systemPrompt: 'testSystemPrompt', @@ -46,273 +55,854 @@ void describe('Bedrock converse adapter', () => { }, }; - void it('calls bedrock to get conversation response', async () => { - const event: ConversationTurnEvent = { - ...commonEvent, - }; + const messages: Array = [ + { + role: 'user', + content: [ + { + text: 'event message', + }, + ], + }, + ]; + const messageHistoryRetriever = new ConversationMessageHistoryRetriever( + commonEvent + ); + const messageHistoryRetrieverMockGetEventMessages = mock.method( + messageHistoryRetriever, + 'getMessageHistory', + () => { + return Promise.resolve(messages); + } + ); - const bedrockClient = new BedrockRuntimeClient(); - const bedrockResponse: ConverseCommandOutput = { - $metadata: {}, - metrics: undefined, - output: { - message: { - role: 'assistant', - content: [ + [false, true].forEach((streamResponse) => { + // This is a common set of use cases that both streaming and non-streaming version must support. + void describe(`${streamResponse ? 'with' : 'without'} streaming`, () => { + void it('calls bedrock to get conversation response', async () => { + const event: ConversationTurnEvent = { + ...commonEvent, + }; + + const bedrockClient = new BedrockRuntimeClient(); + const content = [{ text: 'block1' }, { text: 'block2' }]; + const bedrockResponse = mockBedrockResponse(content, streamResponse); + const bedrockClientSendMock = mock.method(bedrockClient, 'send', () => + Promise.resolve(bedrockResponse) + ); + + const adapter = new BedrockConverseAdapter( + event, + [], + bedrockClient, + undefined, + messageHistoryRetriever + ); + + if (streamResponse) { + const chunks: Array = + await askBedrockWithStreaming(adapter); + // Assertion below is verbose on purpose to assert that correct indexes are rendered. + // See mockConverseStreamCommandOutput below of how split chunks are mocked. + assert.deepStrictEqual(chunks, [ { - text: 'block1', + accumulatedTurnContent: [ + { + text: 'b', + }, + ], + conversationId: event.conversationId, + associatedUserMessageId: event.currentMessageId, + contentBlockText: 'b', + contentBlockIndex: 0, + contentBlockDeltaIndex: 0, + }, + { + accumulatedTurnContent: [ + { + text: 'block1', + }, + ], + conversationId: event.conversationId, + associatedUserMessageId: event.currentMessageId, + contentBlockText: 'lock1', + contentBlockIndex: 0, + contentBlockDeltaIndex: 1, + }, + { + accumulatedTurnContent: [ + { + text: 'block1', + }, + ], + conversationId: event.conversationId, + associatedUserMessageId: event.currentMessageId, + contentBlockIndex: 0, + contentBlockDoneAtIndex: 1, + }, + { + accumulatedTurnContent: [ + { + text: 'block1', + }, + { + text: 'b', + }, + ], + conversationId: event.conversationId, + associatedUserMessageId: event.currentMessageId, + contentBlockText: 'b', + contentBlockIndex: 1, + contentBlockDeltaIndex: 0, + }, + { + accumulatedTurnContent: [ + { + text: 'block1', + }, + { + text: 'block2', + }, + ], + conversationId: event.conversationId, + associatedUserMessageId: event.currentMessageId, + contentBlockText: 'lock2', + contentBlockIndex: 1, + contentBlockDeltaIndex: 1, + }, + { + accumulatedTurnContent: [ + { + text: 'block1', + }, + { + text: 'block2', + }, + ], + conversationId: event.conversationId, + associatedUserMessageId: event.currentMessageId, + contentBlockIndex: 1, + contentBlockDoneAtIndex: 1, + }, + { + accumulatedTurnContent: [ + { + text: 'block1', + }, + { + text: 'block2', + }, + ], + conversationId: event.conversationId, + associatedUserMessageId: event.currentMessageId, + contentBlockIndex: 1, + stopReason: 'end_turn', }, + ]); + } else { + const responseContent = await adapter.askBedrock(); + assert.deepStrictEqual(responseContent, content); + } + + assert.strictEqual(bedrockClientSendMock.mock.calls.length, 1); + const bedrockRequest = bedrockClientSendMock.mock.calls[0] + .arguments[0] as unknown as ConverseCommand; + const expectedBedrockInput: ConverseCommandInput = { + messages: messages as Array, + modelId: event.modelConfiguration.modelId, + inferenceConfig: event.modelConfiguration.inferenceConfiguration, + system: [ { - text: 'block2', + text: event.modelConfiguration.systemPrompt, }, ], - }, - }, - stopReason: 'end_turn', - usage: undefined, - }; - const bedrockClientSendMock = mock.method(bedrockClient, 'send', () => - Promise.resolve(bedrockResponse) - ); + toolConfig: undefined, + }; + assert.deepStrictEqual(bedrockRequest.input, expectedBedrockInput); + }); - const responseContent = await new BedrockConverseAdapter( - event, - [], - bedrockClient - ).askBedrock(); + void it('uses executable tools while calling bedrock', async () => { + const additionalToolOutput: ToolResultContentBlock = { + text: 'additionalToolOutput', + }; + const additionalTool: ExecutableTool = { + name: 'additionalTool', + description: 'additional tool description', + inputSchema: { + json: { + required: ['additionalToolRequiredProperty'], + }, + }, + execute: () => Promise.resolve(additionalToolOutput), + }; + const eventToolOutput: ToolResultContentBlock = { + text: 'eventToolOutput', + }; + const eventTool: ExecutableTool = { + name: 'eventTool', + description: 'event tool description', + inputSchema: { + json: { + required: ['eventToolRequiredProperty'], + }, + }, + execute: () => Promise.resolve(eventToolOutput), + }; - assert.deepStrictEqual( - responseContent, - bedrockResponse.output?.message?.content - ); + const event: ConversationTurnEvent = { + ...commonEvent, + }; - assert.strictEqual(bedrockClientSendMock.mock.calls.length, 1); - const bedrockRequest = bedrockClientSendMock.mock.calls[0] - .arguments[0] as unknown as ConverseCommand; - const expectedBedrockInput: ConverseCommandInput = { - messages: event.messages as Array, - modelId: event.modelConfiguration.modelId, - inferenceConfig: event.modelConfiguration.inferenceConfiguration, - system: [ - { - text: event.modelConfiguration.systemPrompt, - }, - ], - toolConfig: undefined, - }; - assert.deepStrictEqual(bedrockRequest.input, expectedBedrockInput); - }); + const bedrockClient = new BedrockRuntimeClient(); + const bedrockResponseQueue: Array< + ConverseCommandOutput | ConverseStreamCommandOutput + > = []; + const additionalToolUse1 = { + toolUseId: randomUUID().toString(), + name: additionalTool.name, + input: 'additionalToolInput1', + }; + const additionalToolUse2 = { + toolUseId: randomUUID().toString(), + name: additionalTool.name, + input: 'additionalToolInput2', + }; + const additionalToolUseBedrockResponse = mockBedrockResponse( + [ + { + toolUse: additionalToolUse1, + }, + { + toolUse: additionalToolUse2, + }, + ], + streamResponse + ); + bedrockResponseQueue.push(additionalToolUseBedrockResponse); + const eventToolUse1 = { + toolUseId: randomUUID().toString(), + name: eventTool.name, + input: 'eventToolInput1', + }; + const eventToolUse2 = { + toolUseId: randomUUID().toString(), + name: eventTool.name, + input: 'eventToolInput2', + }; + const eventToolUseBedrockResponse = mockBedrockResponse( + [ + { + toolUse: eventToolUse1, + }, + { + toolUse: eventToolUse2, + }, + ], + streamResponse + ); + bedrockResponseQueue.push(eventToolUseBedrockResponse); + const content = [ + { + text: 'finalResponse', + }, + ]; + const finalBedrockResponse = mockBedrockResponse( + content, + streamResponse + ); + bedrockResponseQueue.push(finalBedrockResponse); - void it('uses executable tools while calling bedrock', async () => { - const additionalToolOutput: ToolResultContentBlock = { - text: 'additionalToolOutput', - }; - const additionalTool: ExecutableTool = { - name: 'additionalTool', - description: 'additional tool description', - inputSchema: { - json: { - required: ['additionalToolRequiredProperty'], - }, - }, - execute: () => Promise.resolve(additionalToolOutput), - }; - const eventToolOutput: ToolResultContentBlock = { - text: 'eventToolOutput', - }; - const eventTool: ExecutableTool = { - name: 'eventTool', - description: 'event tool description', - inputSchema: { - json: { - required: ['eventToolRequiredProperty'], - }, - }, - execute: () => Promise.resolve(eventToolOutput), - }; + const bedrockClientSendMock = mock.method(bedrockClient, 'send', () => + Promise.resolve(bedrockResponseQueue.shift()) + ); - const event: ConversationTurnEvent = { - ...commonEvent, - }; + const eventToolsProvider = new ConversationTurnEventToolsProvider( + event + ); + mock.method(eventToolsProvider, 'getEventTools', () => [eventTool]); - const bedrockClient = new BedrockRuntimeClient(); - const bedrockResponseQueue: Array = []; - const additionalToolUseBedrockResponse: ConverseCommandOutput = { - $metadata: {}, - metrics: undefined, - output: { - message: { - role: 'assistant', - content: [ + const adapter = new BedrockConverseAdapter( + event, + [additionalTool], + bedrockClient, + eventToolsProvider, + messageHistoryRetriever + ); + if (streamResponse) { + const chunks: Array = + await askBedrockWithStreaming(adapter); + const responseText = chunks.reduce((acc, next) => { + if (next.contentBlockText) { + acc += next.contentBlockText; + } + return acc; + }, ''); + assert.strictEqual(responseText, 'finalResponse'); + } else { + const responseContent = await adapter.askBedrock(); + assert.deepStrictEqual(responseContent, content); + } + + assert.strictEqual(bedrockClientSendMock.mock.calls.length, 3); + const expectedToolConfig: ToolConfiguration = { + tools: [ { - toolUse: { - toolUseId: randomUUID().toString(), + toolSpec: { + name: eventTool.name, + description: eventTool.description, + inputSchema: eventTool.inputSchema as ToolInputSchema, + }, + }, + { + toolSpec: { name: additionalTool.name, - input: 'additionalToolInput', + description: additionalTool.description, + inputSchema: additionalTool.inputSchema as ToolInputSchema, }, }, ], - }, - }, - stopReason: 'tool_use', - usage: undefined, - }; - bedrockResponseQueue.push(additionalToolUseBedrockResponse); - const eventToolUseBedrockResponse: ConverseCommandOutput = { - $metadata: {}, - metrics: undefined, - output: { - message: { - role: 'assistant', - content: [ + }; + const expectedBedrockInputCommonProperties = { + modelId: event.modelConfiguration.modelId, + inferenceConfig: event.modelConfiguration.inferenceConfiguration, + system: [ { - toolUse: { - toolUseId: randomUUID().toString(), - name: eventTool.name, - input: 'eventToolToolInput', - }, + text: event.modelConfiguration.systemPrompt, }, ], - }, - }, - stopReason: 'tool_use', - usage: undefined, - }; - bedrockResponseQueue.push(eventToolUseBedrockResponse); - const finalBedrockResponse: ConverseCommandOutput = { - $metadata: {}, - metrics: undefined, - output: { - message: { - role: 'assistant', - content: [ + toolConfig: expectedToolConfig, + }; + const bedrockRequest1 = bedrockClientSendMock.mock.calls[0] + .arguments[0] as unknown as ConverseCommand; + const expectedBedrockInput1: ConverseCommandInput = { + messages: messages as Array, + ...expectedBedrockInputCommonProperties, + }; + assert.deepStrictEqual(bedrockRequest1.input, expectedBedrockInput1); + const bedrockRequest2 = bedrockClientSendMock.mock.calls[1] + .arguments[0] as unknown as ConverseCommand; + const expectedBedrockInput2: ConverseCommandInput = { + messages: [ + ...(messages as Array), { - text: 'block1', + role: 'assistant', + content: [ + { toolUse: additionalToolUse1 }, + { toolUse: additionalToolUse2 }, + ], }, { - text: 'block2', + role: 'user', + content: [ + { + toolResult: { + content: [additionalToolOutput], + status: 'success', + toolUseId: additionalToolUse1.toolUseId, + }, + }, + { + toolResult: { + content: [additionalToolOutput], + status: 'success', + toolUseId: additionalToolUse2.toolUseId, + }, + }, + ], }, ], - }, - }, - stopReason: 'end_turn', - usage: undefined, - }; - bedrockResponseQueue.push(finalBedrockResponse); + ...expectedBedrockInputCommonProperties, + }; + assert.deepStrictEqual(bedrockRequest2.input, expectedBedrockInput2); + const bedrockRequest3 = bedrockClientSendMock.mock.calls[2] + .arguments[0] as unknown as ConverseCommand; + assert.ok(expectedBedrockInput2.messages); + const expectedBedrockInput3: ConverseCommandInput = { + messages: [ + ...expectedBedrockInput2.messages, + { + role: 'assistant', + content: [{ toolUse: eventToolUse1 }, { toolUse: eventToolUse2 }], + }, + { + role: 'user', + content: [ + { + toolResult: { + content: [eventToolOutput], + status: 'success', + toolUseId: eventToolUse1.toolUseId, + }, + }, + { + toolResult: { + content: [eventToolOutput], + status: 'success', + toolUseId: eventToolUse2.toolUseId, + }, + }, + ], + }, + ], + ...expectedBedrockInputCommonProperties, + }; + assert.deepStrictEqual(bedrockRequest3.input, expectedBedrockInput3); + }); - const bedrockClientSendMock = mock.method(bedrockClient, 'send', () => - Promise.resolve(bedrockResponseQueue.shift()) - ); + void it('executable tool error is reported to bedrock', async () => { + const tool: ExecutableTool = { + name: 'testTool', + description: 'tool description', + inputSchema: { + json: {}, + }, + execute: () => Promise.reject(new Error('Test tool error')), + }; - const eventToolsProvider = new ConversationTurnEventToolsProvider(event); - mock.method(eventToolsProvider, 'getEventTools', () => [eventTool]); + const event: ConversationTurnEvent = { + ...commonEvent, + }; - const responseContent = await new BedrockConverseAdapter( - event, - [additionalTool], - bedrockClient, - eventToolsProvider - ).askBedrock(); + const bedrockClient = new BedrockRuntimeClient(); + const bedrockResponseQueue: Array< + ConverseCommandOutput | ConverseStreamCommandOutput + > = []; + const toolUse = { + toolUseId: randomUUID().toString(), + name: tool.name, + input: 'testTool', + }; + const toolUseBedrockResponse = mockBedrockResponse( + [ + { + toolUse, + }, + ], + streamResponse + ); + bedrockResponseQueue.push(toolUseBedrockResponse); + const content = [{ text: 'finalResponse' }]; + const finalBedrockResponse = mockBedrockResponse( + content, + streamResponse + ); + bedrockResponseQueue.push(finalBedrockResponse); - assert.deepStrictEqual( - responseContent, - finalBedrockResponse.output?.message?.content - ); + const bedrockClientSendMock = mock.method(bedrockClient, 'send', () => + Promise.resolve(bedrockResponseQueue.shift()) + ); - assert.strictEqual(bedrockClientSendMock.mock.calls.length, 3); - const expectedToolConfig: ToolConfiguration = { - tools: [ - { - toolSpec: { - name: eventTool.name, - description: eventTool.description, - inputSchema: eventTool.inputSchema, - }, - }, - { - toolSpec: { - name: additionalTool.name, - description: additionalTool.description, - inputSchema: additionalTool.inputSchema, - }, - }, - ], - }; - const expectedBedrockInputCommonProperties = { - modelId: event.modelConfiguration.modelId, - inferenceConfig: event.modelConfiguration.inferenceConfiguration, - system: [ - { - text: event.modelConfiguration.systemPrompt, - }, - ], - toolConfig: expectedToolConfig, - }; - const bedrockRequest1 = bedrockClientSendMock.mock.calls[0] - .arguments[0] as unknown as ConverseCommand; - const expectedBedrockInput1: ConverseCommandInput = { - messages: event.messages as Array, - ...expectedBedrockInputCommonProperties, - }; - assert.deepStrictEqual(bedrockRequest1.input, expectedBedrockInput1); - const bedrockRequest2 = bedrockClientSendMock.mock.calls[1] - .arguments[0] as unknown as ConverseCommand; - assert.ok(additionalToolUseBedrockResponse.output?.message?.content); - assert.ok( - additionalToolUseBedrockResponse.output?.message?.content[0].toolUse - ?.toolUseId - ); - const expectedBedrockInput2: ConverseCommandInput = { - messages: [ - ...(event.messages as Array), - additionalToolUseBedrockResponse.output?.message, - { + const adapter = new BedrockConverseAdapter( + event, + [tool], + bedrockClient, + undefined, + messageHistoryRetriever + ); + if (streamResponse) { + const chunks: Array = + await askBedrockWithStreaming(adapter); + const responseText = chunks.reduce((acc, next) => { + if (next.contentBlockText) { + acc += next.contentBlockText; + } + return acc; + }, ''); + assert.strictEqual(responseText, 'finalResponse'); + } else { + const responseContent = await adapter.askBedrock(); + assert.deepStrictEqual(responseContent, content); + } + + assert.strictEqual(bedrockClientSendMock.mock.calls.length, 2); + const bedrockRequest2 = bedrockClientSendMock.mock.calls[1] + .arguments[0] as unknown as ConverseCommand; + assert.deepStrictEqual(bedrockRequest2.input.messages?.pop(), { role: 'user', content: [ { toolResult: { - content: [additionalToolOutput], - status: 'success', - toolUseId: - additionalToolUseBedrockResponse.output?.message.content[0] - .toolUse.toolUseId, + content: [ + { + text: 'Error: Test tool error', + }, + ], + status: 'error', + toolUseId: toolUse.toolUseId, }, }, ], - }, - ], - ...expectedBedrockInputCommonProperties, - }; - assert.deepStrictEqual(bedrockRequest2.input, expectedBedrockInput2); - const bedrockRequest3 = bedrockClientSendMock.mock.calls[2] - .arguments[0] as unknown as ConverseCommand; - assert.ok(eventToolUseBedrockResponse.output?.message?.content); - assert.ok( - eventToolUseBedrockResponse.output?.message?.content[0].toolUse?.toolUseId - ); - assert.ok(expectedBedrockInput2.messages); - const expectedBedrockInput3: ConverseCommandInput = { - messages: [ - ...expectedBedrockInput2.messages, - eventToolUseBedrockResponse.output?.message, - { + } as Message); + }); + + void it('executable tool error of unknown type is reported to bedrock', async () => { + const tool: ExecutableTool = { + name: 'testTool', + description: 'tool description', + inputSchema: { + json: {}, + }, + // This is intentional to cover logical branch that test for error type. + // eslint-disable-next-line prefer-promise-reject-errors + execute: () => Promise.reject('Test tool error'), + }; + + const event: ConversationTurnEvent = { + ...commonEvent, + }; + + const bedrockClient = new BedrockRuntimeClient(); + const bedrockResponseQueue: Array< + ConverseCommandOutput | ConverseStreamCommandOutput + > = []; + const toolUse = { + toolUseId: randomUUID().toString(), + name: tool.name, + input: 'testTool', + }; + const toolUseBedrockResponse = mockBedrockResponse( + [ + { + toolUse, + }, + ], + streamResponse + ); + bedrockResponseQueue.push(toolUseBedrockResponse); + const content = [{ text: 'finalResponse' }]; + const finalBedrockResponse = mockBedrockResponse( + content, + streamResponse + ); + bedrockResponseQueue.push(finalBedrockResponse); + + const bedrockClientSendMock = mock.method(bedrockClient, 'send', () => + Promise.resolve(bedrockResponseQueue.shift()) + ); + + const adapter = new BedrockConverseAdapter( + event, + [tool], + bedrockClient, + undefined, + messageHistoryRetriever + ); + if (streamResponse) { + const chunks: Array = + await askBedrockWithStreaming(adapter); + const responseText = chunks.reduce((acc, next) => { + if (next.contentBlockText) { + acc += next.contentBlockText; + } + return acc; + }, ''); + assert.strictEqual(responseText, 'finalResponse'); + } else { + const responseContent = await adapter.askBedrock(); + assert.deepStrictEqual(responseContent, content); + } + + assert.strictEqual(bedrockClientSendMock.mock.calls.length, 2); + const bedrockRequest2 = bedrockClientSendMock.mock.calls[1] + .arguments[0] as unknown as ConverseCommand; + assert.deepStrictEqual(bedrockRequest2.input.messages?.pop(), { role: 'user', content: [ { toolResult: { - content: [eventToolOutput], - status: 'success', - toolUseId: - eventToolUseBedrockResponse.output?.message.content[0].toolUse - .toolUseId, + content: [ + { + text: 'unknown error occurred', + }, + ], + status: 'error', + toolUseId: toolUse.toolUseId, + }, + }, + ], + } as Message); + }); + + void it('returns client tool input block when client tool is requested and ignores executable tools', async () => { + const additionalToolOutput: ToolResultContentBlock = { + text: 'additionalToolOutput', + }; + const additionalTool: ExecutableTool = { + name: 'additionalTool', + description: 'additional tool description', + inputSchema: { + json: { + required: ['additionalToolRequiredProperty'], + }, + }, + execute: () => Promise.resolve(additionalToolOutput), + }; + const clientTool: ToolDefinition = { + name: 'clientTool', + description: 'client tool description', + inputSchema: { + json: { + required: ['clientToolRequiredProperty'], + }, + }, + }; + + const event: ConversationTurnEvent = { + ...commonEvent, + toolsConfiguration: { + clientTools: [clientTool], + }, + }; + + const bedrockClient = new BedrockRuntimeClient(); + const bedrockResponseQueue: Array< + ConverseCommandOutput | ConverseStreamCommandOutput + > = []; + const additionalToolUse = { + toolUseId: randomUUID().toString(), + name: additionalTool.name, + input: 'additionalToolInput', + }; + const clientToolUse = { + toolUseId: randomUUID().toString(), + name: clientTool.name, + input: 'clientToolInput', + }; + const toolUseBedrockResponse = mockBedrockResponse( + [ + { + toolUse: additionalToolUse, + }, + { toolUse: clientToolUse }, + ], + streamResponse + ); + bedrockResponseQueue.push(toolUseBedrockResponse); + + const bedrockClientSendMock = mock.method(bedrockClient, 'send', () => + Promise.resolve(bedrockResponseQueue.shift()) + ); + + const adapter = new BedrockConverseAdapter( + event, + [additionalTool], + bedrockClient, + undefined, + messageHistoryRetriever + ); + + if (streamResponse) { + const chunks: Array = + await askBedrockWithStreaming(adapter); + assert.deepStrictEqual(chunks, [ + { + accumulatedTurnContent: [{ toolUse: clientToolUse }], + conversationId: event.conversationId, + associatedUserMessageId: event.currentMessageId, + contentBlockIndex: 0, + contentBlockToolUse: JSON.stringify({ toolUse: clientToolUse }), + }, + { + accumulatedTurnContent: [{ toolUse: clientToolUse }], + conversationId: event.conversationId, + associatedUserMessageId: event.currentMessageId, + contentBlockIndex: 0, + stopReason: 'tool_use', + }, + ]); + } else { + const responseContent = await adapter.askBedrock(); + assert.deepStrictEqual(responseContent, [ + { + toolUse: clientToolUse, + }, + ]); + } + + assert.strictEqual(bedrockClientSendMock.mock.calls.length, 1); + const expectedToolConfig: ToolConfiguration = { + tools: [ + { + toolSpec: { + name: additionalTool.name, + description: additionalTool.description, + inputSchema: additionalTool.inputSchema as ToolInputSchema, + }, + }, + { + toolSpec: { + name: clientTool.name, + description: clientTool.description, + inputSchema: clientTool.inputSchema as ToolInputSchema, }, }, ], + }; + const expectedBedrockInputCommonProperties = { + modelId: event.modelConfiguration.modelId, + inferenceConfig: event.modelConfiguration.inferenceConfiguration, + system: [ + { + text: event.modelConfiguration.systemPrompt, + }, + ], + toolConfig: expectedToolConfig, + }; + const bedrockRequest = bedrockClientSendMock.mock.calls[0] + .arguments[0] as unknown as ConverseCommand; + const expectedBedrockInput: ConverseCommandInput = { + messages: messages as Array, + ...expectedBedrockInputCommonProperties, + }; + assert.deepStrictEqual(bedrockRequest.input, expectedBedrockInput); + }); + + void it('decodes base64 encoded images', async () => { + const event: ConversationTurnEvent = { + ...commonEvent, + }; + + const fakeImagePayload = randomBytes(32); + + messageHistoryRetrieverMockGetEventMessages.mock.mockImplementationOnce( + () => { + return Promise.resolve([ + { + id: '', + conversationId: '', + role: 'user', + content: [ + { + image: { + format: 'png', + source: { + bytes: fakeImagePayload.toString('base64'), + }, + }, + }, + ], + }, + ]); + } + ); + + const bedrockClient = new BedrockRuntimeClient(); + const content = [{ text: 'block1' }, { text: 'block2' }]; + const bedrockResponse = mockBedrockResponse(content, streamResponse); + const bedrockClientSendMock = mock.method(bedrockClient, 'send', () => + Promise.resolve(bedrockResponse) + ); + + await new BedrockConverseAdapter( + event, + [], + bedrockClient, + undefined, + messageHistoryRetriever + ).askBedrock(); + + assert.strictEqual(bedrockClientSendMock.mock.calls.length, 1); + const bedrockRequest = bedrockClientSendMock.mock.calls[0] + .arguments[0] as unknown as ConverseCommand; + assert.deepStrictEqual(bedrockRequest.input.messages, [ + { + role: 'user', + content: [ + { + image: { + format: 'png', + source: { + bytes: fakeImagePayload, + }, + }, + }, + ], + }, + ]); + }); + }); + }); + + void it('handles tool use with empty input when streaming', async () => { + const toolOutput: ToolResultContentBlock = { + text: 'additionalToolOutput', + }; + const toolExecuteMock = mock.fn< + (input: unknown) => Promise + >(() => Promise.resolve(toolOutput)); + const tool: ExecutableTool = { + name: 'toolId', + description: 'tool description', + inputSchema: { + json: {}, + }, + execute: toolExecuteMock, + }; + + const event: ConversationTurnEvent = { + ...commonEvent, + }; + + const bedrockClient = new BedrockRuntimeClient(); + const bedrockResponseQueue: Array< + ConverseCommandOutput | ConverseStreamCommandOutput + > = []; + const toolUse1 = { + toolUseId: randomUUID().toString(), + name: tool.name, + input: undefined, + }; + const toolUse2 = { + toolUseId: randomUUID().toString(), + name: tool.name, + input: '', + }; + const toolUseBedrockResponse = mockBedrockResponse( + [ + { + toolUse: toolUse1, + }, + { + toolUse: toolUse2, }, ], - ...expectedBedrockInputCommonProperties, - }; - assert.deepStrictEqual(bedrockRequest3.input, expectedBedrockInput3); + true + ); + bedrockResponseQueue.push(toolUseBedrockResponse); + const content = [ + { + text: 'finalResponse', + }, + ]; + const finalBedrockResponse = mockBedrockResponse(content, true); + bedrockResponseQueue.push(finalBedrockResponse); + + mock.method(bedrockClient, 'send', () => + Promise.resolve(bedrockResponseQueue.shift()) + ); + + const adapter = new BedrockConverseAdapter( + event, + [tool], + bedrockClient, + undefined, + messageHistoryRetriever + ); + + const chunks: Array = await askBedrockWithStreaming( + adapter + ); + const responseText = chunks.reduce((acc, next) => { + if (next.contentBlockText) { + acc += next.contentBlockText; + } + return acc; + }, ''); + assert.strictEqual(responseText, 'finalResponse'); + + assert.strictEqual(toolExecuteMock.mock.calls.length, 2); + assert.deepStrictEqual(toolExecuteMock.mock.calls[0].arguments[0], {}); + assert.deepStrictEqual(toolExecuteMock.mock.calls[1].arguments[0], {}); }); void it('throws if tool is duplicated', () => { @@ -385,375 +975,201 @@ void describe('Bedrock converse adapter', () => { ); }); - void it('executable tool error is reported to bedrock', async () => { - const tool: ExecutableTool = { - name: 'testTool', - description: 'tool description', - inputSchema: { - json: {}, - }, - execute: () => Promise.reject(new Error('Test tool error')), - }; - + void it('adds user agent middleware', async () => { const event: ConversationTurnEvent = { ...commonEvent, }; const bedrockClient = new BedrockRuntimeClient(); - const bedrockResponseQueue: Array = []; - const toolUseBedrockResponse: ConverseCommandOutput = { - $metadata: {}, - metrics: undefined, - output: { - message: { - role: 'assistant', - content: [ - { - toolUse: { - toolUseId: randomUUID().toString(), - name: tool.name, - input: 'testTool', - }, - }, - ], - }, - }, - stopReason: 'tool_use', - usage: undefined, - }; - bedrockResponseQueue.push(toolUseBedrockResponse); - const finalBedrockResponse: ConverseCommandOutput = { - $metadata: {}, - metrics: undefined, - output: { - message: { - role: 'assistant', - content: [ - { - text: 'finalResponse', - }, - ], - }, - }, - stopReason: 'end_turn', - usage: undefined, - }; - bedrockResponseQueue.push(finalBedrockResponse); - - const bedrockClientSendMock = mock.method(bedrockClient, 'send', () => - Promise.resolve(bedrockResponseQueue.shift()) + const addMiddlewareMock = mock.method(bedrockClient.middlewareStack, 'add'); + const userAgentProvider = new UserAgentProvider( + {} as unknown as ConversationTurnEvent ); + mock.method(userAgentProvider, 'getUserAgent', () => 'testUserAgent'); - const responseContent = await new BedrockConverseAdapter( + new BedrockConverseAdapter( event, - [tool], - bedrockClient - ).askBedrock(); - - assert.deepStrictEqual( - responseContent, - finalBedrockResponse.output?.message?.content + [], + bedrockClient, + undefined, + messageHistoryRetriever, + userAgentProvider ); - assert.strictEqual(bedrockClientSendMock.mock.calls.length, 2); - const bedrockRequest2 = bedrockClientSendMock.mock.calls[1] - .arguments[0] as unknown as ConverseCommand; - assert.ok(toolUseBedrockResponse.output?.message?.content); - assert.deepStrictEqual(bedrockRequest2.input.messages?.pop(), { - role: 'user', - content: [ - { - toolResult: { - content: [ - { - text: 'Error: Test tool error', - }, - ], - status: 'error', - toolUseId: - toolUseBedrockResponse.output?.message.content[0].toolUse - ?.toolUseId, - }, - }, - ], - } as Message); - }); - - void it('executable tool error of unknown type is reported to bedrock', async () => { - const tool: ExecutableTool = { - name: 'testTool', - description: 'tool description', - inputSchema: { - json: {}, - }, - // This is intentional to cover logical branch that test for error type. - // eslint-disable-next-line prefer-promise-reject-errors - execute: () => Promise.reject('Test tool error'), - }; - - const event: ConversationTurnEvent = { - ...commonEvent, - }; - - const bedrockClient = new BedrockRuntimeClient(); - const bedrockResponseQueue: Array = []; - const toolUseBedrockResponse: ConverseCommandOutput = { - $metadata: {}, - metrics: undefined, - output: { - message: { - role: 'assistant', - content: [ - { - toolUse: { - toolUseId: randomUUID().toString(), - name: tool.name, - input: 'testTool', - }, - }, - ], - }, + assert.strictEqual(addMiddlewareMock.mock.calls.length, 1); + const middlewareHandler = addMiddlewareMock.mock.calls[0].arguments[0]; + const options = addMiddlewareMock.mock.calls[0].arguments[1]; + assert.strictEqual(options.name, 'amplify-user-agent-injector'); + const args: { + request: { + headers: Record; + }; + } = { + request: { + headers: {}, }, - stopReason: 'tool_use', - usage: undefined, }; - bedrockResponseQueue.push(toolUseBedrockResponse); - const finalBedrockResponse: ConverseCommandOutput = { - $metadata: {}, - metrics: undefined, - output: { - message: { - role: 'assistant', - content: [ - { - text: 'finalResponse', - }, - ], - }, - }, - stopReason: 'end_turn', - usage: undefined, - }; - bedrockResponseQueue.push(finalBedrockResponse); - - const bedrockClientSendMock = mock.method(bedrockClient, 'send', () => - Promise.resolve(bedrockResponseQueue.shift()) + // @ts-expect-error We mock subset of middleware inputs here. + await middlewareHandler(mock.fn(), {})(args); + assert.strictEqual( + args.request.headers['x-amz-user-agent'], + 'testUserAgent' ); - - const responseContent = await new BedrockConverseAdapter( - event, - [tool], - bedrockClient - ).askBedrock(); - - assert.deepStrictEqual( - responseContent, - finalBedrockResponse.output?.message?.content - ); - - assert.strictEqual(bedrockClientSendMock.mock.calls.length, 2); - const bedrockRequest2 = bedrockClientSendMock.mock.calls[1] - .arguments[0] as unknown as ConverseCommand; - assert.ok(toolUseBedrockResponse.output?.message?.content); - assert.deepStrictEqual(bedrockRequest2.input.messages?.pop(), { - role: 'user', - content: [ - { - toolResult: { - content: [ - { - text: 'unknown error occurred', - }, - ], - status: 'error', - toolUseId: - toolUseBedrockResponse.output?.message.content[0].toolUse - ?.toolUseId, - }, - }, - ], - } as Message); }); +}); - void it('returns client tool input block when client tool is requested and ignores executable tools', async () => { - const additionalToolOutput: ToolResultContentBlock = { - text: 'additionalToolOutput', - }; - const additionalTool: ExecutableTool = { - name: 'additionalTool', - description: 'additional tool description', - inputSchema: { - json: { - required: ['additionalToolRequiredProperty'], - }, - }, - execute: () => Promise.resolve(additionalToolOutput), - }; - const clientTool: ToolDefinition = { - name: 'clientTool', - description: 'client tool description', - inputSchema: { - json: { - required: ['clientToolRequiredProperty'], - }, - }, - }; +const askBedrockWithStreaming = async ( + adapter: BedrockConverseAdapter +): Promise> => { + const chunks: Array = []; + for await (const chunk of adapter.askBedrockStreaming()) { + chunks.push(chunk); + } + return chunks; +}; - const event: ConversationTurnEvent = { - ...commonEvent, - toolsConfiguration: { - clientTools: [clientTool], +const mockBedrockResponse = ( + contentBlocks: + | Array + | Array, + streamResponse: boolean +): ConverseStreamCommandOutput | ConverseCommandOutput => { + if (streamResponse) { + return mockConverseStreamCommandOutput(contentBlocks); + } + return mockConverseCommandOutput(contentBlocks); +}; +const mockConverseCommandOutput = ( + contentBlocks: + | Array + | Array +): ConverseCommandOutput => { + let stopReason: StopReason = 'end_turn'; + if (contentBlocks.find((block) => block.toolUse)) { + stopReason = 'tool_use'; + } + return { + $metadata: {}, + metrics: undefined, + output: { + message: { + role: 'assistant', + content: contentBlocks, }, - }; + }, + stopReason, + usage: undefined, + }; +}; - const bedrockClient = new BedrockRuntimeClient(); - const bedrockResponseQueue: Array = []; - const clientToolUseBlock = { - toolUse: { - toolUseId: randomUUID().toString(), - name: clientTool.name, - input: 'clientToolInput', - }, - }; - const toolUseBedrockResponse: ConverseCommandOutput = { - $metadata: {}, - metrics: undefined, - output: { - message: { - role: 'assistant', - content: [ - { - toolUse: { - toolUseId: randomUUID().toString(), - name: additionalTool.name, - input: 'additionalToolInput', - }, +const mockConverseStreamCommandOutput = ( + contentBlocks: + | Array + | Array +): ConverseStreamCommandOutput => { + const streamItems: Array = []; + let stopReason: StopReason | undefined; + streamItems.push({ + messageStart: { + role: 'assistant', + }, + }); + for (let i = 0; i < contentBlocks.length; i++) { + const block = contentBlocks[i]; + if (block.toolUse) { + stopReason = 'tool_use'; + streamItems.push({ + contentBlockStart: { + contentBlockIndex: i, + start: { + toolUse: { + toolUseId: block.toolUse.toolUseId, + name: block.toolUse.name, }, - clientToolUseBlock, - ], - }, - }, - stopReason: 'tool_use', - usage: undefined, - }; - bedrockResponseQueue.push(toolUseBedrockResponse); - - const bedrockClientSendMock = mock.method(bedrockClient, 'send', () => - Promise.resolve(bedrockResponseQueue.shift()) - ); - - const responseContent = await new BedrockConverseAdapter( - event, - [additionalTool], - bedrockClient - ).askBedrock(); - - assert.deepStrictEqual(responseContent, [clientToolUseBlock]); - - assert.strictEqual(bedrockClientSendMock.mock.calls.length, 1); - const expectedToolConfig: ToolConfiguration = { - tools: [ - { - toolSpec: { - name: additionalTool.name, - description: additionalTool.description, - inputSchema: additionalTool.inputSchema, }, }, - { - toolSpec: { - name: clientTool.name, - description: clientTool.description, - inputSchema: clientTool.inputSchema, + }); + const input = block.toolUse.input + ? JSON.stringify(block.toolUse.input) + : undefined; + streamItems.push({ + contentBlockDelta: { + contentBlockIndex: i, + delta: { + toolUse: { + // simulate chunked input + input: input?.substring(0, 1), + }, }, }, - ], - }; - const expectedBedrockInputCommonProperties = { - modelId: event.modelConfiguration.modelId, - inferenceConfig: event.modelConfiguration.inferenceConfiguration, - system: [ - { - text: event.modelConfiguration.systemPrompt, - }, - ], - toolConfig: expectedToolConfig, - }; - const bedrockRequest = bedrockClientSendMock.mock.calls[0] - .arguments[0] as unknown as ConverseCommand; - const expectedBedrockInput: ConverseCommandInput = { - messages: event.messages as Array, - ...expectedBedrockInputCommonProperties, - }; - assert.deepStrictEqual(bedrockRequest.input, expectedBedrockInput); - }); - - void it('decodes base64 encoded images', async () => { - const event: ConversationTurnEvent = { - ...commonEvent, - }; - - const fakeImagePayload = randomBytes(32); - - event.messages = [ - { - role: 'user', - content: [ - { - image: { - format: 'png', - source: { - bytes: fakeImagePayload.toString('base64'), + }); + if (input && input.length > 1) { + streamItems.push({ + contentBlockDelta: { + contentBlockIndex: i, + delta: { + toolUse: { + // simulate chunked input + input: input.substring(1), }, }, }, - ], - }, - ]; - - const bedrockClient = new BedrockRuntimeClient(); - const bedrockResponse: ConverseCommandOutput = { - $metadata: {}, - metrics: undefined, - output: { - message: { - role: 'assistant', - content: [ - { - text: 'block1', - }, - { - text: 'block2', - }, - ], + }); + } + streamItems.push({ + contentBlockStop: { + contentBlockIndex: i, }, - }, - stopReason: 'end_turn', - usage: undefined, - }; - const bedrockClientSendMock = mock.method(bedrockClient, 'send', () => - Promise.resolve(bedrockResponse) - ); - - await new BedrockConverseAdapter(event, [], bedrockClient).askBedrock(); - - assert.strictEqual(bedrockClientSendMock.mock.calls.length, 1); - const bedrockRequest = bedrockClientSendMock.mock.calls[0] - .arguments[0] as unknown as ConverseCommand; - assert.deepStrictEqual(bedrockRequest.input.messages, [ - { - role: 'user', - content: [ - { - image: { - format: 'png', - source: { - bytes: fakeImagePayload, - }, + }); + } else if (block.text) { + stopReason = 'end_turn'; + streamItems.push({ + contentBlockStart: { + contentBlockIndex: i, + start: undefined, + }, + }); + const input = block.text; + streamItems.push({ + contentBlockDelta: { + contentBlockIndex: i, + delta: { + // simulate chunked input + text: input.substring(0, 1), + }, + }, + }); + if (input.length > 1) { + streamItems.push({ + contentBlockDelta: { + contentBlockIndex: i, + delta: { + // simulate chunked input + text: input.substring(1), }, }, - ], - }, - ]); + }); + } + streamItems.push({ + contentBlockStop: { + contentBlockIndex: i, + }, + }); + } else { + throw new Error('Unsupported block type'); + } + } + streamItems.push({ + messageStop: { + stopReason: stopReason, + }, }); -}); + return { + $metadata: {}, + stream: (async function* (): AsyncGenerator { + for (const streamItem of streamItems) { + yield streamItem; + } + })(), + }; +}; diff --git a/packages/ai-constructs/src/conversation/runtime/bedrock_converse_adapter.ts b/packages/ai-constructs/src/conversation/runtime/bedrock_converse_adapter.ts index 7c7a572387..0b692a848a 100644 --- a/packages/ai-constructs/src/conversation/runtime/bedrock_converse_adapter.ts +++ b/packages/ai-constructs/src/conversation/runtime/bedrock_converse_adapter.ts @@ -4,16 +4,25 @@ import { ConverseCommand, ConverseCommandInput, ConverseCommandOutput, + ConverseStreamCommand, + ConverseStreamCommandInput, + ConverseStreamCommandOutput, Message, Tool, ToolConfiguration, + ToolInputSchema, } from '@aws-sdk/client-bedrock-runtime'; import { ConversationTurnEvent, ExecutableTool, + StreamingResponseChunk, ToolDefinition, } from './types.js'; import { ConversationTurnEventToolsProvider } from './event-tools-provider'; +import { ConversationMessageHistoryRetriever } from './conversation_message_history_retriever'; +import * as bedrock from '@aws-sdk/client-bedrock-runtime'; +import { ValidationError } from './errors'; +import { UserAgentProvider } from './user_agent_provider'; /** * This class is responsible for interacting with Bedrock Converse API @@ -36,8 +45,26 @@ export class BedrockConverseAdapter { private readonly bedrockClient: BedrockRuntimeClient = new BedrockRuntimeClient( { region: event.modelConfiguration.region } ), - eventToolsProvider = new ConversationTurnEventToolsProvider(event) + eventToolsProvider = new ConversationTurnEventToolsProvider(event), + private readonly messageHistoryRetriever = new ConversationMessageHistoryRetriever( + event + ), + userAgentProvider = new UserAgentProvider(event), + private readonly logger = console ) { + this.bedrockClient.middlewareStack.add( + (next) => (args) => { + // @ts-expect-error Request is typed as unknown. + // But this is recommended way to alter headers per https://github.com/aws/aws-sdk-js-v3/blob/main/README.md. + args.request.headers['x-amz-user-agent'] = + userAgentProvider.getUserAgent(); + return next(args); + }, + { + step: 'build', + name: 'amplify-user-agent-injector', + } + ); this.executableTools = [ ...eventToolsProvider.getEventTools(), ...additionalTools, @@ -61,7 +88,7 @@ export class BedrockConverseAdapter { this.clientToolByName.set(t.name, t); }); if (duplicateTools.size > 0) { - throw new Error( + throw new ValidationError( `Tools must have unique names. Duplicate tools: ${[ ...duplicateTools, ].join(', ')}.` @@ -73,7 +100,8 @@ export class BedrockConverseAdapter { const { modelId, systemPrompt, inferenceConfiguration } = this.event.modelConfiguration; - const messages: Array = this.getEventMessagesAsBedrockMessages(); + const messages: Array = + await this.getEventMessagesAsBedrockMessages(); let bedrockResponse: ConverseCommandOutput; do { @@ -85,9 +113,16 @@ export class BedrockConverseAdapter { inferenceConfig: inferenceConfiguration, toolConfig, }; + this.logger.info('Sending Bedrock Converse request'); + this.logger.debug('Bedrock Converse request:', converseCommandInput); bedrockResponse = await this.bedrockClient.send( new ConverseCommand(converseCommandInput) ); + this.logger.info( + `Received Bedrock Converse response, requestId=${bedrockResponse.$metadata.requestId}`, + bedrockResponse.usage + ); + this.logger.debug('Bedrock Converse response:', bedrockResponse); if (bedrockResponse.output?.message) { messages.push(bedrockResponse.output?.message); } @@ -107,26 +142,217 @@ export class BedrockConverseAdapter { // and propagate result back to client. return clientToolUseBlocks; } + const toolResponseContentBlocks: Array = []; for (const responseContentBlock of toolUseBlocks) { const toolUseBlock = responseContentBlock as ContentBlock.ToolUseMember; - const toolMessage = await this.executeTool(toolUseBlock); - messages.push(toolMessage); + const toolResultContentBlock = await this.executeTool(toolUseBlock); + toolResponseContentBlocks.push(toolResultContentBlock); } + messages.push({ + role: 'user', + content: toolResponseContentBlocks, + }); } } while (bedrockResponse.stopReason === 'tool_use'); return bedrockResponse.output?.message?.content ?? []; }; + /** + * Asks Bedrock for response using streaming version of Converse API. + */ + async *askBedrockStreaming(): AsyncGenerator { + const { modelId, systemPrompt, inferenceConfiguration } = + this.event.modelConfiguration; + + const messages: Array = + await this.getEventMessagesAsBedrockMessages(); + + let bedrockResponse: ConverseStreamCommandOutput; + // keep our own indexing for blocks instead of using Bedrock's indexes + // since we stream subset of these upstream. + let blockIndex = 0; + let lastBlockIndex = 0; + let stopReason = ''; + // Accumulates client facing content per turn. + // So that upstream can persist full message at the end of the streaming. + const accumulatedTurnContent: Array = []; + do { + const toolConfig = this.createToolConfiguration(); + const converseCommandInput: ConverseStreamCommandInput = { + modelId, + messages: [...messages], + system: [{ text: systemPrompt }], + inferenceConfig: inferenceConfiguration, + toolConfig, + }; + this.logger.info('Sending Bedrock Converse Stream request'); + this.logger.debug( + 'Bedrock Converse Stream request:', + converseCommandInput + ); + bedrockResponse = await this.bedrockClient.send( + new ConverseStreamCommand(converseCommandInput) + ); + this.logger.info( + `Received Bedrock Converse Stream response, requestId=${bedrockResponse.$metadata.requestId}` + ); + if (!bedrockResponse.stream) { + throw new Error('Bedrock response is missing stream'); + } + let toolUseBlock: ContentBlock.ToolUseMember | undefined; + let clientToolsRequested = false; + let text: string = ''; + let toolUseInput: string = ''; + let blockDeltaIndex = 0; + let lastBlockDeltaIndex = 0; + // Accumulate current message for the tool use loop purpose. + const accumulatedAssistantMessage: Message = { + role: undefined, + content: [], + }; + + for await (const chunk of bedrockResponse.stream) { + this.logger.debug('Bedrock Converse Stream response chunk:', chunk); + if (chunk.messageStart) { + accumulatedAssistantMessage.role = chunk.messageStart.role; + } else if (chunk.contentBlockStart) { + blockDeltaIndex = 0; + lastBlockDeltaIndex = 0; + if (chunk.contentBlockStart.start?.toolUse) { + toolUseBlock = { + toolUse: { + ...chunk.contentBlockStart.start?.toolUse, + input: undefined, + }, + }; + } + } else if (chunk.contentBlockDelta) { + if (chunk.contentBlockDelta.delta?.toolUse) { + if (!chunk.contentBlockDelta.delta.toolUse.input) { + toolUseInput = ''; + } else { + toolUseInput += chunk.contentBlockDelta.delta.toolUse.input; + } + } else if (chunk.contentBlockDelta.delta?.text) { + text += chunk.contentBlockDelta.delta.text; + yield { + accumulatedTurnContent: [...accumulatedTurnContent, { text }], + conversationId: this.event.conversationId, + associatedUserMessageId: this.event.currentMessageId, + contentBlockText: chunk.contentBlockDelta.delta.text, + contentBlockIndex: blockIndex, + contentBlockDeltaIndex: blockDeltaIndex, + }; + lastBlockDeltaIndex = blockDeltaIndex; + blockDeltaIndex++; + } + } else if (chunk.contentBlockStop) { + if (toolUseBlock) { + if (toolUseInput) { + toolUseBlock.toolUse.input = JSON.parse(toolUseInput); + } else { + // Bedrock API requires tool input to be non-null in message history. + // Therefore, falling back to empty object. + toolUseBlock.toolUse.input = {}; + } + accumulatedAssistantMessage.content?.push(toolUseBlock); + if ( + toolUseBlock.toolUse.name && + this.clientToolByName.has(toolUseBlock.toolUse.name) + ) { + clientToolsRequested = true; + accumulatedTurnContent.push(toolUseBlock); + yield { + accumulatedTurnContent: [...accumulatedTurnContent], + conversationId: this.event.conversationId, + associatedUserMessageId: this.event.currentMessageId, + contentBlockIndex: blockIndex, + contentBlockToolUse: JSON.stringify(toolUseBlock), + }; + lastBlockIndex = blockIndex; + blockIndex++; + } + toolUseBlock = undefined; + toolUseInput = ''; + } else { + accumulatedAssistantMessage.content?.push({ + text, + }); + accumulatedTurnContent.push({ text }); + yield { + accumulatedTurnContent: [...accumulatedTurnContent], + conversationId: this.event.conversationId, + associatedUserMessageId: this.event.currentMessageId, + contentBlockIndex: blockIndex, + contentBlockDoneAtIndex: lastBlockDeltaIndex, + }; + text = ''; + lastBlockIndex = blockIndex; + blockIndex++; + } + } else if (chunk.messageStop) { + stopReason = chunk.messageStop.stopReason ?? ''; + } + } + this.logger.debug( + 'Accumulated Bedrock Converse Stream response:', + accumulatedAssistantMessage + ); + if (clientToolsRequested) { + // For now if any of client tools is used we ignore executable tools + // and propagate result back to client. + yield { + accumulatedTurnContent: [...accumulatedTurnContent], + conversationId: this.event.conversationId, + associatedUserMessageId: this.event.currentMessageId, + contentBlockIndex: lastBlockIndex, + stopReason: stopReason, + }; + return; + } + messages.push(accumulatedAssistantMessage); + if (stopReason === 'tool_use') { + const responseContentBlocks = accumulatedAssistantMessage.content ?? []; + const toolUseBlocks = responseContentBlocks.filter( + (block) => 'toolUse' in block + ) as Array; + const toolResponseContentBlocks: Array = []; + for (const responseContentBlock of toolUseBlocks) { + const toolUseBlock = + responseContentBlock as ContentBlock.ToolUseMember; + const toolResultContentBlock = await this.executeTool(toolUseBlock); + toolResponseContentBlocks.push(toolResultContentBlock); + } + messages.push({ + role: 'user', + content: toolResponseContentBlocks, + }); + } + } while (stopReason === 'tool_use'); + + yield { + accumulatedTurnContent: [...accumulatedTurnContent], + conversationId: this.event.conversationId, + associatedUserMessageId: this.event.currentMessageId, + contentBlockIndex: lastBlockIndex, + stopReason: stopReason, + }; + } + /** * Maps event messages to Bedrock types. * 1. Makes a copy so that we don't mutate event. * 2. Decodes Base64 encoded images. */ - private getEventMessagesAsBedrockMessages = (): Array => { + private getEventMessagesAsBedrockMessages = async (): Promise< + Array + > => { const messages: Array = []; - for (const message of this.event.messages) { + const eventMessages = + await this.messageHistoryRetriever.getMessageHistory(); + for (const message of eventMessages) { const messageContent: Array = []; for (const contentElement of message.content) { if (typeof contentElement.image?.source?.bytes === 'string') { @@ -162,7 +388,9 @@ export class BedrockConverseAdapter { toolSpec: { name: t.name, description: t.description, - inputSchema: t.inputSchema, + // We have to cast to bedrock type as we're using different types to describe JSON schema in our API. + // These types are runtime compatible. + inputSchema: t.inputSchema as ToolInputSchema, }, }; }), @@ -171,7 +399,7 @@ export class BedrockConverseAdapter { private executeTool = async ( toolUseBlock: ContentBlock.ToolUseMember - ): Promise => { + ): Promise => { if (!toolUseBlock.toolUse.name) { throw Error('Bedrock tool use response is missing a tool name'); } @@ -182,45 +410,34 @@ export class BedrockConverseAdapter { ); } try { + this.logger.info(`Invoking tool ${tool.name}`); + this.logger.debug('Tool input:', toolUseBlock.toolUse.input); const toolResponse = await tool.execute(toolUseBlock.toolUse.input); + this.logger.info(`Received response from ${tool.name} tool`); + this.logger.debug(toolResponse); return { - role: 'user', - content: [ - { - toolResult: { - toolUseId: toolUseBlock.toolUse.toolUseId, - content: [toolResponse], - status: 'success', - }, - }, - ], + toolResult: { + toolUseId: toolUseBlock.toolUse.toolUseId, + content: [toolResponse], + status: 'success', + }, }; } catch (e) { if (e instanceof Error) { return { - role: 'user', - content: [ - { - toolResult: { - toolUseId: toolUseBlock.toolUse.toolUseId, - content: [{ text: e.toString() }], - status: 'error', - }, - }, - ], + toolResult: { + toolUseId: toolUseBlock.toolUse.toolUseId, + content: [{ text: e.toString() }], + status: 'error', + }, }; } return { - role: 'user', - content: [ - { - toolResult: { - toolUseId: toolUseBlock.toolUse.toolUseId, - content: [{ text: 'unknown error occurred' }], - status: 'error', - }, - }, - ], + toolResult: { + toolUseId: toolUseBlock.toolUse.toolUseId, + content: [{ text: 'unknown error occurred' }], + status: 'error', + }, }; } }; diff --git a/packages/ai-constructs/src/conversation/runtime/conversation_message_history_retriever.test.ts b/packages/ai-constructs/src/conversation/runtime/conversation_message_history_retriever.test.ts new file mode 100644 index 0000000000..9215fc80da --- /dev/null +++ b/packages/ai-constructs/src/conversation/runtime/conversation_message_history_retriever.test.ts @@ -0,0 +1,780 @@ +import { describe, it, mock } from 'node:test'; +import assert from 'node:assert'; +import { MutationResponseInput } from './conversation_turn_response_sender'; +import { ConversationMessage, ConversationTurnEvent } from './types'; +import { + GraphqlRequest, + GraphqlRequestExecutor, +} from './graphql_request_executor'; +import { + ConversationHistoryMessageItem, + ConversationMessageHistoryRetriever, + GetQueryOutput, + ListQueryOutput, +} from './conversation_message_history_retriever'; +import { UserAgentProvider } from './user_agent_provider'; + +type TestCase = { + name: string; + mockListResponseMessages: Array; + mockGetCurrentMessage?: ConversationHistoryMessageItem; + expectedMessages: Array; +}; + +void describe('Conversation message history retriever', () => { + const event: ConversationTurnEvent = { + conversationId: 'testConversationId', + currentMessageId: 'testCurrentMessageId', + graphqlApiEndpoint: '', + messageHistoryQuery: { + getQueryName: 'testGetQueryName', + getQueryInputTypeName: 'testGetQueryInputTypeName', + listQueryName: 'testListQueryName', + listQueryInputTypeName: 'testListQueryInputTypeName', + }, + modelConfiguration: { modelId: '', systemPrompt: '' }, + request: { headers: { authorization: '' } }, + responseMutation: { + name: '', + inputTypeName: '', + selectionSet: '', + }, + }; + + const testCases: Array = [ + { + name: 'Retrieves message history that includes current message', + mockListResponseMessages: [ + { + id: 'someNonCurrentMessageId1', + conversationId: event.conversationId, + role: 'user', + content: [ + { + text: 'message1', + }, + ], + }, + { + id: 'someNonCurrentMessageId2', + associatedUserMessageId: 'someNonCurrentMessageId1', + conversationId: event.conversationId, + role: 'assistant', + content: [ + { + text: 'message2', + }, + ], + }, + { + id: event.currentMessageId, + conversationId: event.conversationId, + role: 'user', + content: [ + { + text: 'message3', + }, + ], + }, + ], + expectedMessages: [ + { + role: 'user', + content: [ + { + text: 'message1', + }, + ], + }, + { + role: 'assistant', + content: [ + { + text: 'message2', + }, + ], + }, + { + role: 'user', + content: [ + { + text: 'message3', + }, + ], + }, + ], + }, + { + name: 'Retrieves message history that does not include current message with fallback to get it directly', + mockListResponseMessages: [ + { + id: 'someNonCurrentMessageId1', + conversationId: event.conversationId, + role: 'user', + content: [ + { + text: 'message1', + }, + ], + }, + { + id: 'someNonCurrentMessageId2', + associatedUserMessageId: 'someNonCurrentMessageId1', + conversationId: event.conversationId, + role: 'assistant', + content: [ + { + text: 'message2', + }, + ], + }, + ], + mockGetCurrentMessage: { + id: event.currentMessageId, + conversationId: event.conversationId, + role: 'user', + content: [ + { + text: 'message3', + }, + ], + }, + expectedMessages: [ + { + role: 'user', + content: [ + { + text: 'message1', + }, + ], + }, + { + role: 'assistant', + content: [ + { + text: 'message2', + }, + ], + }, + { + role: 'user', + content: [ + { + text: 'message3', + }, + ], + }, + ], + }, + { + name: 'Re-orders delayed assistant responses', + mockListResponseMessages: [ + // Simulate that two first messages were sent without waiting for assistant response + { + id: 'userMessage1', + conversationId: event.conversationId, + role: 'user', + content: [ + { + text: 'userMessage1', + }, + ], + }, + { + id: 'userMessage2', + conversationId: event.conversationId, + role: 'user', + content: [ + { + text: 'userMessage2', + }, + ], + }, + // also simulate that responses came back out of order + { + id: 'assistantResponse2', + associatedUserMessageId: 'userMessage2', + conversationId: event.conversationId, + role: 'assistant', + content: [ + { + text: 'assistantResponse2', + }, + ], + }, + { + id: 'assistantResponse1', + associatedUserMessageId: 'userMessage1', + conversationId: event.conversationId, + role: 'assistant', + content: [ + { + text: 'assistantResponse1', + }, + ], + }, + { + id: event.currentMessageId, + conversationId: event.conversationId, + role: 'user', + content: [ + { + text: 'currentUserMessage', + }, + ], + }, + ], + expectedMessages: [ + { + role: 'user', + content: [ + { + text: 'userMessage1', + }, + ], + }, + { + role: 'assistant', + content: [ + { + text: 'assistantResponse1', + }, + ], + }, + { + role: 'user', + content: [ + { + text: 'userMessage2', + }, + ], + }, + { + role: 'assistant', + content: [ + { + text: 'assistantResponse2', + }, + ], + }, + { + role: 'user', + content: [ + { + text: 'currentUserMessage', + }, + ], + }, + ], + }, + { + name: 'Skips user message that does not have response yet', + mockListResponseMessages: [ + // Simulate that two first messages were sent without waiting for assistant response + // and none was responded to yet. + { + id: 'userMessage1', + conversationId: event.conversationId, + role: 'user', + content: [ + { + text: 'userMessage1', + }, + ], + }, + { + id: 'userMessage2', + conversationId: event.conversationId, + role: 'user', + content: [ + { + text: 'userMessage2', + }, + ], + }, + { + id: event.currentMessageId, + conversationId: event.conversationId, + role: 'user', + content: [ + { + text: 'currentUserMessage', + }, + ], + }, + ], + expectedMessages: [ + { + role: 'user', + content: [ + { + text: 'currentUserMessage', + }, + ], + }, + ], + }, + { + name: 'Injects aiContext', + mockListResponseMessages: [ + { + id: event.currentMessageId, + conversationId: event.conversationId, + role: 'user', + aiContext: { some: { ai: 'context' } }, + content: [ + { + text: 'currentUserMessage', + }, + ], + }, + ], + expectedMessages: [ + { + role: 'user', + content: [ + { + text: 'currentUserMessage', + }, + { + text: '{"some":{"ai":"context"}}', + }, + ], + }, + ], + }, + { + name: 'Replaces null values with undefined', + mockListResponseMessages: [ + { + id: event.currentMessageId, + conversationId: event.conversationId, + role: 'user', + content: [ + { + text: 'some_text', + // @ts-expect-error Intentionally providing null outside of typing + image: null, + // @ts-expect-error Intentionally providing null outside of typing + document: null, + // @ts-expect-error Intentionally providing null outside of typing + toolUse: null, + // @ts-expect-error Intentionally providing null outside of typing + toolResult: null, + // @ts-expect-error Intentionally providing null outside of typing + guardContent: null, + // @ts-expect-error Intentionally providing null outside of typing + $unknown: null, + }, + { + // @ts-expect-error Intentionally providing null outside of typing + text: null, + document: { format: 'csv', name: 'test_name', source: undefined }, + }, + ], + }, + ], + expectedMessages: [ + { + role: 'user', + content: [ + { + text: 'some_text', + image: undefined, + document: undefined, + toolUse: undefined, + toolResult: undefined, + guardContent: undefined, + $unknown: undefined, + }, + { + text: undefined, + document: { format: 'csv', name: 'test_name', source: undefined }, + }, + ], + }, + ], + }, + { + name: 'Parses client tools json elements', + mockListResponseMessages: [ + { + id: event.currentMessageId, + conversationId: event.conversationId, + role: 'user', + content: [ + { + toolUse: { + name: 'testToolUse', + toolUseId: 'testToolUseId', + input: '{ "testKey": "testValue" }', + }, + }, + { + toolResult: { + status: 'success', + toolUseId: 'testToolUseId', + content: [ + { + json: '{ "testKey": "testValue" }', + }, + ], + }, + }, + ], + }, + ], + expectedMessages: [ + { + role: 'user', + content: [ + { + toolUse: { + name: 'testToolUse', + toolUseId: 'testToolUseId', + input: { testKey: 'testValue' }, + }, + }, + { + toolResult: { + status: 'success', + toolUseId: 'testToolUseId', + content: [ + { + json: { testKey: 'testValue' }, + }, + ], + }, + }, + ], + }, + ], + }, + { + name: 'Removes tool usage from non-current turns', + mockListResponseMessages: [ + { + id: 'someNonCurrentMessageId1', + conversationId: event.conversationId, + role: 'user', + content: [ + { + text: 'nonCurrentMessage1', + }, + ], + }, + { + id: 'someNonCurrentMessageId2', + associatedUserMessageId: 'someNonCurrentMessageId1', + conversationId: event.conversationId, + role: 'assistant', + content: [ + { + text: 'nonCurrentMessage2', + }, + { + toolUse: { + name: 'testToolUse1', + toolUseId: 'testToolUseId1', + input: undefined, + }, + }, + ], + }, + { + id: 'someNonCurrentMessageId3', + conversationId: event.conversationId, + role: 'user', + content: [ + { + toolResult: { + status: 'success', + toolUseId: 'testToolUseId1', + content: undefined, + }, + }, + ], + }, + { + id: 'someNonCurrentMessageId4', + associatedUserMessageId: 'someNonCurrentMessageId3', + conversationId: event.conversationId, + role: 'assistant', + content: [ + { + text: 'nonCurrentMessage3', + }, + { + toolUse: { + name: 'testToolUse2', + toolUseId: 'testToolUseId2', + input: undefined, + }, + }, + ], + }, + { + id: 'someNonCurrentMessageId5', + conversationId: event.conversationId, + role: 'user', + content: [ + { + toolResult: { + status: 'success', + toolUseId: 'testToolUseId2', + content: undefined, + }, + }, + ], + }, + { + id: 'someNonCurrentMessageId5', + associatedUserMessageId: 'someNonCurrentMessageId5', + conversationId: event.conversationId, + role: 'assistant', + content: [ + { + text: 'nonCurrentMessage4', + }, + ], + }, + // Current turn with multiple tool use. + { + id: 'someCurrentMessageId1', + conversationId: event.conversationId, + role: 'user', + content: [ + { + text: 'currentMessage1', + }, + ], + }, + { + id: 'someCurrentMessageId2', + associatedUserMessageId: 'someCurrentMessageId1', + conversationId: event.conversationId, + role: 'assistant', + content: [ + { + text: 'currentMessage2', + }, + { + toolUse: { + name: 'testToolUse3', + toolUseId: 'testToolUseId3', + input: undefined, + }, + }, + ], + }, + { + id: 'someCurrentMessageId3', + conversationId: event.conversationId, + role: 'user', + content: [ + { + toolResult: { + status: 'success', + toolUseId: 'testToolUseId3', + content: undefined, + }, + }, + ], + }, + { + id: 'someCurrentMessageId4', + associatedUserMessageId: 'someCurrentMessageId3', + conversationId: event.conversationId, + role: 'assistant', + content: [ + { + text: 'currentMessage3', + }, + { + toolUse: { + name: 'testToolUse4', + toolUseId: 'testToolUseId4', + input: undefined, + }, + }, + ], + }, + { + id: event.currentMessageId, + conversationId: event.conversationId, + role: 'user', + content: [ + { + toolResult: { + status: 'success', + toolUseId: 'testToolUseId2', + content: undefined, + }, + }, + ], + }, + ], + expectedMessages: [ + { + role: 'user', + content: [ + { + text: 'nonCurrentMessage1', + }, + ], + }, + { + role: 'assistant', + content: [ + { + text: 'nonCurrentMessage2', + }, + { + text: 'nonCurrentMessage3', + }, + { + text: 'nonCurrentMessage4', + }, + ], + }, + { + role: 'user', + content: [ + { + text: 'currentMessage1', + }, + ], + }, + { + role: 'assistant', + content: [ + { + text: 'currentMessage2', + }, + { + toolUse: { + name: 'testToolUse3', + toolUseId: 'testToolUseId3', + input: undefined, + }, + }, + ], + }, + { + role: 'user', + content: [ + { + toolResult: { + status: 'success', + toolUseId: 'testToolUseId3', + content: undefined, + }, + }, + ], + }, + { + role: 'assistant', + content: [ + { + text: 'currentMessage3', + }, + { + toolUse: { + name: 'testToolUse4', + toolUseId: 'testToolUseId4', + input: undefined, + }, + }, + ], + }, + { + role: 'user', + content: [ + { + toolResult: { + status: 'success', + toolUseId: 'testToolUseId2', + content: undefined, + }, + }, + ], + }, + ], + }, + ]; + + for (const testCase of testCases) { + void it(testCase.name, async () => { + const userAgentProvider = new UserAgentProvider( + {} as unknown as ConversationTurnEvent + ); + mock.method(userAgentProvider, 'getUserAgent', () => ''); + const graphqlRequestExecutor = new GraphqlRequestExecutor( + '', + '', + userAgentProvider + ); + const executeGraphqlMock = mock.method( + graphqlRequestExecutor, + 'executeGraphql', + (request: GraphqlRequest) => { + if (request.query.match(/ListMessages/)) { + const mockListResponse: ListQueryOutput = { + data: { + [event.messageHistoryQuery.listQueryName]: { + // clone array + items: [...testCase.mockListResponseMessages], + }, + }, + }; + return Promise.resolve(mockListResponse); + } + if ( + request.query.match(/GetMessage/) && + testCase.mockGetCurrentMessage + ) { + const mockGetResponse: GetQueryOutput = { + data: { + [event.messageHistoryQuery.getQueryName]: + testCase.mockGetCurrentMessage, + }, + }; + return Promise.resolve(mockGetResponse); + } + throw new Error('The query is not mocked'); + } + ); + + const retriever = new ConversationMessageHistoryRetriever( + event, + graphqlRequestExecutor + ); + const messages = await retriever.getMessageHistory(); + + assert.strictEqual( + executeGraphqlMock.mock.calls.length, + testCase.mockGetCurrentMessage ? 2 : 1 + ); + const listRequest = executeGraphqlMock.mock.calls[0] + .arguments[0] as GraphqlRequest; + assert.match(listRequest.query, /ListMessages/); + assert.deepStrictEqual(listRequest.variables, { + filter: { + conversationId: { + eq: 'testConversationId', + }, + }, + limit: 1000, + }); + if (testCase.mockGetCurrentMessage) { + const getRequest = executeGraphqlMock.mock.calls[1] + .arguments[0] as GraphqlRequest; + assert.match(getRequest.query, /GetMessage/); + assert.deepStrictEqual(getRequest.variables, { + id: event.currentMessageId, + }); + } + assert.deepStrictEqual(messages, testCase.expectedMessages); + }); + } +}); diff --git a/packages/ai-constructs/src/conversation/runtime/conversation_message_history_retriever.ts b/packages/ai-constructs/src/conversation/runtime/conversation_message_history_retriever.ts new file mode 100644 index 0000000000..c98889522a --- /dev/null +++ b/packages/ai-constructs/src/conversation/runtime/conversation_message_history_retriever.ts @@ -0,0 +1,351 @@ +import { + ConversationMessage, + ConversationMessageContentBlock, + ConversationTurnEvent, +} from './types'; +import { GraphqlRequestExecutor } from './graphql_request_executor'; +import { UserAgentProvider } from './user_agent_provider'; + +export type ConversationHistoryMessageItem = ConversationMessage & { + id: string; + conversationId: string; + associatedUserMessageId?: string; + aiContext?: unknown; +}; + +export type GetQueryInput = { + id: string; +}; + +export type GetQueryOutput = { + data: Record; +}; + +export type ListQueryInput = { + filter: { + conversationId: { + eq: string; + }; + }; + limit: number; +}; + +export type ListQueryOutput = { + data: Record< + string, + { + items: Array; + } + >; +}; + +/** + * These are all properties we have to pull. + * Unfortunately, GQL doesn't support wildcards. + * https://github.com/graphql/graphql-spec/issues/127 + */ +const messageItemSelectionSet = ` + id + conversationId + associatedUserMessageId + aiContext + role + content { + text + document { + source { + bytes + } + format + name + } + image { + format + source { + bytes + } + } + toolResult { + content { + document { + format + name + source { + bytes + } + } + image { + format + source { + bytes + } + } + json + text + } + status + toolUseId + } + toolUse { + input + name + toolUseId + } + } +`; + +/** + * This class is responsible for retrieving message history that belongs to conversation turn event. + * It queries AppSync to list messages that belong to conversation. + * Additionally, it looks up a current message in case it's missing in the list due to eventual consistency. + */ +export class ConversationMessageHistoryRetriever { + /** + * Creates conversation message history retriever. + */ + constructor( + private readonly event: ConversationTurnEvent, + private readonly graphqlRequestExecutor = new GraphqlRequestExecutor( + event.graphqlApiEndpoint, + event.request.headers.authorization, + new UserAgentProvider(event) + ) + ) {} + + getMessageHistory = async (): Promise> => { + const messages = await this.listMessages(); + + let currentMessage = messages.find( + (m) => m.id === this.event.currentMessageId + ); + + // This is a fallback in case current message is not available in the message list. + // I.e. in a situation when freshly written message is not yet visible in + // eventually consistent reads. + if (!currentMessage) { + currentMessage = await this.getCurrentMessage(); + messages.push(currentMessage); + } + + // Index assistant messages by corresponding user message. + const assistantMessageByUserMessageId: Map< + string, + ConversationHistoryMessageItem + > = new Map(); + messages.forEach((message) => { + if (message.role === 'assistant' && message.associatedUserMessageId) { + assistantMessageByUserMessageId.set( + message.associatedUserMessageId, + message + ); + } + }); + + // Reconcile history and inject aiContext + const orderedMessages = messages.reduce((acc, current) => { + // Bedrock expects that message history is user->assistant->user->assistant->... and so on. + // The chronological order doesn't assure this ordering if there were any concurrent messages sent. + // Therefore, conversation is ordered by user's messages only and corresponding assistant messages are inserted + // into right place regardless of their createdAt value. + // This algorithm assumes that GQL query returns messages sorted by createdAt. + if (current.role === 'assistant') { + // Initially, skip assistant messages, these might be out of chronological order. + return acc; + } + if ( + current.role === 'user' && + !assistantMessageByUserMessageId.has(current.id) && + current.id !== this.event.currentMessageId + ) { + // Skip user messages that didn't get answer from assistant yet. + // These might be still "in-flight", i.e. assistant is still working on them in separate invocation. + // Except current message, we want to process that one. + return acc; + } + const aiContext = current.aiContext; + const content = aiContext + ? [...current.content, { text: JSON.stringify(aiContext) }] + : current.content; + + acc.push({ role: current.role, content }); + + // Find and insert corresponding assistant message. + const correspondingAssistantMessage = assistantMessageByUserMessageId.get( + current.id + ); + if (correspondingAssistantMessage) { + acc.push({ + role: correspondingAssistantMessage.role, + content: correspondingAssistantMessage.content, + }); + } + return acc; + }, [] as Array); + + // Remove tool usage from non-current turn and squash messages. + return this.squashNonCurrentTurns(orderedMessages); + }; + + /** + * This function removes tool usage from non-current turns. + * The tool usage and result blocks don't matter after a turn is completed, + * but do cost extra tokens to process. + * The algorithm is as follows: + * 1. Find where current turn begins. I.e. last user message that isn't tool block. + * 2. Remove toolUse and toolResult blocks before current turn. + * 3. Squash continuous sequences of messages that belong to same 'message.role'. + */ + private squashNonCurrentTurns = (messages: Array) => { + const isNonToolBlockPredicate = ( + contentBlock: ConversationMessageContentBlock + ) => !contentBlock.toolUse && !contentBlock.toolResult; + + // find where current turn begins. I.e. last user message that is not related to tools + const lastNonToolUseUserMessageIndex = messages.findLastIndex((message) => { + return ( + message.role === 'user' && message.content.find(isNonToolBlockPredicate) + ); + }); + + // No non-current turns, don't transform. + if (lastNonToolUseUserMessageIndex <= 0) { + return messages; + } + + const squashedMessages: Array = []; + + // Define a "buffer". I.e. a message we keep around and squash content on. + let currentSquashedMessage: ConversationMessage | undefined = undefined; + // Process messages before current turn begins + // Remove tool usage blocks. + // Combine content for consecutive message that have same role. + for (let i = 0; i < lastNonToolUseUserMessageIndex; i++) { + const currentMessage = messages[i]; + const currentMessageRole = currentMessage.role; + const currentMessageNonToolContent = currentMessage.content.filter( + isNonToolBlockPredicate + ); + if (currentMessageNonToolContent.length === 0) { + // Tool only message. Nothing to squash, skip; + continue; + } + + if (!currentSquashedMessage) { + // Nothing squashed yet, initialize the buffer. + currentSquashedMessage = { + role: currentMessageRole, + content: currentMessageNonToolContent, + }; + } else if (currentSquashedMessage.role === currentMessageRole) { + // if role is same append content. + currentSquashedMessage.content.push(...currentMessageNonToolContent); + } else { + // if role flips push current squashed message and re-initialize the buffer. + squashedMessages.push(currentSquashedMessage); + currentSquashedMessage = { + role: currentMessageRole, + content: currentMessageNonToolContent, + }; + } + } + // flush the last buffer. + if (currentSquashedMessage) { + squashedMessages.push(currentSquashedMessage); + } + + // Append current turn as is. + squashedMessages.push(...messages.slice(lastNonToolUseUserMessageIndex)); + return squashedMessages; + }; + + private getCurrentMessage = + async (): Promise => { + const query = ` + query GetMessage($id: ${this.event.messageHistoryQuery.getQueryInputTypeName}!) { + ${this.event.messageHistoryQuery.getQueryName}(id: $id) { + ${messageItemSelectionSet} + } + } + `; + const variables: GetQueryInput = { + id: this.event.currentMessageId, + }; + + const response = await this.graphqlRequestExecutor.executeGraphql< + GetQueryInput, + GetQueryOutput + >({ + query, + variables, + }); + + return response.data[this.event.messageHistoryQuery.getQueryName]; + }; + + private listMessages = async (): Promise< + Array + > => { + const query = ` + query ListMessages($filter: ${this.event.messageHistoryQuery.listQueryInputTypeName}!, $limit: Int) { + ${this.event.messageHistoryQuery.listQueryName}(filter: $filter, limit: $limit) { + items { + ${messageItemSelectionSet} + } + } + } + `; + const variables: ListQueryInput = { + filter: { + conversationId: { + eq: this.event.conversationId, + }, + }, + limit: this.event.messageHistoryQuery.listQueryLimit ?? 1000, + }; + + const response = await this.graphqlRequestExecutor.executeGraphql< + ListQueryInput, + ListQueryOutput + >({ + query, + variables, + }); + + const items = + response.data[this.event.messageHistoryQuery.listQueryName].items; + + items.forEach((item) => { + item.content?.forEach((contentBlock) => { + let property: keyof typeof contentBlock; + for (property in contentBlock) { + // Deserialization of GraphQl query result sets these properties to 'null' + // This can trigger Bedrock SDK validation as it expects 'undefined' if properties are not set. + // We can't fix how GraphQl response is deserialized. + // Therefore, we apply this transformation to fix the data. + if (contentBlock[property] === null) { + contentBlock[property] = undefined; + } + } + + if (typeof contentBlock.toolUse?.input === 'string') { + // toolUse.input may come as serialized JSON for Client Tools. + // Parse it in that case. + contentBlock.toolUse.input = JSON.parse(contentBlock.toolUse.input); + } + if (contentBlock.toolResult?.content) { + contentBlock.toolResult.content.forEach((toolResultContentBlock) => { + if (typeof toolResultContentBlock.json === 'string') { + // toolResult.content[].json may come as serialized JSON for Client Tools. + // Parse it in that case. + toolResultContentBlock.json = JSON.parse( + toolResultContentBlock.json + ); + } + }); + } + }); + }); + + return items; + }; +} diff --git a/packages/ai-constructs/src/conversation/runtime/conversation_turn_executor.test.ts b/packages/ai-constructs/src/conversation/runtime/conversation_turn_executor.test.ts index e9a0664750..8c42431b63 100644 --- a/packages/ai-constructs/src/conversation/runtime/conversation_turn_executor.test.ts +++ b/packages/ai-constructs/src/conversation/runtime/conversation_turn_executor.test.ts @@ -1,17 +1,23 @@ import { describe, it, mock } from 'node:test'; import assert from 'node:assert'; import { ConversationTurnExecutor } from './conversation_turn_executor'; -import { ConversationTurnEvent } from './types'; +import { ConversationTurnEvent, StreamingResponseChunk } from './types'; import { BedrockConverseAdapter } from './bedrock_converse_adapter'; import { ContentBlock } from '@aws-sdk/client-bedrock-runtime'; import { ConversationTurnResponseSender } from './conversation_turn_response_sender'; +import { Lazy } from './lazy'; void describe('Conversation turn executor', () => { const event: ConversationTurnEvent = { conversationId: 'testConversationId', currentMessageId: 'testCurrentMessageId', graphqlApiEndpoint: '', - messages: [], + messageHistoryQuery: { + getQueryName: '', + getQueryInputTypeName: '', + listQueryName: '', + listQueryInputTypeName: '', + }, modelConfiguration: { modelId: '', systemPrompt: '' }, request: { headers: { authorization: '' } }, responseMutation: { @@ -39,18 +45,26 @@ void describe('Conversation turn executor', () => { () => Promise.resolve() ); + const streamResponseSenderSendResponseMock = mock.method( + responseSender, + 'sendResponseChunk', + () => Promise.resolve() + ); + const consoleErrorMock = mock.fn(); const consoleLogMock = mock.fn(); + const consoleDebugMock = mock.fn(); const consoleMock = { error: consoleErrorMock, log: consoleLogMock, + debug: consoleDebugMock, } as unknown as Console; await new ConversationTurnExecutor( event, [], - bedrockConverseAdapter, - responseSender, + new Lazy(() => responseSender), + new Lazy(() => bedrockConverseAdapter), consoleMock ).execute(); @@ -58,6 +72,10 @@ void describe('Conversation turn executor', () => { bedrockConverseAdapterAskBedrockMock.mock.calls.length, 1 ); + assert.strictEqual( + streamResponseSenderSendResponseMock.mock.calls.length, + 0 + ); assert.strictEqual(responseSenderSendResponseMock.mock.calls.length, 1); assert.deepStrictEqual( responseSenderSendResponseMock.mock.calls[0].arguments[0], @@ -77,6 +95,105 @@ void describe('Conversation turn executor', () => { assert.strictEqual(consoleErrorMock.mock.calls.length, 0); }); + void it('executes turn successfully with streaming response', async () => { + const streamingEvent: ConversationTurnEvent = { + ...event, + streamResponse: true, + }; + const bedrockConverseAdapter = new BedrockConverseAdapter( + streamingEvent, + [] + ); + const chunks: Array = [ + { + contentBlockText: 'chunk1', + contentBlockIndex: 0, + contentBlockDeltaIndex: 1, + conversationId: 'testConversationId', + associatedUserMessageId: 'testCurrentMessageId', + accumulatedTurnContent: [{ text: 'chunk1' }], + }, + { + contentBlockText: 'chunk2', + contentBlockIndex: 0, + contentBlockDeltaIndex: 1, + conversationId: 'testConversationId', + associatedUserMessageId: 'testCurrentMessageId', + accumulatedTurnContent: [{ text: 'chunk1chunk2' }], + }, + ]; + const bedrockConverseAdapterAskBedrockMock = mock.method( + bedrockConverseAdapter, + 'askBedrockStreaming', + () => + (async function* (): AsyncGenerator { + for (const chunk of chunks) { + yield chunk; + } + })() + ); + const responseSender = new ConversationTurnResponseSender(streamingEvent); + const responseSenderSendResponseMock = mock.method( + responseSender, + 'sendResponse', + () => Promise.resolve() + ); + + const streamResponseSenderSendResponseMock = mock.method( + responseSender, + 'sendResponseChunk', + () => Promise.resolve() + ); + + const consoleErrorMock = mock.fn(); + const consoleLogMock = mock.fn(); + const consoleDebugMock = mock.fn(); + const consoleMock = { + error: consoleErrorMock, + log: consoleLogMock, + debug: consoleDebugMock, + } as unknown as Console; + + await new ConversationTurnExecutor( + streamingEvent, + [], + new Lazy(() => responseSender), + new Lazy(() => bedrockConverseAdapter), + consoleMock + ).execute(); + + assert.strictEqual( + bedrockConverseAdapterAskBedrockMock.mock.calls.length, + 1 + ); + assert.strictEqual( + streamResponseSenderSendResponseMock.mock.calls.length, + 2 + ); + assert.deepStrictEqual( + streamResponseSenderSendResponseMock.mock.calls[0].arguments[0], + chunks[0] + ); + assert.deepStrictEqual( + streamResponseSenderSendResponseMock.mock.calls[1].arguments[0], + chunks[1] + ); + + assert.strictEqual(responseSenderSendResponseMock.mock.calls.length, 0); + + assert.strictEqual(consoleLogMock.mock.calls.length, 2); + assert.strictEqual( + consoleLogMock.mock.calls[0].arguments[0], + 'Handling conversation turn event, currentMessageId=testCurrentMessageId, conversationId=testConversationId' + ); + assert.strictEqual( + consoleLogMock.mock.calls[1].arguments[0], + 'Conversation turn event handled successfully, currentMessageId=testCurrentMessageId, conversationId=testConversationId' + ); + + assert.strictEqual(consoleErrorMock.mock.calls.length, 0); + }); + void it('logs and propagates error if bedrock adapter throws', async () => { const bedrockConverseAdapter = new BedrockConverseAdapter(event, []); const bedrockError = new Error('Bedrock failed'); @@ -92,11 +209,27 @@ void describe('Conversation turn executor', () => { () => Promise.resolve() ); + const streamResponseSenderSendResponseMock = mock.method( + responseSender, + 'sendResponseChunk', + () => Promise.resolve() + ); + + const responseSenderSendErrorsMock = mock.method( + responseSender, + 'sendErrors', + () => Promise.resolve() + ); + const consoleErrorMock = mock.fn(); const consoleLogMock = mock.fn(); + const consoleDebugMock = mock.fn(); + const consoleWarnMock = mock.fn(); const consoleMock = { error: consoleErrorMock, log: consoleLogMock, + debug: consoleDebugMock, + warn: consoleWarnMock, } as unknown as Console; await assert.rejects( @@ -104,8 +237,8 @@ void describe('Conversation turn executor', () => { new ConversationTurnExecutor( event, [], - bedrockConverseAdapter, - responseSender, + new Lazy(() => responseSender), + new Lazy(() => bedrockConverseAdapter), consoleMock ).execute(), (error: Error) => { @@ -118,6 +251,10 @@ void describe('Conversation turn executor', () => { bedrockConverseAdapterAskBedrockMock.mock.calls.length, 1 ); + assert.strictEqual( + streamResponseSenderSendResponseMock.mock.calls.length, + 0 + ); assert.strictEqual(responseSenderSendResponseMock.mock.calls.length, 0); assert.strictEqual(consoleLogMock.mock.calls.length, 1); @@ -135,6 +272,16 @@ void describe('Conversation turn executor', () => { consoleErrorMock.mock.calls[0].arguments[1], bedrockError ); + assert.strictEqual(responseSenderSendErrorsMock.mock.calls.length, 1); + assert.deepStrictEqual( + responseSenderSendErrorsMock.mock.calls[0].arguments[0], + [ + { + errorType: 'Error', + message: 'Bedrock failed', + }, + ] + ); }); void it('logs and propagates error if response sender throws', async () => { @@ -156,11 +303,27 @@ void describe('Conversation turn executor', () => { () => Promise.reject(responseSenderError) ); + const streamResponseSenderSendResponseMock = mock.method( + responseSender, + 'sendResponseChunk', + () => Promise.resolve() + ); + + const responseSenderSendErrorsMock = mock.method( + responseSender, + 'sendErrors', + () => Promise.resolve() + ); + const consoleErrorMock = mock.fn(); const consoleLogMock = mock.fn(); + const consoleDebugMock = mock.fn(); + const consoleWarnMock = mock.fn(); const consoleMock = { error: consoleErrorMock, log: consoleLogMock, + debug: consoleDebugMock, + warn: consoleWarnMock, } as unknown as Console; await assert.rejects( @@ -168,8 +331,8 @@ void describe('Conversation turn executor', () => { new ConversationTurnExecutor( event, [], - bedrockConverseAdapter, - responseSender, + new Lazy(() => responseSender), + new Lazy(() => bedrockConverseAdapter), consoleMock ).execute(), (error: Error) => { @@ -182,6 +345,10 @@ void describe('Conversation turn executor', () => { bedrockConverseAdapterAskBedrockMock.mock.calls.length, 1 ); + assert.strictEqual( + streamResponseSenderSendResponseMock.mock.calls.length, + 0 + ); assert.strictEqual(responseSenderSendResponseMock.mock.calls.length, 1); assert.strictEqual(consoleLogMock.mock.calls.length, 1); @@ -199,5 +366,180 @@ void describe('Conversation turn executor', () => { consoleErrorMock.mock.calls[0].arguments[1], responseSenderError ); + assert.strictEqual(responseSenderSendErrorsMock.mock.calls.length, 1); + assert.deepStrictEqual( + responseSenderSendErrorsMock.mock.calls[0].arguments[0], + [ + { + errorType: 'Error', + message: 'Failed to send response', + }, + ] + ); + }); + + void it('throws original exception if error sender fails', async () => { + const bedrockConverseAdapter = new BedrockConverseAdapter(event, []); + const originalError = new Error('original error'); + mock.method(bedrockConverseAdapter, 'askBedrock', () => + Promise.reject(originalError) + ); + const responseSender = new ConversationTurnResponseSender(event); + mock.method(responseSender, 'sendResponse', () => Promise.resolve()); + + mock.method(responseSender, 'sendResponseChunk', () => Promise.resolve()); + + const responseSenderSendErrorsMock = mock.method( + responseSender, + 'sendErrors', + () => Promise.reject(new Error('sender error')) + ); + + const consoleErrorMock = mock.fn(); + const consoleLogMock = mock.fn(); + const consoleDebugMock = mock.fn(); + const consoleWarnMock = mock.fn(); + const consoleMock = { + error: consoleErrorMock, + log: consoleLogMock, + debug: consoleDebugMock, + warn: consoleWarnMock, + } as unknown as Console; + + await assert.rejects( + () => + new ConversationTurnExecutor( + event, + [], + new Lazy(() => responseSender), + new Lazy(() => bedrockConverseAdapter), + consoleMock + ).execute(), + (error: Error) => { + assert.strictEqual(error, originalError); + return true; + } + ); + + assert.strictEqual(responseSenderSendErrorsMock.mock.calls.length, 1); + assert.deepStrictEqual( + responseSenderSendErrorsMock.mock.calls[0].arguments[0], + [ + { + errorType: 'Error', + message: 'original error', + }, + ] + ); + }); + + void it('serializes unknown errors', async () => { + const bedrockConverseAdapter = new BedrockConverseAdapter(event, []); + const unknownError = { some: 'shape' }; + mock.method(bedrockConverseAdapter, 'askBedrock', () => + Promise.reject(unknownError) + ); + const responseSender = new ConversationTurnResponseSender(event); + mock.method(responseSender, 'sendResponse', () => Promise.resolve()); + + mock.method(responseSender, 'sendResponseChunk', () => Promise.resolve()); + + const responseSenderSendErrorsMock = mock.method( + responseSender, + 'sendErrors', + () => Promise.resolve() + ); + + const consoleErrorMock = mock.fn(); + const consoleLogMock = mock.fn(); + const consoleDebugMock = mock.fn(); + const consoleWarnMock = mock.fn(); + const consoleMock = { + error: consoleErrorMock, + log: consoleLogMock, + debug: consoleDebugMock, + warn: consoleWarnMock, + } as unknown as Console; + + await assert.rejects( + () => + new ConversationTurnExecutor( + event, + [], + new Lazy(() => responseSender), + new Lazy(() => bedrockConverseAdapter), + consoleMock + ).execute(), + (error: Error) => { + assert.strictEqual(error, unknownError); + return true; + } + ); + + assert.strictEqual(responseSenderSendErrorsMock.mock.calls.length, 1); + assert.deepStrictEqual( + responseSenderSendErrorsMock.mock.calls[0].arguments[0], + [ + { + errorType: 'UnknownError', + message: '{"some":"shape"}', + }, + ] + ); + }); + + void it('reports initialization errors', async () => { + const bedrockConverseAdapter = new BedrockConverseAdapter(event, []); + mock.method(bedrockConverseAdapter, 'askBedrock', () => Promise.resolve()); + const responseSender = new ConversationTurnResponseSender(event); + mock.method(responseSender, 'sendResponse', () => Promise.resolve()); + + mock.method(responseSender, 'sendResponseChunk', () => Promise.resolve()); + + const responseSenderSendErrorsMock = mock.method( + responseSender, + 'sendErrors', + () => Promise.resolve() + ); + + const consoleErrorMock = mock.fn(); + const consoleLogMock = mock.fn(); + const consoleDebugMock = mock.fn(); + const consoleWarnMock = mock.fn(); + const consoleMock = { + error: consoleErrorMock, + log: consoleLogMock, + debug: consoleDebugMock, + warn: consoleWarnMock, + } as unknown as Console; + + const initializationError = new Error('initialization error'); + await assert.rejects( + () => + new ConversationTurnExecutor( + event, + [], + new Lazy(() => responseSender), + new Lazy(() => { + throw initializationError; + }), + consoleMock + ).execute(), + (error: Error) => { + assert.strictEqual(error, initializationError); + return true; + } + ); + + assert.strictEqual(responseSenderSendErrorsMock.mock.calls.length, 1); + assert.deepStrictEqual( + responseSenderSendErrorsMock.mock.calls[0].arguments[0], + [ + { + errorType: 'Error', + message: 'initialization error', + }, + ] + ); }); }); diff --git a/packages/ai-constructs/src/conversation/runtime/conversation_turn_executor.ts b/packages/ai-constructs/src/conversation/runtime/conversation_turn_executor.ts index 79f87a05cf..9c5389f610 100644 --- a/packages/ai-constructs/src/conversation/runtime/conversation_turn_executor.ts +++ b/packages/ai-constructs/src/conversation/runtime/conversation_turn_executor.ts @@ -1,6 +1,7 @@ import { ConversationTurnResponseSender } from './conversation_turn_response_sender.js'; -import { ConversationTurnEvent, ExecutableTool } from './types.js'; +import { ConversationTurnEvent, ExecutableTool, JSONSchema } from './types.js'; import { BedrockConverseAdapter } from './bedrock_converse_adapter.js'; +import { Lazy } from './lazy'; /** * This class is responsible for orchestrating conversation turn execution. @@ -16,11 +17,13 @@ export class ConversationTurnExecutor { constructor( private readonly event: ConversationTurnEvent, additionalTools: Array, - private readonly bedrockConverseAdapter = new BedrockConverseAdapter( - event, - additionalTools + // We're deferring dependency initialization here so that we can capture all validation errors. + private readonly responseSender = new Lazy( + () => new ConversationTurnResponseSender(event) + ), + private readonly bedrockConverseAdapter = new Lazy( + () => new BedrockConverseAdapter(event, additionalTools) ), - private readonly responseSender = new ConversationTurnResponseSender(event), private readonly logger = console ) {} @@ -29,10 +32,18 @@ export class ConversationTurnExecutor { this.logger.log( `Handling conversation turn event, currentMessageId=${this.event.currentMessageId}, conversationId=${this.event.conversationId}` ); + this.logger.debug('Event received:', this.event); - const assistantResponse = await this.bedrockConverseAdapter.askBedrock(); - - await this.responseSender.sendResponse(assistantResponse); + if (this.event.streamResponse) { + const chunks = this.bedrockConverseAdapter.value.askBedrockStreaming(); + for await (const chunk of chunks) { + await this.responseSender.value.sendResponseChunk(chunk); + } + } else { + const assistantResponse = + await this.bedrockConverseAdapter.value.askBedrock(); + await this.responseSender.value.sendResponse(assistantResponse); + } this.logger.log( `Conversation turn event handled successfully, currentMessageId=${this.event.currentMessageId}, conversationId=${this.event.conversationId}` @@ -42,10 +53,28 @@ export class ConversationTurnExecutor { `Failed to handle conversation turn event, currentMessageId=${this.event.currentMessageId}, conversationId=${this.event.conversationId}`, e ); + await this.tryForwardError(e); // Propagate error to mark lambda execution as failed in metrics. throw e; } }; + + private tryForwardError = async (e: unknown) => { + try { + let errorType = 'UnknownError'; + let message: string; + if (e instanceof Error) { + errorType = e.name; + message = e.message; + } else { + message = JSON.stringify(e); + } + await this.responseSender.value.sendErrors([{ errorType, message }]); + } catch (e) { + // Best effort, only log the fact that we tried to send error back to AppSync. + this.logger.warn('Failed to send error mutation', e); + } + }; } /** @@ -54,7 +83,10 @@ export class ConversationTurnExecutor { */ export const handleConversationTurnEvent = async ( event: ConversationTurnEvent, - props?: { tools?: Array } + // This is by design, so that tools with different input types can be added + // to single arrays. Downstream code doesn't use these types. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + props?: { tools?: Array> } ): Promise => { await new ConversationTurnExecutor(event, props?.tools ?? []).execute(); }; diff --git a/packages/ai-constructs/src/conversation/runtime/conversation_turn_response_sender.test.ts b/packages/ai-constructs/src/conversation/runtime/conversation_turn_response_sender.test.ts index c7201ca1b5..32c579b237 100644 --- a/packages/ai-constructs/src/conversation/runtime/conversation_turn_response_sender.test.ts +++ b/packages/ai-constructs/src/conversation/runtime/conversation_turn_response_sender.test.ts @@ -1,16 +1,33 @@ import { describe, it, mock } from 'node:test'; import assert from 'node:assert'; -import { text } from 'node:stream/consumers'; -import { ConversationTurnResponseSender } from './conversation_turn_response_sender'; -import { ConversationTurnEvent } from './types'; +import { + ConversationTurnResponseSender, + MutationResponseInput, + MutationStreamingResponseInput, +} from './conversation_turn_response_sender'; +import { + ConversationTurnError, + ConversationTurnEvent, + StreamingResponseChunk, +} from './types'; import { ContentBlock } from '@aws-sdk/client-bedrock-runtime'; +import { + GraphqlRequest, + GraphqlRequestExecutor, +} from './graphql_request_executor'; +import { UserAgentProvider } from './user_agent_provider'; void describe('Conversation turn response sender', () => { const event: ConversationTurnEvent = { conversationId: 'testConversationId', currentMessageId: 'testCurrentMessageId', graphqlApiEndpoint: 'http://fake.endpoint/', - messages: [], + messageHistoryQuery: { + getQueryName: '', + getQueryInputTypeName: '', + listQueryName: '', + listQueryInputTypeName: '', + }, modelConfiguration: { modelId: '', systemPrompt: '' }, request: { headers: { authorization: 'testToken' } }, responseMutation: { @@ -21,13 +38,31 @@ void describe('Conversation turn response sender', () => { }; void it('sends response back to appsync', async () => { - const fetchMock = mock.fn( - fetch, - (): Promise => + const userAgentProvider = new UserAgentProvider( + {} as unknown as ConversationTurnEvent + ); + const userAgentProviderMock = mock.method( + userAgentProvider, + 'getUserAgent', + () => 'testUserAgent' + ); + const graphqlRequestExecutor = new GraphqlRequestExecutor( + '', + '', + userAgentProvider + ); + const executeGraphqlMock = mock.method( + graphqlRequestExecutor, + 'executeGraphql', + () => // Mock successful Appsync response - Promise.resolve(new Response('{}', { status: 200 })) + Promise.resolve() + ); + const sender = new ConversationTurnResponseSender( + event, + userAgentProvider, + graphqlRequestExecutor ); - const sender = new ConversationTurnResponseSender(event, fetchMock); const response: Array = [ { text: 'block1', @@ -36,20 +71,17 @@ void describe('Conversation turn response sender', () => { ]; await sender.sendResponse(response); - assert.strictEqual(fetchMock.mock.calls.length, 1); - const request: Request = fetchMock.mock.calls[0].arguments[0] as Request; - assert.strictEqual(request.url, event.graphqlApiEndpoint); - assert.strictEqual(request.method, 'POST'); - assert.strictEqual( - request.headers.get('Content-Type'), - 'application/graphql' - ); - assert.strictEqual( - request.headers.get('Authorization'), - event.request.headers.authorization - ); - assert.ok(request.body); - assert.deepStrictEqual(JSON.parse(await text(request.body)), { + assert.strictEqual(userAgentProviderMock.mock.calls.length, 1); + assert.deepStrictEqual(userAgentProviderMock.mock.calls[0].arguments[0], { + 'turn-response-type': 'single', + }); + assert.strictEqual(executeGraphqlMock.mock.calls.length, 1); + assert.deepStrictEqual(executeGraphqlMock.mock.calls[0].arguments[1], { + userAgent: 'testUserAgent', + }); + const request = executeGraphqlMock.mock.calls[0] + .arguments[0] as GraphqlRequest; + assert.deepStrictEqual(request, { query: '\n' + ' mutation PublishModelResponse($input: testResponseMutationInputTypeName!) {\n' + @@ -73,73 +105,153 @@ void describe('Conversation turn response sender', () => { }); }); - void it('throws if response is not 2xx', async () => { - const fetchMock = mock.fn( - fetch, - (): Promise => + void it('serializes tool use input to JSON', async () => { + const userAgentProvider = new UserAgentProvider( + {} as unknown as ConversationTurnEvent + ); + mock.method(userAgentProvider, 'getUserAgent', () => ''); + const graphqlRequestExecutor = new GraphqlRequestExecutor( + '', + '', + userAgentProvider + ); + const executeGraphqlMock = mock.method( + graphqlRequestExecutor, + 'executeGraphql', + () => // Mock successful Appsync response - Promise.resolve( - new Response('Body with error', { - status: 400, - headers: { testHeaderKey: 'testHeaderValue' }, - }) - ) - ); - const sender = new ConversationTurnResponseSender(event, fetchMock); - const response: Array = []; - await assert.rejects( - () => sender.sendResponse(response), - (error: Error) => { - assert.strictEqual( - error.message, - // eslint-disable-next-line spellcheck/spell-checker - 'Assistant response mutation request was not successful, response headers={"content-type":"text/plain;charset=UTF-8","testheaderkey":"testHeaderValue"}, body=Body with error' - ); - return true; - } + Promise.resolve() + ); + const sender = new ConversationTurnResponseSender( + event, + userAgentProvider, + graphqlRequestExecutor ); + const toolUseBlock: ContentBlock.ToolUseMember = { + toolUse: { + name: 'testTool', + toolUseId: 'testToolUseId', + input: { + testPropertyKey: 'testPropertyValue', + }, + }, + }; + const response: Array = [toolUseBlock]; + await sender.sendResponse(response); + + assert.strictEqual(executeGraphqlMock.mock.calls.length, 1); + const request = executeGraphqlMock.mock.calls[0] + .arguments[0] as GraphqlRequest; + assert.deepStrictEqual(request, { + query: + '\n' + + ' mutation PublishModelResponse($input: testResponseMutationInputTypeName!) {\n' + + ' testResponseMutationName(input: $input) {\n' + + ' testSelectionSet\n' + + ' }\n' + + ' }\n' + + ' ', + variables: { + input: { + conversationId: event.conversationId, + content: [ + { + toolUse: { + input: JSON.stringify(toolUseBlock.toolUse.input), + name: toolUseBlock.toolUse.name, + toolUseId: toolUseBlock.toolUse.toolUseId, + }, + }, + ], + associatedUserMessageId: event.currentMessageId, + }, + }, + }); }); - void it('throws if graphql returns errors', async () => { - const fetchMock = mock.fn( - fetch, - (): Promise => + void it('sends streaming response chunk back to appsync', async () => { + const userAgentProvider = new UserAgentProvider( + {} as unknown as ConversationTurnEvent + ); + const userAgentProviderMock = mock.method( + userAgentProvider, + 'getUserAgent', + () => 'testUserAgent' + ); + const graphqlRequestExecutor = new GraphqlRequestExecutor( + '', + '', + userAgentProvider + ); + const executeGraphqlMock = mock.method( + graphqlRequestExecutor, + 'executeGraphql', + () => // Mock successful Appsync response - Promise.resolve( - new Response( - JSON.stringify({ - errors: ['Some GQL error'], - }), - { - status: 200, - headers: { testHeaderKey: 'testHeaderValue' }, - } - ) - ) - ); - const sender = new ConversationTurnResponseSender(event, fetchMock); - const response: Array = []; - await assert.rejects( - () => sender.sendResponse(response), - (error: Error) => { - assert.strictEqual( - error.message, - // eslint-disable-next-line spellcheck/spell-checker - 'Assistant response mutation request was not successful, response headers={"content-type":"text/plain;charset=UTF-8","testheaderkey":"testHeaderValue"}, body={"errors":["Some GQL error"]}' - ); - return true; - } + Promise.resolve() ); + const sender = new ConversationTurnResponseSender( + event, + userAgentProvider, + graphqlRequestExecutor + ); + const chunk: StreamingResponseChunk = { + accumulatedTurnContent: [{ text: 'testAccumulatedMessageContent' }], + associatedUserMessageId: 'testAssociatedUserMessageId', + contentBlockIndex: 1, + contentBlockDeltaIndex: 2, + conversationId: 'testConversationId', + contentBlockText: 'testBlockText', + }; + await sender.sendResponseChunk(chunk); + + assert.strictEqual(userAgentProviderMock.mock.calls.length, 1); + assert.deepStrictEqual(userAgentProviderMock.mock.calls[0].arguments[0], { + 'turn-response-type': 'streaming', + }); + assert.strictEqual(executeGraphqlMock.mock.calls.length, 1); + assert.deepStrictEqual(executeGraphqlMock.mock.calls[0].arguments[1], { + userAgent: 'testUserAgent', + }); + const request = executeGraphqlMock.mock.calls[0] + .arguments[0] as GraphqlRequest; + assert.deepStrictEqual(request, { + query: + '\n' + + ' mutation PublishModelResponse($input: testResponseMutationInputTypeName!) {\n' + + ' testResponseMutationName(input: $input) {\n' + + ' testSelectionSet\n' + + ' }\n' + + ' }\n' + + ' ', + variables: { + input: chunk, + }, + }); }); - void it('serializes tool use input to JSON', async () => { - const fetchMock = mock.fn( - fetch, - (): Promise => + void it('serializes tool use input to JSON when streaming', async () => { + const userAgentProvider = new UserAgentProvider( + {} as unknown as ConversationTurnEvent + ); + mock.method(userAgentProvider, 'getUserAgent', () => ''); + const graphqlRequestExecutor = new GraphqlRequestExecutor( + '', + '', + userAgentProvider + ); + const executeGraphqlMock = mock.method( + graphqlRequestExecutor, + 'executeGraphql', + () => // Mock successful Appsync response - Promise.resolve(new Response('{}', { status: 200 })) + Promise.resolve() + ); + const sender = new ConversationTurnResponseSender( + event, + userAgentProvider, + graphqlRequestExecutor ); - const sender = new ConversationTurnResponseSender(event, fetchMock); const toolUseBlock: ContentBlock.ToolUseMember = { toolUse: { name: 'testTool', @@ -149,13 +261,20 @@ void describe('Conversation turn response sender', () => { }, }, }; - const response: Array = [toolUseBlock]; - await sender.sendResponse(response); + const chunk: StreamingResponseChunk = { + accumulatedTurnContent: [toolUseBlock], + associatedUserMessageId: 'testAssociatedUserMessageId', + contentBlockIndex: 1, + contentBlockDeltaIndex: 2, + conversationId: 'testConversationId', + contentBlockText: 'testBlockText', + }; + await sender.sendResponseChunk(chunk); - assert.strictEqual(fetchMock.mock.calls.length, 1); - const request: Request = fetchMock.mock.calls[0].arguments[0] as Request; - assert.ok(request.body); - assert.deepStrictEqual(JSON.parse(await text(request.body)), { + assert.strictEqual(executeGraphqlMock.mock.calls.length, 1); + const request = executeGraphqlMock.mock.calls[0] + .arguments[0] as GraphqlRequest; + assert.deepStrictEqual(request, { query: '\n' + ' mutation PublishModelResponse($input: testResponseMutationInputTypeName!) {\n' + @@ -166,8 +285,8 @@ void describe('Conversation turn response sender', () => { ' ', variables: { input: { - conversationId: event.conversationId, - content: [ + ...chunk, + accumulatedTurnContent: [ { toolUse: { input: JSON.stringify(toolUseBlock.toolUse.input), @@ -176,6 +295,82 @@ void describe('Conversation turn response sender', () => { }, }, ], + }, + }, + }); + }); + + void it('sends errors response back to appsync', async () => { + const userAgentProvider = new UserAgentProvider( + {} as unknown as ConversationTurnEvent + ); + const userAgentProviderMock = mock.method( + userAgentProvider, + 'getUserAgent', + () => 'testUserAgent' + ); + const graphqlRequestExecutor = new GraphqlRequestExecutor( + '', + '', + userAgentProvider + ); + const executeGraphqlMock = mock.method( + graphqlRequestExecutor, + 'executeGraphql', + () => + // Mock successful Appsync response + Promise.resolve() + ); + const sender = new ConversationTurnResponseSender( + event, + userAgentProvider, + graphqlRequestExecutor + ); + const errors: Array = [ + { + errorType: 'errorType1', + message: 'errorMessage1', + }, + { + errorType: 'errorType2', + message: 'errorMessage2', + }, + ]; + await sender.sendErrors(errors); + + assert.strictEqual(userAgentProviderMock.mock.calls.length, 1); + assert.deepStrictEqual(userAgentProviderMock.mock.calls[0].arguments[0], { + 'turn-response-type': 'error', + }); + assert.strictEqual(executeGraphqlMock.mock.calls.length, 1); + assert.deepStrictEqual(executeGraphqlMock.mock.calls[0].arguments[1], { + userAgent: 'testUserAgent', + }); + assert.strictEqual(executeGraphqlMock.mock.calls.length, 1); + const request = executeGraphqlMock.mock.calls[0] + .arguments[0] as GraphqlRequest; + assert.deepStrictEqual(request, { + query: + '\n' + + ' mutation PublishModelResponse($input: testResponseMutationInputTypeName!) {\n' + + ' testResponseMutationName(input: $input) {\n' + + ' testSelectionSet\n' + + ' }\n' + + ' }\n' + + ' ', + variables: { + input: { + conversationId: event.conversationId, + errors: [ + { + errorType: 'errorType1', + message: 'errorMessage1', + }, + { + errorType: 'errorType2', + message: 'errorMessage2', + }, + ], associatedUserMessageId: event.currentMessageId, }, }, diff --git a/packages/ai-constructs/src/conversation/runtime/conversation_turn_response_sender.ts b/packages/ai-constructs/src/conversation/runtime/conversation_turn_response_sender.ts index 9fe979aac8..5892b6747c 100644 --- a/packages/ai-constructs/src/conversation/runtime/conversation_turn_response_sender.ts +++ b/packages/ai-constructs/src/conversation/runtime/conversation_turn_response_sender.ts @@ -1,7 +1,13 @@ -import { ConversationTurnEvent } from './types.js'; +import { + ConversationTurnError, + ConversationTurnEvent, + StreamingResponseChunk, +} from './types.js'; import type { ContentBlock } from '@aws-sdk/client-bedrock-runtime'; +import { GraphqlRequestExecutor } from './graphql_request_executor'; +import { UserAgentProvider } from './user_agent_provider'; -type MutationResponseInput = { +export type MutationResponseInput = { input: { conversationId: string; content: ContentBlock[]; @@ -9,6 +15,18 @@ type MutationResponseInput = { }; }; +export type MutationStreamingResponseInput = { + input: StreamingResponseChunk; +}; + +export type MutationErrorsResponseInput = { + input: { + conversationId: string; + errors: ConversationTurnError[]; + associatedUserMessageId: string; + }; +}; + /** * This class is responsible for sending a response produced by Bedrock back to AppSync * in a form of mutation. @@ -19,30 +37,73 @@ export class ConversationTurnResponseSender { */ constructor( private readonly event: ConversationTurnEvent, - private readonly _fetch = fetch + private readonly userAgentProvider = new UserAgentProvider(event), + private readonly graphqlRequestExecutor = new GraphqlRequestExecutor( + event.graphqlApiEndpoint, + event.request.headers.authorization, + userAgentProvider + ), + private readonly logger = console ) {} sendResponse = async (message: ContentBlock[]) => { - const request = this.createMutationRequest(message); - const res = await this._fetch(request); - const responseHeaders: Record = {}; - res.headers.forEach((value, key) => (responseHeaders[key] = value)); - if (!res.ok) { - const body = await res.text(); - throw new Error( - `Assistant response mutation request was not successful, response headers=${JSON.stringify( - responseHeaders - )}, body=${body}` - ); - } - const body = await res.json(); - if (body && typeof body === 'object' && 'errors' in body) { - throw new Error( - `Assistant response mutation request was not successful, response headers=${JSON.stringify( - responseHeaders - )}, body=${JSON.stringify(body)}` - ); - } + const responseMutationRequest = this.createMutationRequest(message); + this.logger.debug('Sending response mutation:', responseMutationRequest); + await this.graphqlRequestExecutor.executeGraphql< + MutationResponseInput, + void + >(responseMutationRequest, { + userAgent: this.userAgentProvider.getUserAgent({ + 'turn-response-type': 'single', + }), + }); + }; + + sendResponseChunk = async (chunk: StreamingResponseChunk) => { + const responseMutationRequest = this.createStreamingMutationRequest(chunk); + this.logger.debug('Sending response mutation:', responseMutationRequest); + await this.graphqlRequestExecutor.executeGraphql< + MutationStreamingResponseInput, + void + >(responseMutationRequest, { + userAgent: this.userAgentProvider.getUserAgent({ + 'turn-response-type': 'streaming', + }), + }); + }; + + sendErrors = async (errors: ConversationTurnError[]) => { + const responseMutationRequest = this.createMutationErrorsRequest(errors); + this.logger.debug( + 'Sending errors response mutation:', + responseMutationRequest + ); + await this.graphqlRequestExecutor.executeGraphql< + MutationErrorsResponseInput, + void + >(responseMutationRequest, { + userAgent: this.userAgentProvider.getUserAgent({ + 'turn-response-type': 'error', + }), + }); + }; + + private createMutationErrorsRequest = (errors: ConversationTurnError[]) => { + const query = ` + mutation PublishModelResponse($input: ${this.event.responseMutation.inputTypeName}!) { + ${this.event.responseMutation.name}(input: $input) { + ${this.event.responseMutation.selectionSet} + } + } + `; + const variables: MutationErrorsResponseInput = { + input: { + conversationId: this.event.conversationId, + errors, + associatedUserMessageId: this.event.currentMessageId, + }, + }; + return { query, variables }; }; private createMutationRequest = (content: ContentBlock[]) => { @@ -53,7 +114,39 @@ export class ConversationTurnResponseSender { } } `; - content = content.map((block) => { + content = this.serializeContent(content); + const variables: MutationResponseInput = { + input: { + conversationId: this.event.conversationId, + content, + associatedUserMessageId: this.event.currentMessageId, + }, + }; + return { query, variables }; + }; + + private createStreamingMutationRequest = (chunk: StreamingResponseChunk) => { + const query = ` + mutation PublishModelResponse($input: ${this.event.responseMutation.inputTypeName}!) { + ${this.event.responseMutation.name}(input: $input) { + ${this.event.responseMutation.selectionSet} + } + } + `; + chunk = { + ...chunk, + accumulatedTurnContent: this.serializeContent( + chunk.accumulatedTurnContent + ), + }; + const variables: MutationStreamingResponseInput = { + input: chunk, + }; + return { query, variables }; + }; + + private serializeContent = (content: ContentBlock[]) => { + return content.map((block) => { if (block.toolUse) { // The `input` field is typed as `AWS JSON` in the GraphQL API because it can represent // arbitrary JSON values. @@ -63,20 +156,5 @@ export class ConversationTurnResponseSender { } return block; }); - const variables: MutationResponseInput = { - input: { - conversationId: this.event.conversationId, - content, - associatedUserMessageId: this.event.currentMessageId, - }, - }; - return new Request(this.event.graphqlApiEndpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/graphql', - Authorization: this.event.request.headers.authorization, - }, - body: JSON.stringify({ query, variables }), - }); }; } diff --git a/packages/ai-constructs/src/conversation/runtime/errors.ts b/packages/ai-constructs/src/conversation/runtime/errors.ts new file mode 100644 index 0000000000..1d3063dd49 --- /dev/null +++ b/packages/ai-constructs/src/conversation/runtime/errors.ts @@ -0,0 +1,12 @@ +/** + * Represents validation errors. + */ +export class ValidationError extends Error { + /** + * Creates validation error instance. + */ + constructor(message: string) { + super(message); + this.name = 'ValidationError'; + } +} diff --git a/packages/ai-constructs/src/conversation/runtime/event-tools-provider/event_tools_provider.test.ts b/packages/ai-constructs/src/conversation/runtime/event-tools-provider/event_tools_provider.test.ts index 12bea0403e..df63abd33d 100644 --- a/packages/ai-constructs/src/conversation/runtime/event-tools-provider/event_tools_provider.test.ts +++ b/packages/ai-constructs/src/conversation/runtime/event-tools-provider/event_tools_provider.test.ts @@ -11,7 +11,12 @@ void describe('events tool provider', () => { conversationId: '', currentMessageId: '', graphqlApiEndpoint: '', - messages: [], + messageHistoryQuery: { + getQueryName: '', + getQueryInputTypeName: '', + listQueryName: '', + listQueryInputTypeName: '', + }, modelConfiguration: { modelId: '', systemPrompt: '' }, request: { headers: { authorization: '' } }, responseMutation: { @@ -30,14 +35,17 @@ void describe('events tool provider', () => { description: 'toolDescription1', inputSchema: { json: { - tool1: 'value1', + type: 'object', + properties: { + tool1Property: { type: 'string' }, + }, }, }, graphqlRequestInputDescriptor: { queryName: 'queryName1', selectionSet: 'selection1', propertyTypes: { - property1: 'type1', + tool1Property: 'type1', }, }, }; @@ -46,14 +54,17 @@ void describe('events tool provider', () => { description: 'toolDescription2', inputSchema: { json: { - tool1: 'value2', + type: 'object', + properties: { + tool2Property: { type: 'string' }, + }, }, }, graphqlRequestInputDescriptor: { queryName: 'queryName2', selectionSet: 'selection2', propertyTypes: { - property1: 'type2', + tool2Property: 'type2', }, }, }; @@ -61,7 +72,12 @@ void describe('events tool provider', () => { conversationId: '', currentMessageId: '', graphqlApiEndpoint: '', - messages: [], + messageHistoryQuery: { + getQueryName: '', + getQueryInputTypeName: '', + listQueryName: '', + listQueryInputTypeName: '', + }, modelConfiguration: { modelId: '', systemPrompt: '' }, request: { headers: { authorization: '' } }, responseMutation: { diff --git a/packages/ai-constructs/src/conversation/runtime/event-tools-provider/event_tools_provider.ts b/packages/ai-constructs/src/conversation/runtime/event-tools-provider/event_tools_provider.ts index c008e7905d..19ff56dc56 100644 --- a/packages/ai-constructs/src/conversation/runtime/event-tools-provider/event_tools_provider.ts +++ b/packages/ai-constructs/src/conversation/runtime/event-tools-provider/event_tools_provider.ts @@ -1,6 +1,7 @@ import { ConversationTurnEvent, ExecutableTool } from '../types'; import { GraphQlTool } from './graphql_tool'; import { GraphQlQueryFactory } from './graphql_query_factory'; +import { UserAgentProvider } from '../user_agent_provider'; /** * Creates executable tools from definitions in conversation turn event. @@ -28,7 +29,8 @@ export class ConversationTurnEventToolsProvider { inputSchema, graphqlApiEndpoint, query, - this.event.request.headers.authorization + this.event.request.headers.authorization, + new UserAgentProvider(this.event) ); }); return tools ?? []; diff --git a/packages/ai-constructs/src/conversation/runtime/event-tools-provider/graphql_tool.test.ts b/packages/ai-constructs/src/conversation/runtime/event-tools-provider/graphql_tool.test.ts index 9989429ef5..d764dca508 100644 --- a/packages/ai-constructs/src/conversation/runtime/event-tools-provider/graphql_tool.test.ts +++ b/packages/ai-constructs/src/conversation/runtime/event-tools-provider/graphql_tool.test.ts @@ -1,14 +1,26 @@ import { describe, it, mock } from 'node:test'; import assert from 'node:assert'; -import { text } from 'node:stream/consumers'; import { GraphQlTool } from './graphql_tool'; +import { + GraphqlRequest, + GraphqlRequestExecutor, +} from '../graphql_request_executor'; +import { DocumentType } from '@smithy/types'; +import { UserAgentProvider } from '../user_agent_provider'; +import { ConversationTurnEvent } from '../types'; void describe('GraphQl tool', () => { const graphQlEndpoint = 'http://test.endpoint/'; const query = 'testQuery'; const accessToken = 'testAccessToken'; + const userAgentProvider = new UserAgentProvider( + {} as unknown as ConversationTurnEvent + ); + mock.method(userAgentProvider, 'getUserAgent', () => ''); - const createGraphQlTool = (fetchMock: typeof fetch): GraphQlTool => { + const createGraphQlTool = ( + graphqlRequestExecutor: GraphqlRequestExecutor + ): GraphQlTool => { return new GraphQlTool( 'testName', 'testDescription', @@ -16,7 +28,8 @@ void describe('GraphQl tool', () => { graphQlEndpoint, query, accessToken, - fetchMock + userAgentProvider, + graphqlRequestExecutor ); }; @@ -24,28 +37,25 @@ void describe('GraphQl tool', () => { const testResponse = { test: 'response', }; - const fetchMock = mock.fn( - fetch, - (): Promise => + const graphqlRequestExecutor = new GraphqlRequestExecutor( + '', + '', + userAgentProvider + ); + const executeGraphqlMock = mock.method( + graphqlRequestExecutor, + 'executeGraphql', + () => // Mock successful Appsync response - Promise.resolve( - new Response(JSON.stringify(testResponse), { status: 200 }) - ) + Promise.resolve(testResponse) ); - const tool = createGraphQlTool(fetchMock); + const tool = createGraphQlTool(graphqlRequestExecutor); const toolResult = await tool.execute({ test: 'input' }); - assert.strictEqual(fetchMock.mock.calls.length, 1); - const request: Request = fetchMock.mock.calls[0].arguments[0] as Request; - assert.strictEqual(request.url, graphQlEndpoint); - assert.strictEqual(request.method, 'POST'); - assert.strictEqual( - request.headers.get('Content-Type'), - 'application/graphql' - ); - assert.strictEqual(request.headers.get('Authorization'), accessToken); - assert.ok(request.body); - assert.deepStrictEqual(JSON.parse(await text(request.body)), { + assert.strictEqual(executeGraphqlMock.mock.calls.length, 1); + const request = executeGraphqlMock.mock.calls[0] + .arguments[0] as GraphqlRequest; + assert.deepStrictEqual(request, { query: 'testQuery', variables: { test: 'input', @@ -57,61 +67,4 @@ void describe('GraphQl tool', () => { }, }); }); - - void it('throws if response is not 2xx', async () => { - const fetchMock = mock.fn( - fetch, - (): Promise => - // Mock successful Appsync response - Promise.resolve( - new Response('Body with error', { - status: 400, - headers: { testHeaderKey: 'testHeaderValue' }, - }) - ) - ); - const tool = createGraphQlTool(fetchMock); - await assert.rejects( - () => tool.execute({ test: 'input' }), - (error: Error) => { - assert.strictEqual( - error.message, - // eslint-disable-next-line spellcheck/spell-checker - 'GraphQl tool \'testName\' failed, response headers={"content-type":"text/plain;charset=UTF-8","testheaderkey":"testHeaderValue"}, body=Body with error' - ); - return true; - } - ); - }); - - void it('throws if graphql returns errors', async () => { - const fetchMock = mock.fn( - fetch, - (): Promise => - // Mock successful Appsync response - Promise.resolve( - new Response( - JSON.stringify({ - errors: ['Some GQL error'], - }), - { - status: 200, - headers: { testHeaderKey: 'testHeaderValue' }, - } - ) - ) - ); - const tool = createGraphQlTool(fetchMock); - await assert.rejects( - () => tool.execute({ test: 'input' }), - (error: Error) => { - assert.strictEqual( - error.message, - // eslint-disable-next-line spellcheck/spell-checker - 'GraphQl tool \'testName\' failed, response headers={"content-type":"text/plain;charset=UTF-8","testheaderkey":"testHeaderValue"}, body={"errors":["Some GQL error"]}' - ); - return true; - } - ); - }); }); diff --git a/packages/ai-constructs/src/conversation/runtime/event-tools-provider/graphql_tool.ts b/packages/ai-constructs/src/conversation/runtime/event-tools-provider/graphql_tool.ts index a6f9cce949..dcd37368a3 100644 --- a/packages/ai-constructs/src/conversation/runtime/event-tools-provider/graphql_tool.ts +++ b/packages/ai-constructs/src/conversation/runtime/event-tools-provider/graphql_tool.ts @@ -1,65 +1,45 @@ -import { ExecutableTool } from '../types'; -import type { - ToolInputSchema, - ToolResultContentBlock, -} from '@aws-sdk/client-bedrock-runtime'; +import { ExecutableTool, JSONSchema, ToolInputSchema } from '../types'; +import type { ToolResultContentBlock } from '@aws-sdk/client-bedrock-runtime'; import { DocumentType } from '@smithy/types'; +import { GraphqlRequestExecutor } from '../graphql_request_executor'; +import { UserAgentProvider } from '../user_agent_provider'; /** * A tool that use GraphQl queries. */ -export class GraphQlTool implements ExecutableTool { +export class GraphQlTool implements ExecutableTool { /** * Creates GraphQl Tool */ constructor( public name: string, public description: string, - public inputSchema: ToolInputSchema, - private readonly graphQlEndpoint: string, + public inputSchema: ToolInputSchema, + readonly graphQlEndpoint: string, private readonly query: string, - private readonly accessToken: string, - private readonly _fetch = fetch + readonly accessToken: string, + readonly userAgentProvider: UserAgentProvider, + private readonly graphqlRequestExecutor = new GraphqlRequestExecutor( + graphQlEndpoint, + accessToken, + userAgentProvider + ) ) {} execute = async ( - input: DocumentType | undefined + input: unknown | undefined ): Promise => { if (!input) { throw Error(`GraphQl tool '${this.name}' requires input to execute.`); } - const options: RequestInit = { - method: 'POST', - headers: { - 'Content-Type': 'application/graphql', - Authorization: this.accessToken, - }, - body: JSON.stringify({ query: this.query, variables: input }), - }; - - const req = new Request(this.graphQlEndpoint, options); - const res = await this._fetch(req); - - const responseHeaders: Record = {}; - res.headers.forEach((value, key) => (responseHeaders[key] = value)); - if (!res.ok) { - const body = await res.text(); - throw new Error( - `GraphQl tool '${this.name}' failed, response headers=${JSON.stringify( - responseHeaders - )}, body=${body}` - ); - } - const body = await res.json(); - if (body && typeof body === 'object' && 'errors' in body) { - throw new Error( - `GraphQl tool '${this.name}' failed, response headers=${JSON.stringify( - responseHeaders - )}, body=${JSON.stringify(body)}` - ); - } - - return { json: body as DocumentType }; + const response = await this.graphqlRequestExecutor.executeGraphql< + unknown, + DocumentType + >({ + query: this.query, + variables: input, + }); + return { json: response }; }; } diff --git a/packages/ai-constructs/src/conversation/runtime/executable_tool_factory.test.ts b/packages/ai-constructs/src/conversation/runtime/executable_tool_factory.test.ts new file mode 100644 index 0000000000..b5ae5f701e --- /dev/null +++ b/packages/ai-constructs/src/conversation/runtime/executable_tool_factory.test.ts @@ -0,0 +1,164 @@ +import { describe, it, mock } from 'node:test'; +import assert from 'node:assert'; +import ts from 'typescript'; +import path from 'path'; +import { createExecutableTool } from './executable_tool_factory'; +import { ToolResultContentBlock } from './types'; + +/** + * This function compiles a TypeScript snippet in memory. + * Inspired by https://stackoverflow.com/questions/53733138/how-do-i-type-check-a-snippet-of-typescript-code-in-memory + */ +const compileInMemory = (rootDir: string, text: string) => { + const options = ts.getDefaultCompilerOptions(); + options.strict = true; + const inMemoryFilePath = path.resolve(path.join(rootDir, '__dummy-file.ts')); + const textAst = ts.createSourceFile( + inMemoryFilePath, + text, + options.target || ts.ScriptTarget.Latest + ); + const host = ts.createCompilerHost(options, true); + + const overrideIfInMemoryFile = ( + methodName: 'getSourceFile' | 'readFile' | 'fileExists', + inMemoryValue: unknown + ) => { + // This is intentional, we don't care about function signature, we just + // want to intercept it. + // eslint-disable-next-line @typescript-eslint/ban-types + const originalMethod: Function = host[methodName]; + mock.method(host, methodName, (...args: unknown[]) => { + // resolve the path because typescript will normalize it + // to forward slashes on windows + const filePath = path.resolve(args[0] as string); + if (filePath === inMemoryFilePath) return inMemoryValue; + return originalMethod.apply(host, args); + }); + }; + + overrideIfInMemoryFile('getSourceFile', textAst); + overrideIfInMemoryFile('readFile', text); + overrideIfInMemoryFile('fileExists', true); + + const program = ts.createProgram({ + options, + rootNames: [inMemoryFilePath], + host, + }); + + return ts.getPreEmitDiagnostics(program, textAst); +}; + +void describe('Executable Tool Factory', () => { + void it('creates a functional executable tool', async () => { + const toolName = 'testToolName'; + const toolDescription = 'testToolDescription'; + const inputSchema = { + type: 'object', + properties: { + testProperty: { type: 'string' }, + }, + required: ['testProperty'], + } as const; + type TypeMatchingSchema = { + testProperty: string; + }; + const tool = createExecutableTool( + toolName, + toolDescription, + { json: inputSchema }, + async (input) => { + const inputText: string = input.testProperty; + return { + text: inputText, + } satisfies ToolResultContentBlock; + } + ); + assert.strictEqual(tool.name, toolName); + assert.strictEqual(tool.description, toolDescription); + assert.deepStrictEqual(tool.inputSchema.json, inputSchema); + const input1: TypeMatchingSchema = { + testProperty: 'testPropertyValue1', + }; + const output1 = await tool.execute(input1); + assert.strictEqual(output1.text, input1.testProperty); + const input2: TypeMatchingSchema = { + testProperty: 'testPropertyValue2', + }; + const output2 = await tool.execute(input2); + assert.strictEqual(output2.text, input2.testProperty); + }); + + void it('fails compilation if unknown property is used', () => { + const sourceSnippet = ` + import { createExecutableTool } from './executable_tool_factory'; + import { ToolResultContentBlock } from './types'; + + const inputSchema = { + type: 'object', + properties: { + testProperty: { type: 'string' }, + }, + required: ['testProperty'], + } as const; + + createExecutableTool( + 'testName', + 'testDescription', + { json: inputSchema }, + async (input) => { + // This should trigger compiler as properties not in schema are 'unknown'. + const someNonExistingPropertyValue: string = input.someNonExistingProperty; + return { + text: 'testResultText', + } satisfies ToolResultContentBlock; + } + ); +`; + + const diagnostics = compileInMemory(__dirname, sourceSnippet); + assert.strictEqual(1, diagnostics.length); + // Properties not in schema are 'unknown'. + assert.strictEqual( + diagnostics[0].messageText, + "Type 'unknown' is not assignable to type 'string'." + ); + }); + + void it('allows overriding input type', () => { + const sourceSnippet = ` + import { createExecutableTool } from './executable_tool_factory'; + import { ToolResultContentBlock } from './types'; + + const inputSchema = { + type: 'object', + properties: { + testProperty: { type: 'string' }, + }, + required: ['testProperty'], + } as const; + + type OverrideInputType = { + someOverriddenProperty: string + } + + createExecutableTool( + 'testName', + 'testDescription', + { json: inputSchema }, + async (input) => { + // This should not trigger compiler because type is overridden. + const someOverriddenPropertyValue: string = input.someOverriddenProperty; + return { + text: 'testResultText', + } satisfies ToolResultContentBlock; + } + ); +`; + + const diagnostics = compileInMemory(__dirname, sourceSnippet); + // Assert that compiler is happy. + assert.strictEqual(0, diagnostics.length); + }); +}); diff --git a/packages/ai-constructs/src/conversation/runtime/executable_tool_factory.ts b/packages/ai-constructs/src/conversation/runtime/executable_tool_factory.ts new file mode 100644 index 0000000000..8b5186ba4d --- /dev/null +++ b/packages/ai-constructs/src/conversation/runtime/executable_tool_factory.ts @@ -0,0 +1,32 @@ +import { + ExecutableTool, + FromJSONSchema, + JSONSchema, + ToolInputSchema, +} from './types'; +import * as bedrock from '@aws-sdk/client-bedrock-runtime'; + +/** + * Creates an executable tool. + */ +export const createExecutableTool: < + TJSONSchema extends JSONSchema = JSONSchema, + TToolInput = FromJSONSchema +>( + name: string, + description: string, + inputSchema: ToolInputSchema, + handler: (input: TToolInput) => Promise +) => ExecutableTool = ( + name, + description, + inputSchema, + handler +) => { + return { + name, + description, + inputSchema, + execute: handler, + }; +}; diff --git a/packages/ai-constructs/src/conversation/runtime/graphql_request_executor.test.ts b/packages/ai-constructs/src/conversation/runtime/graphql_request_executor.test.ts new file mode 100644 index 0000000000..fe605b2171 --- /dev/null +++ b/packages/ai-constructs/src/conversation/runtime/graphql_request_executor.test.ts @@ -0,0 +1,165 @@ +import { describe, it, mock } from 'node:test'; +import assert from 'node:assert'; +import { text } from 'node:stream/consumers'; +import { GraphqlRequestExecutor } from './graphql_request_executor'; +import { UserAgentProvider } from './user_agent_provider'; +import { ConversationTurnEvent } from './types'; + +void describe('Graphql executor test', () => { + const graphqlEndpoint = 'http://fake.endpoint/'; + const accessToken = 'testToken'; + const userAgent = 'testUserAgent'; + const userAgentProvider = new UserAgentProvider( + {} as unknown as ConversationTurnEvent + ); + mock.method(userAgentProvider, 'getUserAgent', () => userAgent); + + void it('sends request to appsync', async () => { + const fetchMock = mock.fn( + fetch, + (): Promise => + // Mock successful Appsync response + Promise.resolve(new Response('{}', { status: 200 })) + ); + const executor = new GraphqlRequestExecutor( + graphqlEndpoint, + accessToken, + userAgentProvider, + fetchMock + ); + const query = 'testQuery'; + const variables = { + testVariableKey: 'testVariableValue', + }; + await executor.executeGraphql({ + query, + variables, + }); + + assert.strictEqual(fetchMock.mock.calls.length, 1); + const request: Request = fetchMock.mock.calls[0].arguments[0] as Request; + assert.strictEqual(request.url, graphqlEndpoint); + assert.strictEqual(request.method, 'POST'); + assert.strictEqual( + request.headers.get('Content-Type'), + 'application/graphql' + ); + assert.strictEqual(request.headers.get('Authorization'), accessToken); + assert.strictEqual(request.headers.get('x-amz-user-agent'), userAgent); + assert.ok(request.body); + assert.deepStrictEqual(JSON.parse(await text(request.body)), { + query: 'testQuery', + variables: { testVariableKey: 'testVariableValue' }, + }); + }); + + void it('method provided user agent takes precedence', async () => { + const fetchMock = mock.fn( + fetch, + (): Promise => + // Mock successful Appsync response + Promise.resolve(new Response('{}', { status: 200 })) + ); + const executor = new GraphqlRequestExecutor( + graphqlEndpoint, + accessToken, + userAgentProvider, + fetchMock + ); + const query = 'testQuery'; + const variables = { + testVariableKey: 'testVariableValue', + }; + await executor.executeGraphql( + { + query, + variables, + }, + { + userAgent: 'methodScopedUserAgent', + } + ); + + assert.strictEqual(fetchMock.mock.calls.length, 1); + const request: Request = fetchMock.mock.calls[0].arguments[0] as Request; + assert.strictEqual( + request.headers.get('x-amz-user-agent'), + 'methodScopedUserAgent' + ); + }); + + void it('throws if response is not 2xx', async () => { + const fetchMock = mock.fn( + fetch, + (): Promise => + // Mock successful Appsync response + Promise.resolve( + new Response('Body with error', { + status: 400, + headers: { testHeaderKey: 'testHeaderValue' }, + }) + ) + ); + const executor = new GraphqlRequestExecutor( + graphqlEndpoint, + accessToken, + userAgentProvider, + fetchMock + ); + const query = 'testQuery'; + const variables = { + testVariableKey: 'testVariableValue', + }; + await assert.rejects( + () => executor.executeGraphql({ query, variables }), + (error: Error) => { + assert.strictEqual( + error.message, + // eslint-disable-next-line spellcheck/spell-checker + 'GraphQL request failed, response headers={"content-type":"text/plain;charset=UTF-8","testheaderkey":"testHeaderValue"}, body=Body with error' + ); + return true; + } + ); + }); + + void it('throws if graphql returns errors', async () => { + const fetchMock = mock.fn( + fetch, + (): Promise => + // Mock successful Appsync response + Promise.resolve( + new Response( + JSON.stringify({ + errors: ['Some GQL error'], + }), + { + status: 200, + headers: { testHeaderKey: 'testHeaderValue' }, + } + ) + ) + ); + const executor = new GraphqlRequestExecutor( + graphqlEndpoint, + accessToken, + userAgentProvider, + fetchMock + ); + const query = 'testQuery'; + const variables = { + testVariableKey: 'testVariableValue', + }; + await assert.rejects( + () => executor.executeGraphql({ query, variables }), + (error: Error) => { + assert.strictEqual( + error.message, + // eslint-disable-next-line spellcheck/spell-checker + 'GraphQL request failed, response headers={"content-type":"text/plain;charset=UTF-8","testheaderkey":"testHeaderValue"}, body={"errors":["Some GQL error"]}' + ); + return true; + } + ); + }); +}); diff --git a/packages/ai-constructs/src/conversation/runtime/graphql_request_executor.ts b/packages/ai-constructs/src/conversation/runtime/graphql_request_executor.ts new file mode 100644 index 0000000000..60f1af44bb --- /dev/null +++ b/packages/ai-constructs/src/conversation/runtime/graphql_request_executor.ts @@ -0,0 +1,65 @@ +import { UserAgentProvider } from './user_agent_provider'; + +export type GraphqlRequest = { + query: string; + variables: TVariables; +}; + +/** + * This class is responsible for executing GraphQL requests. + * Serializing query and it's inputs, adding authorization headers, + * inspecting response for errors and de-serializing output. + */ +export class GraphqlRequestExecutor { + /** + * Creates GraphQL request executor. + */ + constructor( + private readonly graphQlEndpoint: string, + private readonly accessToken: string, + private readonly userAgentProvider: UserAgentProvider, + private readonly _fetch = fetch + ) {} + + executeGraphql = async ( + request: GraphqlRequest, + options?: { + userAgent?: string; + } + ): Promise => { + const httpRequest = new Request(this.graphQlEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/graphql', + Authorization: this.accessToken, + 'x-amz-user-agent': + options?.userAgent ?? this.userAgentProvider.getUserAgent(), + }, + body: JSON.stringify({ + query: request.query, + variables: request.variables, + }), + }); + + const res = await this._fetch(httpRequest); + const responseHeaders: Record = {}; + res.headers.forEach((value, key) => (responseHeaders[key] = value)); + if (!res.ok) { + const body = await res.text(); + throw new Error( + `GraphQL request failed, response headers=${JSON.stringify( + responseHeaders + )}, body=${body}` + ); + } + const body = await res.json(); + if (body && typeof body === 'object' && 'errors' in body) { + throw new Error( + `GraphQL request failed, response headers=${JSON.stringify( + responseHeaders + )}, body=${JSON.stringify(body)}` + ); + } + return body as TReturn; + }; +} diff --git a/packages/ai-constructs/src/conversation/runtime/index.ts b/packages/ai-constructs/src/conversation/runtime/index.ts index 72f1300087..187d962b03 100644 --- a/packages/ai-constructs/src/conversation/runtime/index.ts +++ b/packages/ai-constructs/src/conversation/runtime/index.ts @@ -1,24 +1,24 @@ import { - ConversationMessage, - ConversationMessageContentBlock, ConversationTurnEvent, ExecutableTool, + FromJSONSchema, + JSONSchema, ToolDefinition, - ToolExecutionInput, ToolInputSchema, ToolResultContentBlock, } from './types.js'; import { handleConversationTurnEvent } from './conversation_turn_executor.js'; +import { createExecutableTool } from './executable_tool_factory.js'; export { - ConversationMessage, - ConversationMessageContentBlock, ConversationTurnEvent, + createExecutableTool, ExecutableTool, + FromJSONSchema, + JSONSchema, handleConversationTurnEvent, ToolDefinition, - ToolExecutionInput, ToolInputSchema, ToolResultContentBlock, }; diff --git a/packages/ai-constructs/src/conversation/runtime/lazy.ts b/packages/ai-constructs/src/conversation/runtime/lazy.ts new file mode 100644 index 0000000000..7f5b2032ca --- /dev/null +++ b/packages/ai-constructs/src/conversation/runtime/lazy.ts @@ -0,0 +1,17 @@ +/** + * A class that initializes lazily upon usage. + */ +export class Lazy { + #value?: T; + + /** + * Creates lazy instance. + */ + constructor(private readonly valueFactory: () => T) {} + /** + * Gets a value. Value is create at first access. + */ + public get value(): T { + return (this.#value ??= this.valueFactory()); + } +} diff --git a/packages/ai-constructs/src/conversation/runtime/types.ts b/packages/ai-constructs/src/conversation/runtime/types.ts index 3cab0c9925..3d95030cab 100644 --- a/packages/ai-constructs/src/conversation/runtime/types.ts +++ b/packages/ai-constructs/src/conversation/runtime/types.ts @@ -1,15 +1,18 @@ import * as bedrock from '@aws-sdk/client-bedrock-runtime'; -import * as smithy from '@smithy/types'; +import * as jsonSchemaToTypeScript from 'json-schema-to-ts'; /* Notice: This file contains types that are exposed publicly. Therefore, we avoid eager introduction of types that wouldn't be useful for public API consumer and potentially pollute syntax assist in IDEs. */ - -export type ToolInputSchema = bedrock.ToolInputSchema; +export type JSONSchema = jsonSchemaToTypeScript.JSONSchema; +export type FromJSONSchema = + jsonSchemaToTypeScript.FromSchema; +export type ToolInputSchema = { + json: TJSONSchema; +}; export type ToolResultContentBlock = bedrock.ToolResultContentBlock; -export type ToolExecutionInput = smithy.DocumentType; export type ConversationMessage = { role: 'user' | 'assistant'; @@ -23,12 +26,20 @@ export type ConversationMessageContentBlock = // Upstream (Appsync) may send images in a form of Base64 encoded strings source: { bytes: string }; }; + // These are needed so that union with other content block types works. + // See https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-bedrock-runtime/TypeAlias/ContentBlock/. + text?: never; + document?: never; + toolUse?: never; + toolResult?: never; + guardContent?: never; + $unknown?: never; }; -export type ToolDefinition = { +export type ToolDefinition = { name: string; description: string; - inputSchema: ToolInputSchema; + inputSchema: ToolInputSchema; }; // Customers are not expected to create events themselves, therefore @@ -36,6 +47,7 @@ export type ToolDefinition = { export type ConversationTurnEvent = { conversationId: string; currentMessageId: string; + streamResponse?: boolean; responseMutation: { name: string; inputTypeName: string; @@ -53,11 +65,15 @@ export type ConversationTurnEvent = { }; }; request: { - headers: { - authorization: string; - }; + headers: Record; + }; + messageHistoryQuery: { + getQueryName: string; + getQueryInputTypeName: string; + listQueryName: string; + listQueryInputTypeName: string; + listQueryLimit?: number; }; - messages: Array; toolsConfiguration?: { dataTools?: Array< ToolDefinition & { @@ -72,8 +88,55 @@ export type ConversationTurnEvent = { }; }; -export type ExecutableTool = ToolDefinition & { - execute: ( - input: ToolExecutionInput | undefined - ) => Promise; +export type ExecutableTool< + TJSONSchema extends JSONSchema = JSONSchema, + TToolInput = FromJSONSchema +> = ToolDefinition & { + execute: (input: TToolInput) => Promise; +}; + +export type ConversationTurnError = { + errorType: string; + message: string; }; + +export type StreamingResponseChunk = { + // always required + conversationId: string; + associatedUserMessageId: string; + contentBlockIndex: number; + accumulatedTurnContent: Array; +} & ( + | { + // text chunk + contentBlockText: string; + contentBlockDeltaIndex: number; + contentBlockDoneAtIndex?: never; + contentBlockToolUse?: never; + stopReason?: never; + } + | { + // end of block. applicable to text blocks + contentBlockDoneAtIndex: number; + contentBlockText?: never; + contentBlockDeltaIndex?: never; + contentBlockToolUse?: never; + stopReason?: never; + } + | { + // tool use + contentBlockToolUse: string; // serialized json with full tool use block + contentBlockDoneAtIndex?: never; + contentBlockText?: never; + contentBlockDeltaIndex?: never; + stopReason?: never; + } + | { + // turn complete + stopReason: string; + contentBlockDoneAtIndex?: never; + contentBlockText?: never; + contentBlockDeltaIndex?: never; + contentBlockToolUse?: never; + } +); diff --git a/packages/ai-constructs/src/conversation/runtime/user_agent_provider.test.ts b/packages/ai-constructs/src/conversation/runtime/user_agent_provider.test.ts new file mode 100644 index 0000000000..6309e8de95 --- /dev/null +++ b/packages/ai-constructs/src/conversation/runtime/user_agent_provider.test.ts @@ -0,0 +1,65 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import * as fs from 'node:fs'; +import path from 'path'; +import { UserAgentProvider } from './user_agent_provider'; +import { ConversationTurnEvent } from './types'; + +void describe('User Agent provider', () => { + // Read package json from disk (i.e., in a different way than actual implementation does). + const packageVersion = JSON.parse( + fs.readFileSync( + path.resolve(__dirname, '..', '..', '..', 'package.json'), + 'utf-8' + ) + ).version; + + void it('adds package information as metadata when user agent is present in the event', () => { + const userAgentProvider = new UserAgentProvider({ + request: { + headers: { + 'x-amz-user-agent': 'lib/foo#1.2.3', + }, + }, + } as unknown as ConversationTurnEvent); + + const userAgent = userAgentProvider.getUserAgent(); + + assert.strictEqual( + userAgent, + `lib/foo#1.2.3 md/amplify-ai-constructs#${packageVersion}` + ); + }); + + void it('adds package information as lib when user agent is not present in the event', () => { + const userAgentProvider = new UserAgentProvider({ + request: { + headers: {}, + }, + } as unknown as ConversationTurnEvent); + + const userAgent = userAgentProvider.getUserAgent(); + + assert.strictEqual( + userAgent, + `lib/amplify-ai-constructs#${packageVersion}` + ); + }); + + void it('adds additional metadata', () => { + const userAgentProvider = new UserAgentProvider({ + request: { + headers: {}, + }, + } as unknown as ConversationTurnEvent); + + const userAgent = userAgentProvider.getUserAgent({ + 'turn-response-type': 'streaming', + }); + + assert.strictEqual( + userAgent, + `lib/amplify-ai-constructs#${packageVersion} md/turn-response-type#streaming` + ); + }); +}); diff --git a/packages/ai-constructs/src/conversation/runtime/user_agent_provider.ts b/packages/ai-constructs/src/conversation/runtime/user_agent_provider.ts new file mode 100644 index 0000000000..a958b4eb94 --- /dev/null +++ b/packages/ai-constructs/src/conversation/runtime/user_agent_provider.ts @@ -0,0 +1,53 @@ +import { ConversationTurnEvent } from './types'; + +// This is intentional. There's no other way to read package version. +// 1. The 'imports' field in package.json won't work because this is CommonJS package. +// 2. We can't use `fs.readFile`. This file is bundled by ESBuild. ESBuild needs to know to bundle package.json +// That is achievable by either require or import statements. +// 3. The package.json is outside the rootDir defined in tsconfig.json +// Imports require tsconfig to be broken down (as explained here https://stackoverflow.com/questions/55753163/package-json-is-not-under-rootdir). +// This would however would not work with our scripts that check tsconfig files for correctness. +// 4. Hardcoding version in the code, as opposed to reading package.json file isn't great option either. +// +// Therefore, using require as least problematic solution here. +// eslint-disable-next-line @typescript-eslint/no-var-requires +const packageVersion = require('../../../package.json').version; +// Compliant with https://www.rfc-editor.org/rfc/rfc5234. +const packageName = 'amplify-ai-constructs'; + +export type UserAgentAdditionalMetadata = { + // These keys are user agent friendly intentionally. + // eslint-disable-next-line @typescript-eslint/naming-convention + 'turn-response-type'?: 'single' | 'streaming' | 'error'; +}; + +/** + * Provides user agent. + */ +export class UserAgentProvider { + /** + * Creates user agent provider instance. + */ + constructor(private readonly event: ConversationTurnEvent) {} + + getUserAgent = (additionalMetadata?: UserAgentAdditionalMetadata): string => { + let userAgent = this.event.request.headers['x-amz-user-agent']; + + // append library version + if (userAgent) { + // if user agent was forwarded from AppSync then append our package information as metadata. + userAgent = `${userAgent} md/${packageName}#${packageVersion}`; + } else { + // if user agent was not forwarded use our package information as library. + userAgent = `lib/${packageName}#${packageVersion}`; + } + + if (additionalMetadata) { + Object.entries(additionalMetadata).forEach(([key, value]) => { + userAgent = `${userAgent} md/${key}#${value}`; + }); + } + + return userAgent; + }; +} diff --git a/packages/ai-constructs/tsconfig.json b/packages/ai-constructs/tsconfig.json index c07fe67565..fd5619c21a 100644 --- a/packages/ai-constructs/tsconfig.json +++ b/packages/ai-constructs/tsconfig.json @@ -9,5 +9,10 @@ "outDir": "lib", "allowJs": true }, - "references": [{ "path": "../plugin-types" }] + "references": [ + { "path": "../backend-output-schemas" }, + { "path": "../platform-core" }, + { "path": "../plugin-types" }, + { "path": "../backend-output-storage" } + ] } diff --git a/packages/ampx/CHANGELOG.md b/packages/ampx/CHANGELOG.md index 4491409e23..038f571e2f 100644 --- a/packages/ampx/CHANGELOG.md +++ b/packages/ampx/CHANGELOG.md @@ -1,5 +1,11 @@ # ampx +## 0.2.2 + +### Patch Changes + +- e648e8e: added main field to package.json so these packages are resolvable + ## 0.2.1 ### Patch Changes diff --git a/packages/ampx/package.json b/packages/ampx/package.json index d029196189..be64fde07a 100644 --- a/packages/ampx/package.json +++ b/packages/ampx/package.json @@ -1,6 +1,6 @@ { "name": "ampx", - "version": "0.2.1", + "version": "0.2.2", "type": "module", "publishConfig": { "access": "public" diff --git a/packages/auth-construct/API.md b/packages/auth-construct/API.md index 6afb8647cb..f3c895c6ce 100644 --- a/packages/auth-construct/API.md +++ b/packages/auth-construct/API.md @@ -9,6 +9,7 @@ import { AuthResources } from '@aws-amplify/plugin-types'; import { aws_cognito } from 'aws-cdk-lib'; import { BackendOutputStorageStrategy } from '@aws-amplify/plugin-types'; import { Construct } from 'constructs'; +import { IFunction } from 'aws-cdk-lib/aws-lambda'; import { NumberAttributeConstraints } from 'aws-cdk-lib/aws-cognito'; import { ResourceProvider } from '@aws-amplify/plugin-types'; import { SecretValue } from 'aws-cdk-lib'; @@ -47,7 +48,7 @@ export type AuthProps = { externalProviders?: ExternalProviderOptions; }; senders?: { - email: Pick; + email: Pick | CustomEmailSender; }; userAttributes?: UserAttributes; multifactor?: MFA; @@ -84,6 +85,12 @@ export type CustomAttributeString = CustomAttributeBase & StringAttributeConstra dataType: 'String'; }; +// @public +export type CustomEmailSender = { + handler: IFunction; + kmsKeyArn?: string; +}; + // @public export type EmailLogin = true | EmailLoginSettings; diff --git a/packages/auth-construct/CHANGELOG.md b/packages/auth-construct/CHANGELOG.md index d41c584235..d4205498d7 100644 --- a/packages/auth-construct/CHANGELOG.md +++ b/packages/auth-construct/CHANGELOG.md @@ -1,5 +1,46 @@ # @aws-amplify/auth-construct +## 1.5.0 + +### Minor Changes + +- 90a7c49: Add support for referenceAuth. + +### Patch Changes + +- Updated dependencies [90a7c49] + - @aws-amplify/plugin-types@1.4.0 + +## 1.4.0 + +### Minor Changes + +- 11d62fe: Add support for custom Lambda function email senders in Auth construct + +### Patch Changes + +- b56d344: update aws-cdk lib to ^2.158.0 +- Updated dependencies [b56d344] + - @aws-amplify/backend-output-storage@1.1.3 + - @aws-amplify/plugin-types@1.3.1 + +## 1.3.2 + +### Patch Changes + +- 5f46d8d: add user groups to outputs +- Updated dependencies [5f46d8d] + - @aws-amplify/backend-output-schemas@1.4.0 + +## 1.3.1 + +### Patch Changes + +- e648e8e: added main field to package.json so these packages are resolvable +- Updated dependencies [8dd7286] + - @aws-amplify/backend-output-storage@1.1.2 + - @aws-amplify/plugin-types@1.2.2 + ## 1.3.0 ### Minor Changes diff --git a/packages/auth-construct/package.json b/packages/auth-construct/package.json index cbd26233a5..42c20143ee 100644 --- a/packages/auth-construct/package.json +++ b/packages/auth-construct/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/auth-construct", - "version": "1.3.0", + "version": "1.5.0", "type": "commonjs", "publishConfig": { "access": "public" @@ -19,13 +19,13 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/backend-output-schemas": "^1.1.0", - "@aws-amplify/backend-output-storage": "^1.1.1", - "@aws-amplify/plugin-types": "^1.2.1", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/backend-output-storage": "^1.1.3", + "@aws-amplify/plugin-types": "^1.4.0", "@aws-sdk/util-arn-parser": "^3.568.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0" } } diff --git a/packages/auth-construct/src/construct.test.ts b/packages/auth-construct/src/construct.test.ts index c2a268f7d2..d597c0222e 100644 --- a/packages/auth-construct/src/construct.test.ts +++ b/packages/auth-construct/src/construct.test.ts @@ -1098,6 +1098,7 @@ void describe('Auth construct', () => { 'oauthRedirectSignOut', 'oauthResponseType', 'oauthClientId', + 'groups', ], }, }, @@ -1480,6 +1481,34 @@ void describe('Auth construct', () => { const outputs = template.findOutputs('*'); assert.equal(outputs['socialProviders']['Value'], `["GOOGLE"]`); }); + void it('can override group precedence and correctly updates stored output', () => { + const app = new App(); + const stack = new Stack(app); + const auth = new AmplifyAuth(stack, 'test', { + loginWith: { email: true }, + groups: ['admins', 'managers'], + }); + auth.resources.groups['admins'].cfnUserGroup.precedence = 2; + const expectedGroups = [ + { + admins: { + precedence: 2, + }, + }, + { + managers: { + precedence: 1, + }, + }, + ]; + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::Cognito::UserPoolGroup', { + GroupName: 'admins', + Precedence: 2, + }); + const outputs = template.findOutputs('*'); + assert.equal(outputs['groups']['Value'], JSON.stringify(expectedGroups)); + }); }); void describe('Auth external login', () => { diff --git a/packages/auth-construct/src/construct.ts b/packages/auth-construct/src/construct.ts index 6c033aaf5b..7c94f9ad00 100644 --- a/packages/auth-construct/src/construct.ts +++ b/packages/auth-construct/src/construct.ts @@ -34,6 +34,7 @@ import { UserPoolIdentityProviderOidc, UserPoolIdentityProviderSaml, UserPoolIdentityProviderSamlMetadataType, + UserPoolOperation, UserPoolProps, } from 'aws-cdk-lib/aws-cognito'; import { FederatedPrincipal, Role } from 'aws-cdk-lib/aws-iam'; @@ -51,6 +52,7 @@ import { StackMetadataBackendOutputStorageStrategy, } from '@aws-amplify/backend-output-storage'; import * as path from 'path'; +import { IKey, Key } from 'aws-cdk-lib/aws-kms'; type DefaultRoles = { auth: Role; unAuth: Role }; type IdentityProviderSetupResult = { @@ -130,6 +132,11 @@ export class AmplifyAuth role: Role; }; } = {}; + /** + * The KMS key used for encrypting custom email sender data. + * This is only set when using a custom email sender. + */ + private customEmailSenderKMSkey: IKey | undefined; /** * Create a new Auth construct with AuthProps. @@ -141,24 +148,39 @@ export class AmplifyAuth props: AuthProps = DEFAULTS.IF_NO_PROPS_PROVIDED ) { super(scope, id); - this.name = props.name ?? ''; this.domainPrefix = props.loginWith.externalProviders?.domainPrefix; - // UserPool this.computedUserPoolProps = this.getUserPoolProps(props); + this.userPool = new cognito.UserPool( this, `${this.name}UserPool`, this.computedUserPoolProps ); + /** + * Configure custom email sender for Cognito User Pool + * Grant necessary permissions for Lambda function to decrypt emails + * and allow Cognito to invoke the Lambda function + */ + if ( + props.senders?.email && + 'handler' in props.senders.email && + this.customEmailSenderKMSkey + ) { + this.customEmailSenderKMSkey.grantDecrypt(props.senders.email.handler); + this.customEmailSenderKMSkey.grantEncrypt(props.senders.email.handler); + this.userPool.addTrigger( + UserPoolOperation.of('customEmailSender'), + props.senders.email.handler + ); + } // UserPool - External Providers (Oauth, SAML, OIDC) and User Pool Domain this.providerSetupResult = this.setupExternalProviders( this.userPool, props.loginWith ); - // UserPool Client const userPoolClient = new cognito.UserPoolClient( this, @@ -201,6 +223,7 @@ export class AmplifyAuth userPoolClient, authenticatedUserIamRole: auth, unauthenticatedUserIamRole: unAuth, + identityPoolId: identityPool.ref, cfnResources: { cfnUserPool, cfnUserPoolClient, @@ -478,7 +501,30 @@ export class AmplifyAuth }, { standardAttributes: {}, customAttributes: {} } ); - + /** + * Handle KMS key for custom email sender + * If a custom email sender is provided, we either use the provided KMS key ARN + * or create a new KMS key if one is not provided. + */ + if (props.senders?.email && 'handler' in props.senders.email) { + if (props.senders.email.kmsKeyArn) { + // Use the provided KMS key ARN + this.customEmailSenderKMSkey = Key.fromKeyArn( + this, + `${this.name}CustomSenderKey`, + props.senders.email.kmsKeyArn + ); + } else { + // Create a new KMS key if not provided + this.customEmailSenderKMSkey = new Key( + props.senders.email.handler.stack, + `${this.name}CustomSenderKey`, + { + enableKeyRotation: true, + } + ); + } + } const userPoolProps: UserPoolProps = { signInCaseSensitive: DEFAULTS.SIGN_IN_CASE_SENSITIVE, signInAliases: { @@ -503,15 +549,15 @@ export class AmplifyAuth customAttributes: { ...customAttributes, }, - email: props.senders - ? cognito.UserPoolEmail.withSES({ - fromEmail: props.senders.email.fromEmail, - fromName: props.senders.email.fromName, - replyTo: props.senders.email.replyTo, - sesRegion: Stack.of(this).region, - }) - : undefined, - + email: + props.senders && 'fromEmail' in props.senders.email + ? cognito.UserPoolEmail.withSES({ + fromEmail: props.senders.email.fromEmail, + fromName: props.senders.email.fromName, + replyTo: props.senders.email.replyTo, + sesRegion: Stack.of(this).region, + }) + : undefined, selfSignUpEnabled: DEFAULTS.ALLOW_SELF_SIGN_UP, mfa: mfaMode, mfaMessage: this.getMFAMessage(props.multifactor), @@ -528,6 +574,7 @@ export class AmplifyAuth props.loginWith.email?.userInvitation ) : undefined, + customSenderKmsKey: this.customEmailSenderKMSkey, }; return userPoolProps; }; @@ -1194,6 +1241,28 @@ export class AmplifyAuth }, }); + // user group precedence can be overwritten, so they are exposed via cdk LAZY + output.groups = Lazy.string({ + produce: () => { + const groupsArray: { + [key: string]: { + precedence?: number; + }; + }[] = []; + Object.keys(this.resources.groups).forEach((groupName) => { + const precedence = + this.resources.groups[groupName].cfnUserGroup.precedence; + groupsArray.push({ + [groupName]: { + precedence, + }, + }); + }, {} as Record); + + return JSON.stringify(groupsArray); + }, + }); + outputStorageStrategy.addBackendOutputEntry(authOutputKey, { version: '1', payload: output, diff --git a/packages/auth-construct/src/index.ts b/packages/auth-construct/src/index.ts index 13af450f20..85e3aa6c6c 100644 --- a/packages/auth-construct/src/index.ts +++ b/packages/auth-construct/src/index.ts @@ -26,6 +26,7 @@ export { CustomAttributeBoolean, CustomAttributeDateTime, CustomAttributeBase, + CustomEmailSender, } from './types.js'; export { AmplifyAuth } from './construct.js'; export { triggerEvents } from './trigger_events.js'; diff --git a/packages/auth-construct/src/types.ts b/packages/auth-construct/src/types.ts index 5083ffb73c..c3d4ddbbea 100644 --- a/packages/auth-construct/src/types.ts +++ b/packages/auth-construct/src/types.ts @@ -9,6 +9,7 @@ import { UserPoolIdentityProviderSamlMetadata, UserPoolSESOptions, } from 'aws-cdk-lib/aws-cognito'; +import { IFunction } from 'aws-cdk-lib/aws-lambda'; export type VerificationEmailWithLink = { /** * The type of verification. Must be one of "CODE" or "LINK". @@ -380,6 +381,14 @@ export type CustomAttribute = export type UserAttributes = StandardAttributes & Record<`custom:${string}`, CustomAttribute>; +/** + * CustomEmailSender type for configuring a custom Lambda function for email sending + */ +export type CustomEmailSender = { + handler: IFunction; + kmsKeyArn?: string; +}; + /** * Input props for the AmplifyAuth construct */ @@ -417,11 +426,15 @@ export type AuthProps = { */ senders?: { /** - * Configure Cognito to send emails from SES + * Configure Cognito to send emails from SES or a custom message trigger * SES configurations enable the use of customized email sender addresses and names + * Custom message triggers enable the use of third-party email providers when sending email notifications to users * @see https://docs.amplify.aws/react/build-a-backend/auth/moving-to-production/#email + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-custom-email-sender.html */ - email: Pick; + email: + | Pick + | CustomEmailSender; }; /** * The set of attributes that are required for every user in the user pool. Read more on attributes here - https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html diff --git a/packages/backend-ai/API.md b/packages/backend-ai/API.md index 623d8380f5..2b75a01c57 100644 --- a/packages/backend-ai/API.md +++ b/packages/backend-ai/API.md @@ -4,14 +4,16 @@ ```ts +import { AiModel } from '@aws-amplify/data-schema-types'; import { ConstructFactory } from '@aws-amplify/plugin-types'; -import { DocumentType } from '@smithy/types'; +import { ConversationTurnEventVersion } from '@aws-amplify/ai-constructs/conversation'; import { FunctionResources } from '@aws-amplify/plugin-types'; import { ResourceProvider } from '@aws-amplify/plugin-types'; import * as runtime from '@aws-amplify/ai-constructs/conversation/runtime'; declare namespace __export__conversation { export { + ConversationHandlerFunctionFactory, DefineConversationHandlerFunctionProps, defineConversationHandlerFunction } @@ -23,37 +25,45 @@ declare namespace __export__conversation__runtime { ToolResultContentBlock, ExecutableTool, ConversationTurnEvent, - handleConversationTurnEvent + handleConversationTurnEvent, + createExecutableTool } } export { __export__conversation__runtime } +// @public (undocumented) +type ConversationHandlerFunctionFactory = ConstructFactory> & { + readonly eventVersion: ConversationTurnEventVersion; +}; + // @public (undocumented) type ConversationTurnEvent = runtime.ConversationTurnEvent; +// @public (undocumented) +const createExecutableTool: >(name: string, description: string, inputSchema: runtime.ToolInputSchema, handler: (input: TToolInput) => Promise) => ExecutableTool; + // @public -const defineConversationHandlerFunction: (props: DefineConversationHandlerFunctionProps) => ConstructFactory>; +const defineConversationHandlerFunction: (props: DefineConversationHandlerFunctionProps) => ConversationHandlerFunctionFactory; // @public (undocumented) type DefineConversationHandlerFunctionProps = { name: string; entry?: string; models: Array<{ - modelId: string | { - resourcePath: string; - }; + modelId: string | AiModel; region?: string; }>; + memoryMB?: number; }; // @public (undocumented) -type ExecutableTool = runtime.ToolDefinition & { - execute: (input: DocumentType | undefined) => Promise; +type ExecutableTool> = runtime.ToolDefinition & { + execute: (input: TToolInput) => Promise; }; // @public (undocumented) const handleConversationTurnEvent: (event: ConversationTurnEvent, props?: { - tools?: Array; + tools?: Array>; }) => Promise; // @public (undocumented) diff --git a/packages/backend-ai/CHANGELOG.md b/packages/backend-ai/CHANGELOG.md index 6ea84e7fda..86f57f0bfb 100644 --- a/packages/backend-ai/CHANGELOG.md +++ b/packages/backend-ai/CHANGELOG.md @@ -1,5 +1,108 @@ # @aws-amplify/backend-ai +## 1.0.1 + +### Patch Changes + +- f1db886: add resourceGroupName prop to function +- Updated dependencies [f1db886] +- Updated dependencies [71ef398] + - @aws-amplify/plugin-types@1.5.0 + - @aws-amplify/platform-core@1.2.1 + +## 1.0.0 + +### Major Changes + +- bbd6add: GA release of backend AI features + +### Patch Changes + +- Updated dependencies [fd8759d] +- Updated dependencies [bbd6add] + - @aws-amplify/ai-constructs@1.0.0 + +## 0.3.5 + +### Patch Changes + +- b56d344: update aws-cdk lib to ^2.158.0 +- Updated dependencies [37dd87c] +- Updated dependencies [613bca9] +- Updated dependencies [b56d344] + - @aws-amplify/ai-constructs@0.8.0 + - @aws-amplify/backend-output-storage@1.1.3 + - @aws-amplify/plugin-types@1.3.1 + +## 0.3.4 + +### Patch Changes + +- Updated dependencies [63fb254] + - @aws-amplify/ai-constructs@0.7.0 + +## 0.3.3 + +### Patch Changes + +- bd4ff4d: Add memory setting to conversation handler +- 0d6489d: Use AiModel from data-schema-types as possible input +- Updated dependencies [5f46d8d] +- Updated dependencies [bd4ff4d] + - @aws-amplify/backend-output-schemas@1.4.0 + - @aws-amplify/ai-constructs@0.6.2 + +## 0.3.2 + +### Patch Changes + +- Updated dependencies [b6761b0] + - @aws-amplify/ai-constructs@0.6.0 + +## 0.3.1 + +### Patch Changes + +- Updated dependencies [46a0e85] +- Updated dependencies [faacd1b] + - @aws-amplify/ai-constructs@0.5.0 + +## 0.3.0 + +### Minor Changes + +- 4781704: Add information about event version to conversation components + +### Patch Changes + +- Updated dependencies [4781704] +- Updated dependencies [3a29d43] +- Updated dependencies [6e4a62f] + - @aws-amplify/ai-constructs@0.4.0 + +## 0.2.0 + +### Minor Changes + +- 300a72d: Infer executable tool input type from input schema +- 0a5e51c: Stream conversation logs in sandbox + +### Patch Changes + +- Updated dependencies [300a72d] +- Updated dependencies [0a5e51c] + - @aws-amplify/ai-constructs@0.3.0 + - @aws-amplify/backend-output-schemas@1.3.0 + +## 0.1.2 + +### Patch Changes + +- Updated dependencies [d0a90b1] +- Updated dependencies [d538ecc] + - @aws-amplify/ai-constructs@0.2.0 + - @aws-amplify/backend-output-schemas@1.2.1 + ## 0.1.1 ### Patch Changes diff --git a/packages/backend-ai/package.json b/packages/backend-ai/package.json index d758fedfa3..6699cc19a5 100644 --- a/packages/backend-ai/package.json +++ b/packages/backend-ai/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/backend-ai", - "version": "0.1.1", + "version": "1.0.1", "type": "module", "publishConfig": { "access": "public" @@ -22,15 +22,15 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/ai-constructs": "^0.1.4", - "@aws-amplify/backend-output-schemas": "^1.1.0", - "@aws-amplify/backend-output-storage": "^1.0.2", - "@aws-amplify/platform-core": "^1.1.0", - "@aws-amplify/plugin-types": "^1.0.1" + "@aws-amplify/ai-constructs": "^1.0.0", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/backend-output-storage": "^1.1.3", + "@aws-amplify/data-schema-types": "^1.2.0", + "@aws-amplify/platform-core": "^1.2.1", + "@aws-amplify/plugin-types": "^1.5.0" }, "peerDependencies": { - "@smithy/types": "^3.3.0", - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0" } } diff --git a/packages/backend-ai/src/conversation/factory.test.ts b/packages/backend-ai/src/conversation/factory.test.ts index aa6f76fc7d..9802e4944b 100644 --- a/packages/backend-ai/src/conversation/factory.test.ts +++ b/packages/backend-ai/src/conversation/factory.test.ts @@ -15,6 +15,7 @@ import { defaultEntryHandler } from './test-assets/with-default-entry/resource.j import { customEntryHandler } from './test-assets/with-custom-entry/resource.js'; import { Template } from 'aws-cdk-lib/assertions'; import { defineConversationHandlerFunction } from './factory.js'; +import { ConversationHandlerFunction } from '@aws-amplify/ai-constructs/conversation'; const createStackAndSetContext = (): Stack => { const app = new App(); @@ -57,6 +58,14 @@ void describe('ConversationHandlerFactory', () => { assert.strictEqual(instance1, instance2); }); + void it('has event version corresponding to construct', () => { + const factory = defaultEntryHandler; + assert.strictEqual( + factory.eventVersion, + ConversationHandlerFunction.eventVersion + ); + }); + void it('resolves default entry when not specified', () => { const factory = defaultEntryHandler; const lambda = factory.getInstance(getInstanceProps); @@ -158,8 +167,8 @@ void describe('ConversationHandlerFactory', () => { }); factory.getInstance(getInstanceProps); const template = Template.fromStack(rootStack); - const outputValue = - template.findOutputs('definedFunctions').definedFunctions.Value; + const outputValue = template.findOutputs('definedConversationHandlers') + .definedConversationHandlers.Value; assert.deepStrictEqual(outputValue, { ['Fn::Join']: [ '', @@ -179,4 +188,19 @@ void describe('ConversationHandlerFactory', () => { }); }); }); + + void it('passes memory setting to construct', () => { + const factory = defineConversationHandlerFunction({ + entry: './test-assets/with-default-entry/handler.ts', + name: 'testHandlerName', + models: [], + memoryMB: 271, + }); + const lambda = factory.getInstance(getInstanceProps); + const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + template.resourceCountIs('AWS::Lambda::Function', 1); + template.hasResourceProperties('AWS::Lambda::Function', { + MemorySize: 271, + }); + }); }); diff --git a/packages/backend-ai/src/conversation/factory.ts b/packages/backend-ai/src/conversation/factory.ts index 0a095204de..6b4242288f 100644 --- a/packages/backend-ai/src/conversation/factory.ts +++ b/packages/backend-ai/src/conversation/factory.ts @@ -1,8 +1,6 @@ +import { AIConversationOutput } from '@aws-amplify/backend-output-schemas'; import { - FunctionOutput, - functionOutputKey, -} from '@aws-amplify/backend-output-schemas'; -import { + AmplifyResourceGroupName, BackendOutputStorageStrategy, ConstructContainerEntryGenerator, ConstructFactory, @@ -14,18 +12,21 @@ import { import { ConversationHandlerFunction, ConversationHandlerFunctionProps, + ConversationTurnEventVersion, } from '@aws-amplify/ai-constructs/conversation'; import path from 'path'; import { CallerDirectoryExtractor } from '@aws-amplify/platform-core'; +import { AiModel } from '@aws-amplify/data-schema-types'; class ConversationHandlerFunctionGenerator implements ConstructContainerEntryGenerator { - readonly resourceGroupName = 'conversationHandlerFunction'; + readonly resourceGroupName: AmplifyResourceGroupName = + 'conversationHandlerFunction'; constructor( private readonly props: DefineConversationHandlerFunctionProps, - private readonly outputStorageStrategy: BackendOutputStorageStrategy + private readonly outputStorageStrategy: BackendOutputStorageStrategy ) {} generateContainerEntry = ({ scope }: GenerateContainerEntryProps) => { @@ -43,38 +44,29 @@ class ConversationHandlerFunctionGenerator region: model.region, }; }), + outputStorageStrategy: this.outputStorageStrategy, + memoryMB: this.props.memoryMB, }; const conversationHandlerFunction = new ConversationHandlerFunction( scope, this.props.name, constructProps ); - this.storeOutput(this.outputStorageStrategy, conversationHandlerFunction); return conversationHandlerFunction; }; - - /** - * Append conversation handler to defined functions. - * Explicitly defined custom handler is customer's function and should be visible - * in the outputs. - */ - private storeOutput = ( - outputStorageStrategy: BackendOutputStorageStrategy, - conversationHandlerFunction: ConversationHandlerFunction - ): void => { - outputStorageStrategy.appendToBackendOutputList(functionOutputKey, { - version: '1', - payload: { - definedFunctions: - conversationHandlerFunction.resources.lambda.functionName, - }, - }); - }; } -class ConversationHandlerFunctionFactory - implements ConstructFactory +export type ConversationHandlerFunctionFactory = ConstructFactory< + ResourceProvider +> & { + readonly eventVersion: ConversationTurnEventVersion; +}; + +class DefaultConversationHandlerFunctionFactory + implements ConversationHandlerFunctionFactory { + readonly eventVersion: ConversationTurnEventVersion = + ConversationHandlerFunction.eventVersion; private generator: ConstructContainerEntryGenerator; constructor( @@ -127,14 +119,15 @@ export type DefineConversationHandlerFunctionProps = { name: string; entry?: string; models: Array<{ - modelId: - | string - | { - // This is to match return of 'a.ai.model.anthropic.claude3Haiku()' - resourcePath: string; - }; + modelId: string | AiModel; region?: string; }>; + /** + * An amount of memory (RAM) to allocate to the function between 128 and 10240 MB. + * Must be a whole number. + * Default is 512MB. + */ + memoryMB?: number; }; /** @@ -142,6 +135,6 @@ export type DefineConversationHandlerFunctionProps = { */ export const defineConversationHandlerFunction = ( props: DefineConversationHandlerFunctionProps -): ConstructFactory> => +): ConversationHandlerFunctionFactory => // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors - new ConversationHandlerFunctionFactory(props, new Error().stack); + new DefaultConversationHandlerFunctionFactory(props, new Error().stack); diff --git a/packages/backend-ai/src/conversation/index.ts b/packages/backend-ai/src/conversation/index.ts index dcf2f7c441..489209c219 100644 --- a/packages/backend-ai/src/conversation/index.ts +++ b/packages/backend-ai/src/conversation/index.ts @@ -1,9 +1,11 @@ import { + ConversationHandlerFunctionFactory, DefineConversationHandlerFunctionProps, defineConversationHandlerFunction, } from './factory.js'; export { + ConversationHandlerFunctionFactory, DefineConversationHandlerFunctionProps, defineConversationHandlerFunction, }; diff --git a/packages/backend-ai/src/conversation/runtime/index.ts b/packages/backend-ai/src/conversation/runtime/index.ts index e758247893..1b26bb981d 100644 --- a/packages/backend-ai/src/conversation/runtime/index.ts +++ b/packages/backend-ai/src/conversation/runtime/index.ts @@ -1,17 +1,32 @@ import * as runtime from '@aws-amplify/ai-constructs/conversation/runtime'; -import { DocumentType } from '@smithy/types'; // Re-export types useful for lambda runtime customization. // Some of these types are partially re-defined so that their member use // symbols from same package. export type ToolResultContentBlock = runtime.ToolResultContentBlock; -export type ExecutableTool = runtime.ToolDefinition & { - execute: (input: DocumentType | undefined) => Promise; +export type ExecutableTool< + TJSONSchema extends runtime.JSONSchema = runtime.JSONSchema, + TToolInput = runtime.FromJSONSchema +> = runtime.ToolDefinition & { + execute: (input: TToolInput) => Promise; }; export type ConversationTurnEvent = runtime.ConversationTurnEvent; export const handleConversationTurnEvent: ( event: ConversationTurnEvent, - props?: { tools?: Array } + // This is by design, so that tools with different input types can be added + // to single arrays. Downstream code doesn't use these types. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + props?: { tools?: Array> } ) => Promise = runtime.handleConversationTurnEvent; + +export const createExecutableTool: < + TJSONSchema extends runtime.JSONSchema = runtime.JSONSchema, + TToolInput = runtime.FromJSONSchema +>( + name: string, + description: string, + inputSchema: runtime.ToolInputSchema, + handler: (input: TToolInput) => Promise +) => ExecutableTool = runtime.createExecutableTool; diff --git a/packages/backend-auth/.npmignore b/packages/backend-auth/.npmignore index dbde1fb5db..78143c7113 100644 --- a/packages/backend-auth/.npmignore +++ b/packages/backend-auth/.npmignore @@ -10,5 +10,6 @@ # Then ignore test js and ts declaration files *.test.js *.test.d.ts +**/test-resources/** # This leaves us with including only js and ts declaration files of functional code diff --git a/packages/backend-auth/API.md b/packages/backend-auth/API.md index b3cf7a91cd..6663c93e78 100644 --- a/packages/backend-auth/API.md +++ b/packages/backend-auth/API.md @@ -5,10 +5,13 @@ ```ts import { AmazonProviderProps } from '@aws-amplify/auth-construct'; +import { AmplifyFunction } from '@aws-amplify/plugin-types'; import { AppleProviderProps } from '@aws-amplify/auth-construct'; +import { AuthOutput } from '@aws-amplify/backend-output-schemas'; import { AuthProps } from '@aws-amplify/auth-construct'; import { AuthResources } from '@aws-amplify/plugin-types'; import { AuthRoleName } from '@aws-amplify/plugin-types'; +import { BackendOutputStorageStrategy } from '@aws-amplify/plugin-types'; import { BackendSecret } from '@aws-amplify/plugin-types'; import { ConstructFactory } from '@aws-amplify/plugin-types'; import { ConstructFactoryGetInstanceProps } from '@aws-amplify/plugin-types'; @@ -16,11 +19,15 @@ import { ExternalProviderOptions } from '@aws-amplify/auth-construct'; import { FacebookProviderProps } from '@aws-amplify/auth-construct'; import { FunctionResources } from '@aws-amplify/plugin-types'; import { GoogleProviderProps } from '@aws-amplify/auth-construct'; +import { IFunction } from 'aws-cdk-lib/aws-lambda'; import { OidcProviderProps } from '@aws-amplify/auth-construct'; +import { ReferenceAuthResources } from '@aws-amplify/plugin-types'; import { ResourceAccessAcceptor } from '@aws-amplify/plugin-types'; import { ResourceAccessAcceptorFactory } from '@aws-amplify/plugin-types'; import { ResourceProvider } from '@aws-amplify/plugin-types'; +import { StackProvider } from '@aws-amplify/plugin-types'; import { TriggerEvent } from '@aws-amplify/auth-construct'; +import { UserPoolSESOptions } from 'aws-cdk-lib/aws-cognito'; // @public export type ActionIam = 'addUserToGroup' | 'createGroup' | 'createUser' | 'deleteGroup' | 'deleteUser' | 'deleteUserAttributes' | 'disableUser' | 'enableUser' | 'forgetDevice' | 'getDevice' | 'getGroup' | 'getUser' | 'listUsers' | 'listUsersInGroup' | 'listGroups' | 'listDevices' | 'listGroupsForUser' | 'removeUserFromGroup' | 'resetUserPassword' | 'setUserMfaPreference' | 'setUserPassword' | 'setUserSettings' | 'updateDeviceStatus' | 'updateGroup' | 'updateUserAttributes'; @@ -35,10 +42,18 @@ export type AmazonProviderFactoryProps = Omit & { +export type AmplifyAuthProps = Expand & { loginWith: Expand; triggers?: Partial>>>; access?: AuthAccessGenerator; + senders?: { + email: Pick | CustomEmailSender; + }; +}>; + +// @public (undocumented) +export type AmplifyReferenceAuthProps = Expand & { + access?: AuthAccessGenerator; }>; // @public @@ -77,7 +92,16 @@ export type AuthLoginWithFactoryProps = Omit & ResourceAccessAcceptorFactory; +export type BackendAuth = ResourceProvider & ResourceAccessAcceptorFactory & StackProvider; + +// @public (undocumented) +export type BackendReferenceAuth = ResourceProvider & ResourceAccessAcceptorFactory & StackProvider; + +// @public +export type CustomEmailSender = { + handler: ConstructFactory | IFunction; + kmsKeyArn?: string; +}; // @public export const defineAuth: (props: AmplifyAuthProps) => ConstructFactory; @@ -117,6 +141,22 @@ export type OidcProviderFactoryProps = Omit ConstructFactory; + +// @public (undocumented) +export type ReferenceAuthProps = { + outputStorageStrategy?: BackendOutputStorageStrategy; + userPoolId: string; + identityPoolId: string; + userPoolClientId: string; + authRoleArn: string; + unauthRoleArn: string; + groups?: { + [groupName: string]: string; + }; +}; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-auth/CHANGELOG.md b/packages/backend-auth/CHANGELOG.md index eac5836ecc..712a426ff3 100644 --- a/packages/backend-auth/CHANGELOG.md +++ b/packages/backend-auth/CHANGELOG.md @@ -1,5 +1,62 @@ # @aws-amplify/backend-auth +## 1.4.1 + +### Patch Changes + +- f1db886: add resourceGroupName prop to function +- Updated dependencies [f1db886] + - @aws-amplify/plugin-types@1.5.0 + +## 1.4.0 + +### Minor Changes + +- 90a7c49: Add support for referenceAuth. + +### Patch Changes + +- Updated dependencies [90a7c49] + - @aws-amplify/auth-construct@1.5.0 + - @aws-amplify/plugin-types@1.4.0 + +## 1.3.0 + +### Minor Changes + +- 11d62fe: Add support for custom Lambda function email senders in Auth construct + +### Patch Changes + +- b56d344: update aws-cdk lib to ^2.158.0 +- Updated dependencies [11d62fe] +- Updated dependencies [b56d344] + - @aws-amplify/auth-construct@1.4.0 + - @aws-amplify/backend-output-storage@1.1.3 + - @aws-amplify/plugin-types@1.3.1 + +## 1.2.0 + +### Minor Changes + +- 87dbf41: expose stack property for backend, function resource, storage resource, and auth resource + +### Patch Changes + +- Updated dependencies [87dbf41] + - @aws-amplify/plugin-types@1.3.0 + +## 1.1.5 + +### Patch Changes + +- e648e8e: added main field to package.json so these packages are resolvable +- Updated dependencies [e648e8e] +- Updated dependencies [8dd7286] + - @aws-amplify/auth-construct@1.3.1 + - @aws-amplify/backend-output-storage@1.1.2 + - @aws-amplify/plugin-types@1.2.2 + ## 1.1.4 ### Patch Changes diff --git a/packages/backend-auth/package.json b/packages/backend-auth/package.json index bee451734b..43ea82bfdc 100644 --- a/packages/backend-auth/package.json +++ b/packages/backend-auth/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/backend-auth", - "version": "1.1.4", + "version": "1.4.1", "type": "module", "publishConfig": { "access": "public" @@ -19,16 +19,21 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/auth-construct": "^1.3.0", - "@aws-amplify/backend-output-storage": "^1.1.1", - "@aws-amplify/plugin-types": "^1.2.1" + "@aws-amplify/auth-construct": "^1.5.0", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/backend-output-storage": "^1.1.3", + "@aws-amplify/plugin-types": "^1.5.0" }, "devDependencies": { - "@aws-amplify/backend-platform-test-stubs": "^0.3.4", - "@aws-amplify/platform-core": "^1.0.6" + "@aws-amplify/backend-platform-test-stubs": "^0.3.6", + "@aws-amplify/platform-core": "^1.2.1", + "@aws-sdk/client-cognito-identity-provider": "^3.624.0", + "@aws-sdk/client-cognito-identity": "^3.624.0", + "@types/aws-lambda": "^8.10.119", + "aws-lambda": "^1.0.7" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0" } } diff --git a/packages/backend-auth/src/factory.test.ts b/packages/backend-auth/src/factory.test.ts index 3865fdcec4..eb7ed5336e 100644 --- a/packages/backend-auth/src/factory.test.ts +++ b/packages/backend-auth/src/factory.test.ts @@ -26,6 +26,8 @@ import { import { Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; import { AmplifyUserError } from '@aws-amplify/platform-core'; import { CfnFunction } from 'aws-cdk-lib/aws-lambda'; +import { Key } from 'aws-cdk-lib/aws-kms'; +import { CustomEmailSender } from './types.js'; const createStackAndSetContext = (): Stack => { const app = new App(); @@ -79,12 +81,15 @@ void describe('AmplifyAuthFactory', () => { assert.strictEqual(instance1, instance2); }); + void it('verifies stack property exists and is equivalent to auth stack', () => { + const backendAuth = authFactory.getInstance(getInstanceProps); + assert.equal(backendAuth.stack, Stack.of(backendAuth.resources.userPool)); + }); + void it('adds construct to stack', () => { const backendAuth = authFactory.getInstance(getInstanceProps); - const template = Template.fromStack( - Stack.of(backendAuth.resources.userPool) - ); + const template = Template.fromStack(backendAuth.stack); template.resourceCountIs('AWS::Cognito::UserPool', 1); }); @@ -98,9 +103,7 @@ void describe('AmplifyAuthFactory', () => { const backendAuth = authFactory.getInstance(getInstanceProps); - const template = Template.fromStack( - Stack.of(backendAuth.resources.userPool) - ); + const template = Template.fromStack(backendAuth.stack); template.resourceCountIs('AWS::Cognito::UserPool', 1); template.hasResourceProperties('AWS::Cognito::UserPool', { @@ -150,8 +153,8 @@ void describe('AmplifyAuthFactory', () => { }, new AmplifyUserError('MultipleSingletonResourcesError', { message: - 'Multiple `defineAuth` calls are not allowed within an Amplify backend', - resolution: 'Remove all but one `defineAuth` call', + 'Multiple `defineAuth` or `referenceAuth` calls are not allowed within an Amplify backend', + resolution: 'Remove all but one `defineAuth` or `referenceAuth` call', }) ); }); @@ -247,9 +250,7 @@ void describe('AmplifyAuthFactory', () => { const backendAuth = authWithTriggerFactory.getInstance(getInstanceProps); - const template = Template.fromStack( - Stack.of(backendAuth.resources.userPool) - ); + const template = Template.fromStack(backendAuth.stack); template.hasResourceProperties('AWS::Cognito::UserPool', { LambdaConfig: { // The key in the CFN template is the trigger event name with the first character uppercase @@ -356,6 +357,144 @@ void describe('AmplifyAuthFactory', () => { }); }); }); + + void it('sets customEmailSender when function is provided as email sender', () => { + const testFunc = new aws_lambda.Function(stack, 'testFunc', { + code: aws_lambda.Code.fromInline('test placeholder'), + runtime: aws_lambda.Runtime.NODEJS_18_X, + handler: 'index.handler', + }); + const funcStub: ConstructFactory> = { + getInstance: () => { + return { + resources: { + lambda: testFunc, + cfnResources: { + cfnFunction: testFunc.node.findChild('Resource') as CfnFunction, + }, + }, + }; + }, + }; + const customEmailSender: CustomEmailSender = { + handler: funcStub, + }; + resetFactoryCount(); + + const authWithTriggerFactory = defineAuth({ + loginWith: { email: true }, + senders: { email: customEmailSender }, + }); + + const backendAuth = authWithTriggerFactory.getInstance(getInstanceProps); + + const template = Template.fromStack(backendAuth.stack); + + template.hasResourceProperties('AWS::Cognito::UserPool', { + LambdaConfig: { + CustomEmailSender: { + LambdaArn: { + Ref: Match.stringLikeRegexp('testFunc'), + }, + }, + KMSKeyID: { + Ref: Match.stringLikeRegexp('CustomSenderKey'), + }, + }, + }); + }); + void it('ensures empty lambdaTriggers do not remove triggers added elsewhere', () => { + const testFunc = new aws_lambda.Function(stack, 'testFunc', { + code: aws_lambda.Code.fromInline('test placeholder'), + runtime: aws_lambda.Runtime.NODEJS_18_X, + handler: 'index.handler', + }); + const funcStub: ConstructFactory> = { + getInstance: () => { + return { + resources: { + lambda: testFunc, + cfnResources: { + cfnFunction: testFunc.node.findChild('Resource') as CfnFunction, + }, + }, + }; + }, + }; + const customEmailSender: CustomEmailSender = { + handler: funcStub, + }; + resetFactoryCount(); + + const authWithTriggerFactory = defineAuth({ + loginWith: { email: true }, + senders: { email: customEmailSender }, + triggers: { preSignUp: funcStub }, + }); + + const backendAuth = authWithTriggerFactory.getInstance(getInstanceProps); + + const template = Template.fromStack(backendAuth.stack); + template.hasResourceProperties('AWS::Cognito::UserPool', { + LambdaConfig: { + PreSignUp: { + Ref: Match.stringLikeRegexp('testFunc'), + }, + CustomEmailSender: { + LambdaArn: { + Ref: Match.stringLikeRegexp('testFunc'), + }, + }, + KMSKeyID: { + Ref: Match.stringLikeRegexp('CustomSenderKey'), + }, + }, + }); + }); + void it('uses provided KMS key ARN and sets up custom email sender', () => { + const customKmsKeyArn = new Key(stack, `CustomSenderKey`, { + enableKeyRotation: true, + }); + const testFunc = new aws_lambda.Function(stack, 'testFunc', { + code: aws_lambda.Code.fromInline('test placeholder'), + runtime: aws_lambda.Runtime.NODEJS_18_X, + handler: 'index.handler', + }); + const funcStub: ConstructFactory> = { + getInstance: () => ({ + resources: { + lambda: testFunc, + cfnResources: { + cfnFunction: testFunc.node.findChild('Resource') as CfnFunction, + }, + }, + }), + }; + const customEmailSender: CustomEmailSender = { + handler: funcStub, + kmsKeyArn: customKmsKeyArn.keyArn, + }; + resetFactoryCount(); + + const authWithTriggerFactory = defineAuth({ + loginWith: { email: true }, + senders: { + email: customEmailSender, + }, + triggers: { preSignUp: funcStub }, + }); + + const backendAuth = authWithTriggerFactory.getInstance(getInstanceProps); + const template = Template.fromStack(backendAuth.stack); + + template.hasResourceProperties('AWS::Cognito::UserPool', { + LambdaConfig: { + KMSKeyID: { + Ref: Match.stringLikeRegexp('CustomSenderKey'), + }, + }, + }); + }); }); const upperCaseFirstChar = (str: string) => { diff --git a/packages/backend-auth/src/factory.ts b/packages/backend-auth/src/factory.ts index c5184cb743..c0a4c63445 100644 --- a/packages/backend-auth/src/factory.ts +++ b/packages/backend-auth/src/factory.ts @@ -1,6 +1,10 @@ import * as path from 'path'; import { Policy } from 'aws-cdk-lib/aws-iam'; -import { UserPool, UserPoolOperation } from 'aws-cdk-lib/aws-cognito'; +import { + UserPool, + UserPoolOperation, + UserPoolSESOptions, +} from 'aws-cdk-lib/aws-cognito'; import { AmplifyUserError, TagName } from '@aws-amplify/platform-core'; import { AmplifyAuth, @@ -8,6 +12,7 @@ import { TriggerEvent, } from '@aws-amplify/auth-construct'; import { + AmplifyResourceGroupName, AuthResources, AuthRoleName, ConstructContainerEntryGenerator, @@ -18,23 +23,29 @@ import { ResourceAccessAcceptor, ResourceAccessAcceptorFactory, ResourceProvider, + StackProvider, } from '@aws-amplify/plugin-types'; -import { translateToAuthConstructLoginWith } from './translate_auth_props.js'; +import { + translateToAuthConstructLoginWith, + translateToAuthConstructSenders, +} from './translate_auth_props.js'; import { authAccessBuilder as _authAccessBuilder } from './access_builder.js'; import { AuthAccessPolicyArbiterFactory } from './auth_access_policy_arbiter.js'; import { AuthAccessGenerator, AuthLoginWithFactoryProps, + CustomEmailSender, Expand, } from './types.js'; import { UserPoolAccessPolicyFactory } from './userpool_access_policy_factory.js'; -import { Tags } from 'aws-cdk-lib'; +import { Stack, Tags } from 'aws-cdk-lib'; export type BackendAuth = ResourceProvider & - ResourceAccessAcceptorFactory; + ResourceAccessAcceptorFactory & + StackProvider; export type AmplifyAuthProps = Expand< - Omit & { + Omit & { /** * Specify how you would like users to log in. You can choose from email, phone, and even external providers such as LoginWithAmazon. */ @@ -58,6 +69,14 @@ export type AmplifyAuthProps = Expand< * access: (allow) => [allow.resource(groupManager).to(["manageGroups"])] */ access?: AuthAccessGenerator; + /** + * Configure email sender options + */ + senders?: { + email: + | Pick + | CustomEmailSender; + }; } >; @@ -85,8 +104,8 @@ export class AmplifyAuthFactory implements ConstructFactory { if (AmplifyAuthFactory.factoryCount > 0) { throw new AmplifyUserError('MultipleSingletonResourcesError', { message: - 'Multiple `defineAuth` calls are not allowed within an Amplify backend', - resolution: 'Remove all but one `defineAuth` call', + 'Multiple `defineAuth` or `referenceAuth` calls are not allowed within an Amplify backend', + resolution: 'Remove all but one `defineAuth` or `referenceAuth` call', }); } AmplifyAuthFactory.factoryCount++; @@ -116,7 +135,7 @@ export class AmplifyAuthFactory implements ConstructFactory { } class AmplifyAuthGenerator implements ConstructContainerEntryGenerator { - readonly resourceGroupName = 'auth'; + readonly resourceGroupName: AmplifyResourceGroupName = 'auth'; private readonly name: string; constructor( @@ -140,6 +159,10 @@ class AmplifyAuthGenerator implements ConstructContainerEntryGenerator { this.props.loginWith, backendSecretResolver ), + senders: translateToAuthConstructSenders( + this.props.senders, + this.getInstanceProps + ), outputStorageStrategy: this.getInstanceProps.outputStorageStrategy, }; if (authProps.loginWith.externalProviders) { @@ -195,6 +218,7 @@ class AmplifyAuthGenerator implements ConstructContainerEntryGenerator { policy.attachToRole(role); }, }), + stack: Stack.of(authConstruct), }; if (!this.props.access) { return authConstructMixin; diff --git a/packages/backend-auth/src/index.ts b/packages/backend-auth/src/index.ts index 90ce00ddc3..8a53f0225d 100644 --- a/packages/backend-auth/src/index.ts +++ b/packages/backend-auth/src/index.ts @@ -1,2 +1,8 @@ export { BackendAuth, AmplifyAuthProps, defineAuth } from './factory.js'; +export { + BackendReferenceAuth, + AmplifyReferenceAuthProps, + referenceAuth, + ReferenceAuthProps, +} from './reference_factory.js'; export * from './types.js'; diff --git a/packages/backend-auth/src/lambda/.eslintrc.json b/packages/backend-auth/src/lambda/.eslintrc.json new file mode 100644 index 0000000000..fa0db4e422 --- /dev/null +++ b/packages/backend-auth/src/lambda/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "rules": { + "no-console": "off", + "amplify-backend-rules/prefer-amplify-errors": "off" + } +} diff --git a/packages/backend-auth/src/lambda/reference_auth_initializer.test.ts b/packages/backend-auth/src/lambda/reference_auth_initializer.test.ts new file mode 100644 index 0000000000..81c50484aa --- /dev/null +++ b/packages/backend-auth/src/lambda/reference_auth_initializer.test.ts @@ -0,0 +1,558 @@ +import { beforeEach, describe, it, mock } from 'node:test'; +import { ReferenceAuthInitializer } from './reference_auth_initializer.js'; +import { CloudFormationCustomResourceEvent } from 'aws-lambda'; +import assert from 'node:assert'; +import { + CognitoIdentityProviderClient, + DescribeUserPoolClientCommand, + DescribeUserPoolClientCommandOutput, + DescribeUserPoolCommand, + DescribeUserPoolCommandOutput, + GetUserPoolMfaConfigCommand, + GetUserPoolMfaConfigCommandOutput, + ListGroupsCommand, + ListGroupsCommandOutput, + ListIdentityProvidersCommand, + ListIdentityProvidersCommandOutput, +} from '@aws-sdk/client-cognito-identity-provider'; +import { + CognitoIdentityClient, + DescribeIdentityPoolCommand, + DescribeIdentityPoolCommandOutput, + GetIdentityPoolRolesCommand, + GetIdentityPoolRolesCommandOutput, +} from '@aws-sdk/client-cognito-identity'; +import { + IdentityPool, + IdentityPoolRoles, + IdentityProviders, + MFAResponse, + SampleInputProperties, + UserPool, + UserPoolClient, + UserPoolGroups, +} from '../test-resources/sample_data.js'; + +const customResourceEventCommon: Omit< + CloudFormationCustomResourceEvent, + 'RequestType' +> = { + ServiceToken: 'mockServiceToken', + ResponseURL: 'mockPreSignedS3Url', + StackId: 'mockStackId', + RequestId: '123', + LogicalResourceId: 'logicalId', + ResourceType: 'AWS::CloudFormation::CustomResource', + ResourceProperties: { + ...SampleInputProperties, + ServiceToken: 'token', + }, +}; +const createCfnEvent: CloudFormationCustomResourceEvent = { + RequestType: 'Create', + ...customResourceEventCommon, +}; + +const updateCfnEvent: CloudFormationCustomResourceEvent = { + RequestType: 'Update', + PhysicalResourceId: 'physicalId', + OldResourceProperties: { + ...SampleInputProperties, + ServiceToken: 'token', + }, + ...customResourceEventCommon, +}; + +const deleteCfnEvent: CloudFormationCustomResourceEvent = { + RequestType: 'Delete', + PhysicalResourceId: 'physicalId', + ...customResourceEventCommon, +}; +const httpError = { + $metadata: { + httpStatusCode: 500, + }, +}; +const httpSuccess = { + $metadata: { + httpStatusCode: 200, + }, +}; +const groupName = 'ADMINS'; +const groupRoleARN = 'arn:aws:iam::000000000000:role/sample-group-role'; +const groupRoleARNNotOnUserPool = + 'arn:aws:iam::000000000000:role/sample-bad-group-role'; +// aws sdk will throw with error message for any non 200 status so we don't need to re-package it +const awsSDKErrorMessageMock = new Error('this message comes from the aws sdk'); +const uuidMock = () => '00000000-0000-0000-0000-000000000000'; +const identityProviderClient = new CognitoIdentityProviderClient(); +const identityClient = new CognitoIdentityClient(); +const expectedData = { + userPoolId: SampleInputProperties.userPoolId, + webClientId: SampleInputProperties.userPoolClientId, + identityPoolId: SampleInputProperties.identityPoolId, + signupAttributes: '["sub","email"]', + usernameAttributes: '["email"]', + verificationMechanisms: '["email"]', + passwordPolicyMinLength: '10', + passwordPolicyRequirements: + '["REQUIRES_NUMBERS","REQUIRES_LOWERCASE","REQUIRES_UPPERCASE"]', + mfaConfiguration: 'ON', + mfaTypes: '["TOTP"]', + socialProviders: '["FACEBOOK","GOOGLE","LOGIN_WITH_AMAZON"]', + oauthCognitoDomain: 'ref-auth-userpool-1.auth.us-east-1.amazoncognito.com', + allowUnauthenticatedIdentities: 'true', + oauthScope: '["email","openid","phone"]', + oauthRedirectSignIn: 'https://redirect.com,https://redirect2.com', + oauthRedirectSignOut: 'https://anotherlogouturl.com,https://logouturl.com', + oauthResponseType: 'code', + oauthClientId: SampleInputProperties.userPoolClientId, +}; + +void describe('ReferenceAuthInitializer', () => { + let handler: ReferenceAuthInitializer; + let describeUserPoolResponse: DescribeUserPoolCommandOutput; + let getUserPoolMfaConfigResponse: GetUserPoolMfaConfigCommandOutput; + let listIdentityProvidersResponse: ListIdentityProvidersCommandOutput; + let describeUserPoolClientResponse: DescribeUserPoolClientCommandOutput; + let describeIdentityPoolResponse: DescribeIdentityPoolCommandOutput; + let getIdentityPoolRolesResponse: GetIdentityPoolRolesCommandOutput; + let listGroupsResponse: ListGroupsCommandOutput; + const rejectsAndMatchError = async ( + fn: Promise, + expectedErrorMessage: string + ): Promise => { + await assert.rejects(fn, (error: Error) => { + assert.strictEqual(error.message, expectedErrorMessage); + return true; + }); + }; + beforeEach(() => { + handler = new ReferenceAuthInitializer( + identityClient, + identityProviderClient, + uuidMock + ); + describeUserPoolResponse = { + ...httpSuccess, + UserPool: UserPool, + }; + getUserPoolMfaConfigResponse = { + ...httpSuccess, + ...MFAResponse, + }; + listIdentityProvidersResponse = { + ...httpSuccess, + Providers: [...IdentityProviders], + }; + describeUserPoolClientResponse = { + ...httpSuccess, + UserPoolClient: UserPoolClient, + }; + describeIdentityPoolResponse = { + ...httpSuccess, + ...IdentityPool, + }; + getIdentityPoolRolesResponse = { + ...httpSuccess, + ...IdentityPoolRoles, + }; + listGroupsResponse = { + ...httpSuccess, + ...UserPoolGroups, + }; + mock.method( + identityProviderClient, + 'send', + async ( + request: + | DescribeUserPoolCommand + | GetUserPoolMfaConfigCommand + | ListIdentityProvidersCommand + | DescribeUserPoolClientCommand + | ListGroupsCommand + ) => { + if (request instanceof DescribeUserPoolCommand) { + if (describeUserPoolResponse.$metadata.httpStatusCode !== 200) { + throw awsSDKErrorMessageMock; + } + return describeUserPoolResponse; + } + if (request instanceof GetUserPoolMfaConfigCommand) { + if (getUserPoolMfaConfigResponse.$metadata.httpStatusCode !== 200) { + throw awsSDKErrorMessageMock; + } + return getUserPoolMfaConfigResponse; + } + if (request instanceof ListIdentityProvidersCommand) { + if (listIdentityProvidersResponse.$metadata.httpStatusCode !== 200) { + throw awsSDKErrorMessageMock; + } + return listIdentityProvidersResponse; + } + if (request instanceof DescribeUserPoolClientCommand) { + if (describeUserPoolClientResponse.$metadata.httpStatusCode !== 200) { + throw awsSDKErrorMessageMock; + } + return describeUserPoolClientResponse; + } + if (request instanceof ListGroupsCommand) { + if (listGroupsResponse.$metadata.httpStatusCode !== 200) { + throw awsSDKErrorMessageMock; + } + return listGroupsResponse; + } + return undefined; + } + ); + mock.method( + identityClient, + 'send', + async ( + request: DescribeIdentityPoolCommand | GetIdentityPoolRolesCommand + ) => { + if (request instanceof DescribeIdentityPoolCommand) { + if (describeIdentityPoolResponse.$metadata.httpStatusCode !== 200) { + throw awsSDKErrorMessageMock; + } + return describeIdentityPoolResponse; + } + if (request instanceof GetIdentityPoolRolesCommand) { + if (getIdentityPoolRolesResponse.$metadata.httpStatusCode !== 200) { + throw awsSDKErrorMessageMock; + } + return getIdentityPoolRolesResponse; + } + return undefined; + } + ); + }); + void it('handles create events', async () => { + const result = await handler.handleEvent(createCfnEvent); + assert.deepEqual(result.Status, 'SUCCESS'); + assert.equal( + result.PhysicalResourceId, + '00000000-0000-0000-0000-000000000000' + ); + assert.deepEqual(result.Data, expectedData); + }); + + void it('handles update events', async () => { + const result = await handler.handleEvent(updateCfnEvent); + assert.deepEqual(result.Status, 'SUCCESS'); + assert.deepEqual(result.Data, expectedData); + }); + + void it('handles delete events', async () => { + const result = await handler.handleEvent(deleteCfnEvent); + assert.deepEqual(result.Status, 'SUCCESS'); + }); + + void it('throws if fetching user pool fails', async () => { + describeUserPoolResponse = httpError; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + awsSDKErrorMessageMock.message + ); + }); + + void it('throws if fetching user pool fails', async () => { + describeUserPoolResponse = { + ...httpSuccess, + UserPool: undefined, + }; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + 'Failed to retrieve the specified UserPool.' + ); + }); + + void it('throws if user pool has no password policy', async () => { + describeUserPoolResponse = { + ...httpSuccess, + UserPool: { + ...UserPool, + Policies: undefined, + }, + }; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + 'Failed to retrieve password policy.' + ); + }); + + void it('throws if user pool uses alias attributes', async () => { + describeUserPoolResponse = { + ...httpSuccess, + UserPool: { + ...UserPool, + UsernameAttributes: [], + AliasAttributes: ['email', 'phone_number'], + }, + }; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + 'The specified user pool is configured with alias attributes which are not currently supported.' + ); + }); + + void it('throws if user pool does not have a domain configured and external login providers are enabled', async () => { + describeUserPoolResponse = { + ...httpSuccess, + UserPool: { + ...UserPool, + Domain: undefined, + CustomDomain: undefined, + }, + }; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + 'You must configure a domain for your UserPool if external login providers are enabled.' + ); + }); + + void it('throws if user pool group is not found', async () => { + listGroupsResponse = { + ...httpSuccess, + Groups: [ + { + GroupName: 'OTHERGROUP', + RoleArn: groupRoleARNNotOnUserPool, + }, + ], + }; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + `The group '${groupName}' with role '${groupRoleARN}' does not match any group for the specified user pool.` + ); + }); + + void it('throws if user pool groups request fails', async () => { + listGroupsResponse = { + ...httpError, + Groups: undefined, + }; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + awsSDKErrorMessageMock.message + ); + }); + + void it('throws if user pool groups response is undefined', async () => { + listGroupsResponse = { + ...httpSuccess, + Groups: undefined, + }; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + 'An error occurred while retrieving the groups for the user pool.' + ); + }); + + void it('throws if fetching user pool MFA config fails', async () => { + getUserPoolMfaConfigResponse = httpError; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + awsSDKErrorMessageMock.message + ); + }); + + void it('throws if fetching user pool providers fails', async () => { + listIdentityProvidersResponse = { + $metadata: { + httpStatusCode: 500, + }, + Providers: [], + }; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + awsSDKErrorMessageMock.message + ); + }); + + void it('throws if fetching user pool providers returns undefined', async () => { + listIdentityProvidersResponse = { + ...httpSuccess, + Providers: undefined, + }; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + 'An error occurred while retrieving identity providers for the user pool.' + ); + }); + + void it('throws if fetching user pool client fails', async () => { + describeUserPoolClientResponse = httpError; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + awsSDKErrorMessageMock.message + ); + }); + void it('throws if fetching user pool client returns undefined', async () => { + describeUserPoolClientResponse = { + ...httpSuccess, + UserPoolClient: undefined, + }; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + 'An error occurred while retrieving the user pool client details.' + ); + }); + void it('throws if user pool client does not have sign-out / logout URLs configured and external login providers are enabled', async () => { + describeUserPoolClientResponse = { + ...httpSuccess, + UserPoolClient: { + ...UserPoolClient, + LogoutURLs: [], + }, + }; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + 'Your UserPool client must have "Allowed sign-out URLs" configured if external login providers are enabled.' + ); + }); + void it('throws if user pool client does not have callback URLs configured and external login providers are enabled', async () => { + describeUserPoolClientResponse = { + ...httpSuccess, + UserPoolClient: { + ...UserPoolClient, + CallbackURLs: [], + }, + }; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + 'Your UserPool client must have "Allowed callback URLs" configured if external login providers are enabled.' + ); + }); + + void it('throws if fetching identity pool fails', async () => { + describeIdentityPoolResponse = { + $metadata: { + httpStatusCode: 500, + }, + IdentityPoolId: undefined, + IdentityPoolName: undefined, + AllowUnauthenticatedIdentities: undefined, + }; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + awsSDKErrorMessageMock.message + ); + }); + void it('throws if fetching identity pool returns undefined', async () => { + describeIdentityPoolResponse = { + ...httpSuccess, + IdentityPoolId: undefined, + IdentityPoolName: undefined, + AllowUnauthenticatedIdentities: undefined, + }; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + 'An error occurred while retrieving the identity pool details.' + ); + }); + + void it('throws if fetching identity pool roles fails', async () => { + getIdentityPoolRolesResponse = httpError; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + awsSDKErrorMessageMock.message + ); + }); + void it('throws if fetching identity pool roles return undefined', async () => { + getIdentityPoolRolesResponse = { + ...httpSuccess, + Roles: undefined, + }; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + 'An error occurred while retrieving the roles for the identity pool.' + ); + }); + // throws if userPool or client doesn't match identity pool + void it('throws there is not matching userPool for the identity pool', async () => { + describeIdentityPoolResponse = { + ...describeIdentityPoolResponse, + CognitoIdentityProviders: [ + { + ProviderName: + 'cognito-idp.us-east-1.amazonaws.com/us-east-1_wrongUserPool', + ClientId: 'sampleUserPoolClientId', + ServerSideTokenCheck: false, + }, + ], + }; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + 'The user pool and user pool client pair do not match any cognito identity providers for the specified identity pool.' + ); + }); + void it('throws if identity pool does not have cognito identity providers configured', async () => { + describeIdentityPoolResponse = { + ...describeIdentityPoolResponse, + CognitoIdentityProviders: [], + }; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + 'The specified identity pool does not have any cognito identity providers.' + ); + }); + void it('throws if the client id does not match any cognito provider on the identity pool', async () => { + describeIdentityPoolResponse = { + ...describeIdentityPoolResponse, + CognitoIdentityProviders: [ + { + ProviderName: + 'cognito-idp.us-east-1.amazonaws.com/us-east-1_userpoolTest', + ClientId: 'wrongClientId', + ServerSideTokenCheck: false, + }, + ], + }; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + 'The user pool and user pool client pair do not match any cognito identity providers for the specified identity pool.' + ); + }); + void it('throws if auth role ARN does not match', async () => { + getIdentityPoolRolesResponse = { + ...httpSuccess, + IdentityPoolId: SampleInputProperties.identityPoolId, + Roles: { + authenticated: 'wrongAuthRole', + unauthenticated: SampleInputProperties.unauthRoleArn, + }, + }; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + 'The provided authRoleArn does not match the authenticated role for the specified identity pool.' + ); + }); + void it('throws if unauth role ARN does not match', async () => { + getIdentityPoolRolesResponse = { + ...httpSuccess, + IdentityPoolId: SampleInputProperties.identityPoolId, + Roles: { + authenticated: SampleInputProperties.authRoleArn, + unauthenticated: 'wrongUnauthRole', + }, + }; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + 'The provided unauthRoleArn does not match the unauthenticated role for the specified identity pool.' + ); + }); + void it('throws if user pool client is not a web client', async () => { + describeUserPoolClientResponse = { + ...httpSuccess, + UserPoolClient: { + ...UserPoolClient, + ClientSecret: 'sample', + }, + }; + await rejectsAndMatchError( + handler.handleEvent(createCfnEvent), + 'The specified user pool client is not configured as a web client.' + ); + }); +}); diff --git a/packages/backend-auth/src/lambda/reference_auth_initializer.ts b/packages/backend-auth/src/lambda/reference_auth_initializer.ts new file mode 100644 index 0000000000..9f31a7302b --- /dev/null +++ b/packages/backend-auth/src/lambda/reference_auth_initializer.ts @@ -0,0 +1,544 @@ +import { + CloudFormationCustomResourceEvent, + CloudFormationCustomResourceResponse, + CloudFormationCustomResourceSuccessResponse, +} from 'aws-lambda'; +import { + CognitoIdentityProviderClient, + DescribeUserPoolClientCommand, + DescribeUserPoolCommand, + GetUserPoolMfaConfigCommand, + GetUserPoolMfaConfigCommandOutput, + GroupType, + ListGroupsCommand, + ListIdentityProvidersCommand, + PasswordPolicyType, + ProviderDescription, + UserPoolClientType, + UserPoolType, +} from '@aws-sdk/client-cognito-identity-provider'; +import { + CognitoIdentityClient, + DescribeIdentityPoolCommand, + DescribeIdentityPoolCommandOutput, + GetIdentityPoolRolesCommand, +} from '@aws-sdk/client-cognito-identity'; +import { randomUUID } from 'node:crypto'; +import { AuthOutput } from '@aws-amplify/backend-output-schemas'; +export type ReferenceAuthInitializerProps = { + userPoolId: string; + identityPoolId: string; + authRoleArn: string; + unauthRoleArn: string; + userPoolClientId: string; + groups: Record; + region: string; +}; + +/** + * Initializer that fetches and process auth resources. + */ +export class ReferenceAuthInitializer { + /** + * Create a new initializer + * @param cognitoIdentityClient identity client + * @param cognitoIdentityProviderClient identity provider client + */ + constructor( + private cognitoIdentityClient: CognitoIdentityClient, + private cognitoIdentityProviderClient: CognitoIdentityProviderClient, + private uuidGenerator: () => string + ) {} + + /** + * Handles custom resource events + * @param event event to process + * @returns custom resource response + */ + public handleEvent = async (event: CloudFormationCustomResourceEvent) => { + console.info(`Received '${event.RequestType}' event`); + // physical id is only generated on create, otherwise it must stay the same + const physicalId = + event.RequestType === 'Create' + ? this.uuidGenerator() + : event.PhysicalResourceId; + + // on delete, just respond with success since we don't need to do anything + if (event.RequestType === 'Delete') { + return { + RequestId: event.RequestId, + LogicalResourceId: event.LogicalResourceId, + PhysicalResourceId: physicalId, + StackId: event.StackId, + NoEcho: true, + Status: 'SUCCESS', + } as CloudFormationCustomResourceSuccessResponse; + } + // for create or update events, we will fetch and validate resource properties + const props = + event.ResourceProperties as unknown as ReferenceAuthInitializerProps; + const { + userPool, + userPoolPasswordPolicy, + userPoolMFA, + userPoolGroups, + userPoolProviders, + userPoolClient, + identityPool, + roles, + } = await this.getResourceDetails( + props.userPoolId, + props.identityPoolId, + props.userPoolClientId + ); + + this.validateResources( + userPool, + userPoolProviders, + userPoolGroups, + userPoolClient, + identityPool, + roles, + props + ); + + const userPoolOutputs = await this.getUserPoolOutputs( + userPool, + userPoolPasswordPolicy, + userPoolProviders, + userPoolMFA, + props.region + ); + const identityPoolOutputs = await this.getIdentityPoolOutputs(identityPool); + const userPoolClientOutputs = await this.getUserPoolClientOutputs( + userPoolClient + ); + const data: Omit = { + userPoolId: props.userPoolId, + webClientId: props.userPoolClientId, + identityPoolId: props.identityPoolId, + ...userPoolOutputs, + ...identityPoolOutputs, + ...userPoolClientOutputs, + }; + return { + RequestId: event.RequestId, + LogicalResourceId: event.LogicalResourceId, + PhysicalResourceId: physicalId, + StackId: event.StackId, + NoEcho: true, + Data: data, + Status: 'SUCCESS', + } as CloudFormationCustomResourceSuccessResponse; + }; + + private getUserPool = async (userPoolId: string) => { + const userPoolCommand = new DescribeUserPoolCommand({ + UserPoolId: userPoolId, + }); + const userPoolResponse = await this.cognitoIdentityProviderClient.send( + userPoolCommand + ); + if (!userPoolResponse.UserPool) { + throw new Error('Failed to retrieve the specified UserPool.'); + } + const userPool = userPoolResponse.UserPool; + const policy = userPool.Policies?.PasswordPolicy; + if (!policy) { + throw new Error('Failed to retrieve password policy.'); + } + return { + userPool: userPoolResponse.UserPool, + userPoolPasswordPolicy: policy, + }; + }; + + private getUserPoolMFASettings = async (userPoolId: string) => { + // mfa types + const mfaCommand = new GetUserPoolMfaConfigCommand({ + UserPoolId: userPoolId, + }); + const mfaResponse = await this.cognitoIdentityProviderClient.send( + mfaCommand + ); + return mfaResponse; + }; + + private getUserPoolGroups = async (userPoolId: string) => { + let nextToken: string | undefined; + const groups: GroupType[] = []; + do { + const listGroupsResponse = await this.cognitoIdentityProviderClient.send( + new ListGroupsCommand({ + UserPoolId: userPoolId, + NextToken: nextToken, + }) + ); + if (!listGroupsResponse.Groups) { + throw new Error( + 'An error occurred while retrieving the groups for the user pool.' + ); + } + groups.push(...listGroupsResponse.Groups); + nextToken = listGroupsResponse.NextToken; + } while (nextToken); + return groups; + }; + + private getUserPoolProviders = async (userPoolId: string) => { + const providers: ProviderDescription[] = []; + let nextToken: string | undefined; + do { + const providersResponse = await this.cognitoIdentityProviderClient.send( + new ListIdentityProvidersCommand({ + UserPoolId: userPoolId, + NextToken: nextToken, + }) + ); + if (providersResponse.Providers === undefined) { + throw new Error( + 'An error occurred while retrieving identity providers for the user pool.' + ); + } + providers.push(...providersResponse.Providers); + nextToken = providersResponse.NextToken; + } while (nextToken); + return providers; + }; + + private getIdentityPool = async (identityPoolId: string) => { + const idpResponse = await this.cognitoIdentityClient.send( + new DescribeIdentityPoolCommand({ + IdentityPoolId: identityPoolId, + }) + ); + if (!idpResponse.IdentityPoolId) { + throw new Error( + 'An error occurred while retrieving the identity pool details.' + ); + } + return idpResponse; + }; + + private getIdentityPoolRoles = async (identityPoolId: string) => { + const rolesCommand = new GetIdentityPoolRolesCommand({ + IdentityPoolId: identityPoolId, + }); + const rolesResponse = await this.cognitoIdentityClient.send(rolesCommand); + if (!rolesResponse.Roles) { + throw new Error( + 'An error occurred while retrieving the roles for the identity pool.' + ); + } + return rolesResponse.Roles; + }; + + private getUserPoolClient = async ( + userPoolId: string, + userPoolClientId: string + ) => { + const userPoolClientCommand = new DescribeUserPoolClientCommand({ + UserPoolId: userPoolId, + ClientId: userPoolClientId, + }); + const userPoolClientResponse = + await this.cognitoIdentityProviderClient.send(userPoolClientCommand); + if (!userPoolClientResponse.UserPoolClient) { + throw new Error( + 'An error occurred while retrieving the user pool client details.' + ); + } + return userPoolClientResponse.UserPoolClient; + }; + + /** + * Retrieves all of the resource data that is necessary for validation and output generation. + * @param userPoolId userPoolId + * @param identityPoolId identityPoolId + * @param userPoolClientId userPoolClientId + * @returns all necessary resource data + */ + private getResourceDetails = async ( + userPoolId: string, + identityPoolId: string, + userPoolClientId: string + ) => { + const { userPool, userPoolPasswordPolicy } = await this.getUserPool( + userPoolId + ); + const userPoolMFA = await this.getUserPoolMFASettings(userPoolId); + const userPoolProviders = await this.getUserPoolProviders(userPoolId); + const userPoolGroups = await this.getUserPoolGroups(userPoolId); + const userPoolClient = await this.getUserPoolClient( + userPoolId, + userPoolClientId + ); + const identityPool = await this.getIdentityPool(identityPoolId); + const roles = await this.getIdentityPoolRoles(identityPoolId); + return { + userPool, + userPoolPasswordPolicy, + userPoolMFA, + userPoolProviders, + userPoolGroups, + userPoolClient, + identityPool, + roles, + }; + }; + + /** + * Validate the resource associations. + * 1. make sure the user pool & user pool client pair are a cognito provider for the identity pool + * 2. make sure the provided auth/unauth role ARNs match the roles for the identity pool + * 3. make sure the user pool client is a web client + * @param userPool userPool + * @param userPoolProviders the user pool providers + * @param userPoolGroups the existing groups for the userPool + * @param userPoolClient userPoolClient + * @param identityPool identityPool + * @param identityPoolRoles identityPool roles + * @param props props that include the roles which we compare with the actual roles for the identity pool + */ + private validateResources = ( + userPool: UserPoolType, + userPoolProviders: ProviderDescription[], + userPoolGroups: GroupType[], + userPoolClient: UserPoolClientType, + identityPool: DescribeIdentityPoolCommandOutput, + identityPoolRoles: Record, + props: ReferenceAuthInitializerProps + ) => { + // verify the user pool is a cognito provider for this identity pool + if ( + !identityPool.CognitoIdentityProviders || + identityPool.CognitoIdentityProviders.length === 0 + ) { + throw new Error( + 'The specified identity pool does not have any cognito identity providers.' + ); + } + // check for alias attributes, since we don't support this yet + if (userPool.AliasAttributes && userPool.AliasAttributes.length > 0) { + throw new Error( + 'The specified user pool is configured with alias attributes which are not currently supported.' + ); + } + + // check OAuth settings + if (userPoolProviders.length > 0) { + // validate user pool + const domainSpecified = userPool.Domain || userPool.CustomDomain; + if (!domainSpecified) { + throw new Error( + 'You must configure a domain for your UserPool if external login providers are enabled.' + ); + } + + // validate user pool client + const hasLogoutUrls = + userPoolClient.LogoutURLs && userPoolClient.LogoutURLs.length > 0; + const hasCallbackUrls = + userPoolClient.CallbackURLs && userPoolClient.CallbackURLs.length > 0; + if (!hasLogoutUrls) { + throw new Error( + 'Your UserPool client must have "Allowed sign-out URLs" configured if external login providers are enabled.' + ); + } + if (!hasCallbackUrls) { + throw new Error( + 'Your UserPool client must have "Allowed callback URLs" configured if external login providers are enabled.' + ); + } + } + + // make sure props groups Roles actually exist for the user pool + const groupEntries = Object.entries(props.groups); + for (const [groupName, groupRoleARN] of groupEntries) { + const match = userPoolGroups.find((g) => g.RoleArn === groupRoleARN); + if (match === undefined) { + throw new Error( + `The group '${groupName}' with role '${groupRoleARN}' does not match any group for the specified user pool.` + ); + } + } + // verify that the user pool + user pool client pair are configured with the identity pool + const matchingProvider = identityPool.CognitoIdentityProviders.find((p) => { + const matchingUserPool: boolean = + p.ProviderName === + `cognito-idp.${props.region}.amazonaws.com/${userPool.Id}`; + const matchingUserPoolClient: boolean = + p.ClientId === userPoolClient.ClientId; + return matchingUserPool && matchingUserPoolClient; + }); + if (!matchingProvider) { + throw new Error( + 'The user pool and user pool client pair do not match any cognito identity providers for the specified identity pool.' + ); + } + // verify the auth / unauth roles from the props match the identity pool roles that we retrieved + const authRoleArn = identityPoolRoles['authenticated']; + const unauthRoleArn = identityPoolRoles['unauthenticated']; + if (authRoleArn !== props.authRoleArn) { + throw new Error( + 'The provided authRoleArn does not match the authenticated role for the specified identity pool.' + ); + } + if (unauthRoleArn !== props.unauthRoleArn) { + throw new Error( + 'The provided unauthRoleArn does not match the unauthenticated role for the specified identity pool.' + ); + } + + // make sure the client is a web client here (web clients shouldn't have client secrets) + if (userPoolClient?.ClientSecret) { + throw new Error( + 'The specified user pool client is not configured as a web client.' + ); + } + }; + + /** + * Transform the userPool data into outputs. + * @param userPool user pool + * @param userPoolPasswordPolicy user pool password policy + * @param userPoolProviders user pool providers + * @param userPoolMFA user pool MFA settings + * @returns formatted outputs + */ + private getUserPoolOutputs = ( + userPool: UserPoolType, + userPoolPasswordPolicy: PasswordPolicyType, + userPoolProviders: ProviderDescription[], + userPoolMFA: GetUserPoolMfaConfigCommandOutput, + region: string + ) => { + // password policy requirements + const requirements: string[] = []; + userPoolPasswordPolicy.RequireNumbers && + requirements.push('REQUIRES_NUMBERS'); + userPoolPasswordPolicy.RequireLowercase && + requirements.push('REQUIRES_LOWERCASE'); + userPoolPasswordPolicy.RequireUppercase && + requirements.push('REQUIRES_UPPERCASE'); + userPoolPasswordPolicy.RequireSymbols && + requirements.push('REQUIRES_SYMBOLS'); + // mfa types + const mfaTypes: string[] = []; + if ( + userPoolMFA.SmsMfaConfiguration && + userPoolMFA.SmsMfaConfiguration.SmsConfiguration + ) { + mfaTypes.push('SMS_MFA'); + } + if (userPoolMFA.SoftwareTokenMfaConfiguration?.Enabled) { + mfaTypes.push('TOTP'); + } + // social providers + const socialProviders: string[] = []; + if (userPoolProviders) { + for (const provider of userPoolProviders) { + const providerType = provider.ProviderType; + const providerName = provider.ProviderName; + if (providerType === 'Google') { + socialProviders.push('GOOGLE'); + } + if (providerType === 'Facebook') { + socialProviders.push('FACEBOOK'); + } + if (providerType === 'SignInWithApple') { + socialProviders.push('SIGN_IN_WITH_APPLE'); + } + if (providerType === 'LoginWithAmazon') { + socialProviders.push('LOGIN_WITH_AMAZON'); + } + if (providerType === 'OIDC' && providerName) { + socialProviders.push(providerName); + } + if (providerType === 'SAML' && providerName) { + socialProviders.push(providerName); + } + } + } + + // domain + const oauthDomain = userPool.CustomDomain ?? userPool.Domain ?? ''; + const fullDomainPath = `${oauthDomain}.auth.${region}.amazoncognito.com`; + const data = { + signupAttributes: JSON.stringify( + userPool.SchemaAttributes?.filter( + (attribute) => attribute.Required && attribute.Name + ).map((attribute) => attribute.Name?.toLowerCase()) || [] + ), + usernameAttributes: JSON.stringify( + userPool.UsernameAttributes?.map((attribute) => + attribute.toLowerCase() + ) || [] + ), + verificationMechanisms: JSON.stringify( + userPool.AutoVerifiedAttributes ?? [] + ), + passwordPolicyMinLength: + userPoolPasswordPolicy.MinimumLength === undefined + ? '' + : userPoolPasswordPolicy.MinimumLength.toString(), + passwordPolicyRequirements: JSON.stringify(requirements), + mfaConfiguration: userPool.MfaConfiguration ?? 'OFF', + mfaTypes: JSON.stringify(mfaTypes), + socialProviders: JSON.stringify(socialProviders), + oauthCognitoDomain: fullDomainPath, + }; + return data; + }; + + /** + * Transforms identityPool info into outputs. + * @param identityPool identity pool data + * @returns formatted outputs + */ + private getIdentityPoolOutputs = ( + identityPool: DescribeIdentityPoolCommandOutput + ) => { + const data = { + allowUnauthenticatedIdentities: + identityPool.AllowUnauthenticatedIdentities === true ? 'true' : 'false', + }; + return data; + }; + + /** + * Transforms userPoolClient info into outputs. + * @param userPoolClient userPoolClient data + * @returns formatted outputs + */ + private getUserPoolClientOutputs = (userPoolClient: UserPoolClientType) => { + const data = { + oauthScope: JSON.stringify(userPoolClient.AllowedOAuthScopes ?? []), + oauthRedirectSignIn: userPoolClient.CallbackURLs + ? userPoolClient.CallbackURLs.join(',') + : '', + oauthRedirectSignOut: userPoolClient.LogoutURLs + ? userPoolClient.LogoutURLs.join(',') + : '', + oauthResponseType: userPoolClient.AllowedOAuthFlows + ? userPoolClient.AllowedOAuthFlows.join(',') + : '', + oauthClientId: userPoolClient.ClientId, + }; + return data; + }; +} + +/** + * Entry point for the lambda-backend custom resource to retrieve auth outputs. + */ +export const handler = async ( + event: CloudFormationCustomResourceEvent +): Promise => { + const initializer = new ReferenceAuthInitializer( + new CognitoIdentityClient(), + new CognitoIdentityProviderClient(), + randomUUID + ); + return initializer.handleEvent(event); +}; diff --git a/packages/backend-auth/src/reference_construct.test.ts b/packages/backend-auth/src/reference_construct.test.ts new file mode 100644 index 0000000000..8c852add5e --- /dev/null +++ b/packages/backend-auth/src/reference_construct.test.ts @@ -0,0 +1,184 @@ +import { beforeEach, describe, it, mock } from 'node:test'; +import assert from 'assert'; +import { + AmplifyReferenceAuth, + OUTPUT_PROPERTIES_PROVIDED_BY_AUTH_CUSTOM_RESOURCE, + authOutputKey, +} from './reference_construct.js'; +import { + BackendOutputEntry, + BackendOutputStorageStrategy, +} from '@aws-amplify/plugin-types'; +import { Template } from 'aws-cdk-lib/assertions'; +import { App, Stack } from 'aws-cdk-lib'; +import { ReferenceAuthProps } from './reference_factory.js'; +const refAuthProps: ReferenceAuthProps = { + authRoleArn: 'arn:aws:iam::000000000000:role/amplify-sample-auth-role-name', + unauthRoleArn: + 'arn:aws:iam::000000000000:role/amplify-sample-unauth-role-name', + identityPoolId: 'us-east-1:identityPoolId', + userPoolClientId: 'userPoolClientId', + userPoolId: 'us-east-1_userPoolId', +}; + +void describe('AmplifyConstruct', () => { + void it('creates custom resource initializer', () => { + const app = new App(); + const stack = new Stack(app); + new AmplifyReferenceAuth(stack, 'test', refAuthProps); + const template = Template.fromStack(stack); + // check that custom resource is created with properties + template.hasResourceProperties('Custom::AmplifyRefAuth', { + identityPoolId: refAuthProps.identityPoolId, + userPoolId: refAuthProps.userPoolId, + userPoolClientId: refAuthProps.userPoolClientId, + }); + }); + + void it('creates policy documents for custom resource', () => { + const app = new App(); + const stack = new Stack(app); + new AmplifyReferenceAuth(stack, 'test', refAuthProps); + const template = Template.fromStack(stack); + const policyStatements = [ + { + Action: [ + 'cognito-idp:DescribeUserPool', + 'cognito-idp:GetUserPoolMfaConfig', + 'cognito-idp:ListIdentityProviders', + 'cognito-idp:ListGroups', + 'cognito-idp:DescribeUserPoolClient', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':cognito-idp:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + `:userpool/${refAuthProps.userPoolId}`, + ], + ], + }, + }, + { + Action: [ + 'cognito-identity:DescribeIdentityPool', + 'cognito-identity:GetIdentityPoolRoles', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:aws:cognito-identity:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + `:identitypool/${refAuthProps.identityPoolId}`, + ], + ], + }, + }, + ]; + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: policyStatements, + Version: '2012-10-17', + }, + }); + }); + + void it('generates the correct output values', () => { + const app = new App(); + const stack = new Stack(app); + new AmplifyReferenceAuth(stack, 'test', refAuthProps); + const template = Template.fromStack(stack); + // check that outputs reference custom resource attributes + const outputs = template.findOutputs('*'); + for (const property of OUTPUT_PROPERTIES_PROVIDED_BY_AUTH_CUSTOM_RESOURCE) { + const expectedValue = { + 'Fn::GetAtt': ['AmplifyRefAuthCustomResource', `${property}`], + }; + assert.ok(outputs[property]); + const actualValue = outputs[property]['Value']; + assert.deepEqual(actualValue, expectedValue); + } + }); + + void describe('storeOutput strategy', () => { + let app: App; + let stack: Stack; + const storeOutputMock = mock.fn(); + const stubBackendOutputStorageStrategy: BackendOutputStorageStrategy = + { + addBackendOutputEntry: storeOutputMock, + appendToBackendOutputList: storeOutputMock, + }; + + void beforeEach(() => { + app = new App(); + stack = new Stack(app); + storeOutputMock.mock.resetCalls(); + }); + + void it('stores output using custom strategy and basic props', () => { + const authConstruct = new AmplifyReferenceAuth(stack, 'test', { + ...refAuthProps, + outputStorageStrategy: stubBackendOutputStorageStrategy, + }); + + const expectedUserPoolId = authConstruct.resources.userPool.userPoolId; + const expectedIdentityPoolId = authConstruct.resources.identityPoolId; + const expectedWebClientId = + authConstruct.resources.userPoolClient.userPoolClientId; + const expectedRegion = Stack.of(authConstruct).region; + + const storeOutputArgs = storeOutputMock.mock.calls[0].arguments; + assert.equal(storeOutputArgs.length, 2); + assert.equal(storeOutputArgs[0], authOutputKey); + assert.equal(storeOutputArgs[1]['version'], '1'); + const payload = storeOutputArgs[1]['payload']; + assert.equal(payload['userPoolId'], expectedUserPoolId); + assert.equal(payload['identityPoolId'], expectedIdentityPoolId); + assert.equal(payload['webClientId'], expectedWebClientId); + assert.equal(payload['authRegion'], expectedRegion); + }); + + void it('stores output when no storage strategy is injected', () => { + const app = new App(); + const stack = new Stack(app); + new AmplifyReferenceAuth(stack, 'test', refAuthProps); + + const template = Template.fromStack(stack); + template.templateMatches({ + Metadata: { + [authOutputKey]: { + version: '1', + stackOutputs: [ + 'userPoolId', + 'webClientId', + 'identityPoolId', + 'authRegion', + ...OUTPUT_PROPERTIES_PROVIDED_BY_AUTH_CUSTOM_RESOURCE, + ], + }, + }, + }); + }); + }); +}); diff --git a/packages/backend-auth/src/reference_construct.ts b/packages/backend-auth/src/reference_construct.ts new file mode 100644 index 0000000000..939319a26f --- /dev/null +++ b/packages/backend-auth/src/reference_construct.ts @@ -0,0 +1,224 @@ +import { Construct } from 'constructs'; +import { + CustomResource, + Duration, + Stack, + aws_cognito, + aws_iam, +} from 'aws-cdk-lib'; +import { + BackendOutputStorageStrategy, + ReferenceAuthResources, + ResourceProvider, +} from '@aws-amplify/plugin-types'; +import { + AttributionMetadataStorage, + StackMetadataBackendOutputStorageStrategy, +} from '@aws-amplify/backend-output-storage'; +import { AuthOutput } from '@aws-amplify/backend-output-schemas'; +import * as path from 'path'; +import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { Runtime } from 'aws-cdk-lib/aws-lambda'; +import { Provider } from 'aws-cdk-lib/custom-resources'; +import { Role } from 'aws-cdk-lib/aws-iam'; +import { ReferenceAuthInitializerProps } from './lambda/reference_auth_initializer.js'; +import { fileURLToPath } from 'node:url'; +import { ReferenceAuthProps } from './reference_factory.js'; + +/** + * Expected key that auth output is stored under - must match backend-output-schemas's authOutputKey + */ +export const authOutputKey = 'AWS::Amplify::Auth'; + +const REFERENCE_AUTH_CUSTOM_RESOURCE_PROVIDER_ID = + 'AmplifyRefAuthCustomResourceProvider'; +const REFERENCE_AUTH_CUSTOM_RESOURCE_ID = 'AmplifyRefAuthCustomResource'; +const RESOURCE_TYPE = 'Custom::AmplifyRefAuth'; + +const filename = fileURLToPath(import.meta.url); +const dirname = path.dirname(filename); +const resourcesRoot = path.normalize(path.join(dirname, 'lambda')); +const refAuthLambdaFilePath = path.join( + resourcesRoot, + 'reference_auth_initializer.js' +); + +const authStackType = 'auth-Cognito'; + +/** + * These properties are fetched by the custom resource and must be accounted for + * in the final AuthOutput payload. + */ +export const OUTPUT_PROPERTIES_PROVIDED_BY_AUTH_CUSTOM_RESOURCE: (keyof AuthOutput['payload'])[] = + [ + 'allowUnauthenticatedIdentities', + 'signupAttributes', + 'usernameAttributes', + 'verificationMechanisms', + 'passwordPolicyMinLength', + 'passwordPolicyRequirements', + 'mfaConfiguration', + 'mfaTypes', + 'socialProviders', + 'oauthCognitoDomain', + 'oauthScope', + 'oauthRedirectSignIn', + 'oauthRedirectSignOut', + 'oauthResponseType', + 'oauthClientId', + ]; +/** + * Reference Auth construct for using external auth resources + */ +export class AmplifyReferenceAuth + extends Construct + implements ResourceProvider +{ + resources: ReferenceAuthResources; + + private configurationCustomResource: CustomResource; + + /** + * Create a new AmplifyConstruct + */ + constructor(scope: Construct, id: string, props: ReferenceAuthProps) { + super(scope, id); + + this.resources = { + userPool: aws_cognito.UserPool.fromUserPoolId( + this, + 'UserPool', + props.userPoolId + ), + userPoolClient: aws_cognito.UserPoolClient.fromUserPoolClientId( + this, + 'UserPoolClient', + props.userPoolClientId + ), + authenticatedUserIamRole: aws_iam.Role.fromRoleArn( + this, + 'authenticatedUserRole', + props.authRoleArn + ), + unauthenticatedUserIamRole: aws_iam.Role.fromRoleArn( + this, + 'unauthenticatedUserRole', + props.unauthRoleArn + ), + identityPoolId: props.identityPoolId, + groups: {}, + }; + + // mapping of existing group roles + if (props.groups) { + Object.entries(props.groups).forEach(([groupName, roleArn]) => { + this.resources.groups[groupName] = { + role: Role.fromRoleArn(this, `${groupName}GroupRole`, roleArn), + }; + }); + } + + // custom resource lambda + const refAuthLambda = new NodejsFunction( + scope, + `${REFERENCE_AUTH_CUSTOM_RESOURCE_PROVIDER_ID}Lambda`, + { + runtime: Runtime.NODEJS_18_X, + timeout: Duration.seconds(10), + entry: refAuthLambdaFilePath, + handler: 'handler', + } + ); + // UserPool & UserPoolClient specific permissions + refAuthLambda.grantPrincipal.addToPrincipalPolicy( + new aws_iam.PolicyStatement({ + effect: aws_iam.Effect.ALLOW, + actions: [ + 'cognito-idp:DescribeUserPool', + 'cognito-idp:GetUserPoolMfaConfig', + 'cognito-idp:ListIdentityProviders', + 'cognito-idp:ListGroups', + 'cognito-idp:DescribeUserPoolClient', + ], + resources: [this.resources.userPool.userPoolArn], + }) + ); + // IdentityPool specific permissions + const stack = Stack.of(this); + refAuthLambda.grantPrincipal.addToPrincipalPolicy( + new aws_iam.PolicyStatement({ + effect: aws_iam.Effect.ALLOW, + actions: [ + 'cognito-identity:DescribeIdentityPool', + 'cognito-identity:GetIdentityPoolRoles', + ], + resources: [ + `arn:aws:cognito-identity:${stack.region}:${stack.account}:identitypool/${this.resources.identityPoolId}`, + ], + }) + ); + const provider = new Provider( + scope, + REFERENCE_AUTH_CUSTOM_RESOURCE_PROVIDER_ID, + { + onEventHandler: refAuthLambda, + } + ); + const initializerProps: ReferenceAuthInitializerProps = { + userPoolId: props.userPoolId, + identityPoolId: props.identityPoolId, + userPoolClientId: props.userPoolClientId, + authRoleArn: props.authRoleArn, + unauthRoleArn: props.unauthRoleArn, + groups: props.groups ?? {}, + region: Stack.of(this).region, + }; + // custom resource + this.configurationCustomResource = new CustomResource( + scope, + REFERENCE_AUTH_CUSTOM_RESOURCE_ID, + { + serviceToken: provider.serviceToken, + properties: { + ...initializerProps, + }, + resourceType: RESOURCE_TYPE, + } + ); + + this.storeOutput(props.outputStorageStrategy); + new AttributionMetadataStorage().storeAttributionMetadata( + Stack.of(this), + authStackType, + fileURLToPath(new URL('../package.json', import.meta.url)) + ); + } + + /** + * Stores auth output using the provided strategy + */ + private storeOutput = ( + outputStorageStrategy: BackendOutputStorageStrategy = new StackMetadataBackendOutputStorageStrategy( + Stack.of(this) + ) + ): void => { + // these properties cannot be overwritten + const output: AuthOutput['payload'] = { + userPoolId: this.resources.userPool.userPoolId, + webClientId: this.resources.userPoolClient.userPoolClientId, + identityPoolId: this.resources.identityPoolId, + authRegion: Stack.of(this).region, + }; + + // assign cdk tokens which will be resolved during deployment + for (const property of OUTPUT_PROPERTIES_PROVIDED_BY_AUTH_CUSTOM_RESOURCE) { + output[property] = + this.configurationCustomResource.getAttString(property); + } + + outputStorageStrategy.addBackendOutputEntry(authOutputKey, { + version: '1', + payload: output, + }); + }; +} diff --git a/packages/backend-auth/src/reference_factory.test.ts b/packages/backend-auth/src/reference_factory.test.ts new file mode 100644 index 0000000000..ee16e7317e --- /dev/null +++ b/packages/backend-auth/src/reference_factory.test.ts @@ -0,0 +1,279 @@ +import { beforeEach, describe, it, mock } from 'node:test'; +import { App, Stack } from 'aws-cdk-lib'; +import assert from 'node:assert'; +import { Template } from 'aws-cdk-lib/assertions'; +import { + BackendOutputEntry, + BackendOutputStorageStrategy, + ConstructContainer, + ConstructFactory, + ConstructFactoryGetInstanceProps, + ImportPathVerifier, + ResourceAccessAcceptorFactory, + ResourceNameValidator, + ResourceProvider, +} from '@aws-amplify/plugin-types'; +import { StackMetadataBackendOutputStorageStrategy } from '@aws-amplify/backend-output-storage'; +import { + ConstructContainerStub, + ImportPathVerifierStub, + ResourceNameValidatorStub, + StackResolverStub, +} from '@aws-amplify/backend-platform-test-stubs'; +import { Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import { AmplifyUserError } from '@aws-amplify/platform-core'; +import { + AmplifyReferenceAuthProps, + BackendReferenceAuth, + referenceAuth, +} from './reference_factory.js'; +import { AmplifyAuthFactory } from './factory.js'; + +const defaultReferenceAuthProps: AmplifyReferenceAuthProps = { + authRoleArn: 'arn:aws:iam::000000000000:role/amplify-sample-auth-role-name', + unauthRoleArn: + 'arn:aws:iam::000000000000:role/amplify-sample-unauth-role-name', + identityPoolId: 'us-east-1:identityPoolId', + userPoolClientId: 'userPoolClientId', + userPoolId: 'us-east-1_userPoolId', +}; + +const createStackAndSetContext = (): Stack => { + const app = new App(); + app.node.setContext('amplify-backend-name', 'testEnvName'); + app.node.setContext('amplify-backend-namespace', 'testBackendId'); + app.node.setContext('amplify-backend-type', 'branch'); + const stack = new Stack(app); + return stack; +}; + +void describe('AmplifyReferenceAuthFactory', () => { + let authFactory: ConstructFactory; + let constructContainer: ConstructContainer; + let outputStorageStrategy: BackendOutputStorageStrategy; + let importPathVerifier: ImportPathVerifier; + let getInstanceProps: ConstructFactoryGetInstanceProps; + let resourceNameValidator: ResourceNameValidator; + let stack: Stack; + beforeEach(() => { + resetFactoryCount(); + authFactory = referenceAuth(defaultReferenceAuthProps); + + stack = createStackAndSetContext(); + + constructContainer = new ConstructContainerStub( + new StackResolverStub(stack) + ); + + outputStorageStrategy = new StackMetadataBackendOutputStorageStrategy( + stack + ); + + importPathVerifier = new ImportPathVerifierStub(); + + resourceNameValidator = new ResourceNameValidatorStub(); + + getInstanceProps = { + constructContainer, + outputStorageStrategy, + importPathVerifier, + resourceNameValidator, + }; + }); + + void it('returns singleton instance', () => { + const instance1 = authFactory.getInstance(getInstanceProps); + const instance2 = authFactory.getInstance(getInstanceProps); + + assert.strictEqual(instance1, instance2); + }); + + void it('verifies stack property exists and is equivalent to auth stack', () => { + const backendAuth = authFactory.getInstance(getInstanceProps); + assert.equal(backendAuth.stack, Stack.of(backendAuth.resources.userPool)); + }); + + void it('adds construct to stack', () => { + const backendAuth = authFactory.getInstance(getInstanceProps); + + const template = Template.fromStack(backendAuth.stack); + + template.resourceCountIs('Custom::AmplifyRefAuth', 1); + }); + + void it('verifies constructor import path', () => { + const importPathVerifier = { + verify: mock.fn(), + }; + + authFactory.getInstance({ ...getInstanceProps, importPathVerifier }); + + assert.ok( + (importPathVerifier.verify.mock.calls[0].arguments[0] as string).includes( + 'referenceAuth' + ) + ); + }); + + void it('should throw TooManyAmplifyAuthFactoryError when referenceAuth is called multiple times', () => { + assert.throws( + () => { + referenceAuth({ + ...defaultReferenceAuthProps, + }); + referenceAuth({ + ...defaultReferenceAuthProps, + }); + }, + new AmplifyUserError('MultipleSingletonResourcesError', { + message: + 'Multiple `defineAuth` or `referenceAuth` calls are not allowed within an Amplify backend', + resolution: 'Remove all but one `defineAuth` or `referenceAuth` call', + }) + ); + }); + + void it('if access is defined, it should attach valid policy to the resource', () => { + const mockAcceptResourceAccess = mock.fn(); + const lambdaResourceStub = { + getInstance: () => ({ + getResourceAccessAcceptor: () => ({ + acceptResourceAccess: mockAcceptResourceAccess, + }), + }), + } as unknown as ConstructFactory< + ResourceProvider & ResourceAccessAcceptorFactory + >; + + resetFactoryCount(); + + authFactory = referenceAuth({ + ...defaultReferenceAuthProps, + access: (allow) => [ + allow.resource(lambdaResourceStub).to(['managePasswordRecovery']), + allow.resource(lambdaResourceStub).to(['createUser']), + ], + }); + + const backendAuth = authFactory.getInstance(getInstanceProps); + + assert.equal(mockAcceptResourceAccess.mock.callCount(), 2); + assert.ok( + mockAcceptResourceAccess.mock.calls[0].arguments[0] instanceof Policy + ); + assert.deepStrictEqual( + mockAcceptResourceAccess.mock.calls[0].arguments[0].document.toJSON(), + { + Statement: [ + { + Action: [ + 'cognito-idp:AdminResetUserPassword', + 'cognito-idp:AdminSetUserPassword', + ], + Effect: 'Allow', + Resource: backendAuth.resources.userPool.userPoolArn, + }, + ], + Version: '2012-10-17', + } + ); + assert.ok( + mockAcceptResourceAccess.mock.calls[1].arguments[0] instanceof Policy + ); + assert.deepStrictEqual( + mockAcceptResourceAccess.mock.calls[1].arguments[0].document.toJSON(), + { + Statement: [ + { + Action: 'cognito-idp:AdminCreateUser', + Effect: 'Allow', + Resource: backendAuth.resources.userPool.userPoolArn, + }, + ], + Version: '2012-10-17', + } + ); + }); + + void describe('getResourceAccessAcceptor', () => { + void it('attaches policies to the authenticated role', () => { + const backendAuth = authFactory.getInstance(getInstanceProps); + const testPolicy = new Policy(stack, 'testPolicy', { + statements: [ + new PolicyStatement({ + actions: ['s3:GetObject'], + resources: ['testBucket/testObject/*'], + }), + ], + }); + const resourceAccessAcceptor = backendAuth.getResourceAccessAcceptor( + 'authenticatedUserIamRole' + ); + + assert.equal( + resourceAccessAcceptor.identifier, + 'authenticatedUserIamRoleResourceAccessAcceptor' + ); + + resourceAccessAcceptor.acceptResourceAccess(testPolicy, [ + { name: 'test', path: 'test' }, + ]); + const template = Template.fromStack(stack); + template.resourceCountIs('AWS::IAM::Policy', 1); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 's3:GetObject', + Effect: 'Allow', + Resource: 'testBucket/testObject/*', + }, + ], + }, + Roles: [backendAuth.resources.authenticatedUserIamRole.roleName], + }); + }); + + void it('attaches policies to the unauthenticated role', () => { + const backendAuth = authFactory.getInstance(getInstanceProps); + const testPolicy = new Policy(stack, 'testPolicy', { + statements: [ + new PolicyStatement({ + actions: ['s3:GetObject'], + resources: ['testBucket/testObject/*'], + }), + ], + }); + const resourceAccessAcceptor = backendAuth.getResourceAccessAcceptor( + 'unauthenticatedUserIamRole' + ); + + assert.equal( + resourceAccessAcceptor.identifier, + 'unauthenticatedUserIamRoleResourceAccessAcceptor' + ); + + resourceAccessAcceptor.acceptResourceAccess(testPolicy, [ + { name: 'test', path: 'test' }, + ]); + const template = Template.fromStack(stack); + template.resourceCountIs('AWS::IAM::Policy', 1); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 's3:GetObject', + Effect: 'Allow', + Resource: 'testBucket/testObject/*', + }, + ], + }, + Roles: [backendAuth.resources.unauthenticatedUserIamRole.roleName], + }); + }); + }); +}); + +const resetFactoryCount = () => { + AmplifyAuthFactory.factoryCount = 0; +}; diff --git a/packages/backend-auth/src/reference_factory.ts b/packages/backend-auth/src/reference_factory.ts new file mode 100644 index 0000000000..81f4ff51b6 --- /dev/null +++ b/packages/backend-auth/src/reference_factory.ts @@ -0,0 +1,240 @@ +import { + AmplifyResourceGroupName, + AuthRoleName, + BackendOutputStorageStrategy, + ConstructContainerEntryGenerator, + ConstructFactory, + ConstructFactoryGetInstanceProps, + GenerateContainerEntryProps, + ReferenceAuthResources, + ResourceAccessAcceptor, + ResourceAccessAcceptorFactory, + ResourceProvider, + StackProvider, +} from '@aws-amplify/plugin-types'; +import { AuthAccessGenerator, Expand } from './types.js'; +import { authAccessBuilder as _authAccessBuilder } from './access_builder.js'; +import path from 'path'; +import { AmplifyUserError, TagName } from '@aws-amplify/platform-core'; +import { AuthAccessPolicyArbiterFactory } from './auth_access_policy_arbiter.js'; +import { Stack, Tags } from 'aws-cdk-lib'; +import { Policy } from 'aws-cdk-lib/aws-iam'; +import { UserPoolAccessPolicyFactory } from './userpool_access_policy_factory.js'; +import { AmplifyAuthFactory } from './factory.js'; +import { AmplifyReferenceAuth } from './reference_construct.js'; +import { AuthOutput } from '@aws-amplify/backend-output-schemas'; + +export type ReferenceAuthProps = { + /** + * @internal + */ + outputStorageStrategy?: BackendOutputStorageStrategy; + /** + * Existing UserPool Id + */ + userPoolId: string; + /** + * Existing IdentityPool Id + */ + identityPoolId: string; + /** + * Existing UserPoolClient Id + */ + userPoolClientId: string; + /** + * Existing AuthRole ARN + */ + authRoleArn: string; + /** + * Existing UnauthRole ARN + */ + unauthRoleArn: string; + /** + * A mapping of existing group names and their associated role ARNs + * which can be used for group permissions. + */ + groups?: { + [groupName: string]: string; + }; +}; + +export type BackendReferenceAuth = ResourceProvider & + ResourceAccessAcceptorFactory & + StackProvider; + +export type AmplifyReferenceAuthProps = Expand< + Omit & { + /** + * Configure access to auth for other Amplify resources + * @see https://docs.amplify.aws/react/build-a-backend/auth/grant-access-to-auth-resources/ + * @example + * access: (allow) => [allow.resource(postConfirmation).to(["addUserToGroup"])] + * @example + * access: (allow) => [allow.resource(groupManager).to(["manageGroups"])] + */ + access?: AuthAccessGenerator; + } +>; +/** + * Singleton factory for AmplifyReferenceAuth that can be used in Amplify project files. + * + * Exported for testing purpose only & should NOT be exported out of the package. + */ +export class AmplifyReferenceAuthFactory + implements ConstructFactory +{ + readonly provides = 'AuthResources'; + + private generator: ConstructContainerEntryGenerator; + + /** + * Set the properties that will be used to initialize AmplifyReferenceAuth + */ + constructor( + private readonly props: AmplifyReferenceAuthProps, + // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors + private readonly importStack = new Error().stack + ) { + if (AmplifyAuthFactory.factoryCount > 0) { + throw new AmplifyUserError('MultipleSingletonResourcesError', { + message: + 'Multiple `defineAuth` or `referenceAuth` calls are not allowed within an Amplify backend', + resolution: 'Remove all but one `defineAuth` or `referenceAuth` call', + }); + } + AmplifyAuthFactory.factoryCount++; + } + /** + * Get a singleton instance of AmplifyReferenceAuth + */ + getInstance = ( + getInstanceProps: ConstructFactoryGetInstanceProps + ): BackendReferenceAuth => { + const { constructContainer, importPathVerifier } = getInstanceProps; + importPathVerifier?.verify( + this.importStack, + path.join('amplify', 'auth', 'resource'), + 'Amplify Auth must be defined in amplify/auth/resource.ts' + ); + if (!this.generator) { + this.generator = new AmplifyReferenceAuthGenerator( + this.props, + getInstanceProps + ); + } + return constructContainer.getOrCompute( + this.generator + ) as BackendReferenceAuth; + }; +} +class AmplifyReferenceAuthGenerator + implements ConstructContainerEntryGenerator +{ + readonly resourceGroupName: AmplifyResourceGroupName = 'auth'; + private readonly name: string; + + constructor( + private readonly props: AmplifyReferenceAuthProps, + private readonly getInstanceProps: ConstructFactoryGetInstanceProps, + private readonly authAccessBuilder = _authAccessBuilder, + private readonly authAccessPolicyArbiterFactory = new AuthAccessPolicyArbiterFactory() + ) { + this.name = 'amplifyAuth'; + } + + generateContainerEntry = ({ + scope, + ssmEnvironmentEntriesGenerator, + }: GenerateContainerEntryProps) => { + const authProps: ReferenceAuthProps = { + ...this.props, + outputStorageStrategy: this.getInstanceProps.outputStorageStrategy, + }; + + let authConstruct: AmplifyReferenceAuth; + try { + authConstruct = new AmplifyReferenceAuth(scope, this.name, authProps); + } catch (error) { + throw new AmplifyUserError( + 'AmplifyReferenceAuthConstructInitializationError', + { + message: 'Failed to instantiate reference auth construct', + resolution: 'See the underlying error message for more details.', + }, + error as Error + ); + } + + Tags.of(authConstruct).add(TagName.FRIENDLY_NAME, this.name); + + const authConstructMixin: BackendReferenceAuth = { + ...authConstruct, + /** + * Returns a resourceAccessAcceptor for the given role + * @param roleIdentifier Either the auth or unauth role name or the name of a UserPool group + */ + getResourceAccessAcceptor: ( + roleIdentifier: AuthRoleName | string + ): ResourceAccessAcceptor => ({ + identifier: `${roleIdentifier}ResourceAccessAcceptor`, + acceptResourceAccess: (policy: Policy) => { + const role = roleNameIsAuthRoleName(roleIdentifier) + ? authConstruct.resources[roleIdentifier] + : authConstruct.resources.groups?.[roleIdentifier]?.role; + if (!role) { + throw new AmplifyUserError('InvalidResourceAccessConfigError', { + message: `No auth IAM role found for "${roleIdentifier}".`, + resolution: `If you are trying to configure UserPool group access, ensure that the group name is specified correctly.`, + }); + } + policy.attachToRole(role); + }, + }), + stack: Stack.of(authConstruct), + }; + if (!this.props.access) { + return authConstructMixin; + } + // props.access is the access callback defined by the customer + // here we inject the authAccessBuilder into the callback and run it + // this produces the access definition that will be used to create the auth access policies + const accessDefinition = this.props.access(this.authAccessBuilder); + + const ssmEnvironmentEntries = + ssmEnvironmentEntriesGenerator.generateSsmEnvironmentEntries({ + [`${this.name}_USERPOOL_ID`]: + authConstructMixin.resources.userPool.userPoolId, + }); + + const authPolicyArbiter = this.authAccessPolicyArbiterFactory.getInstance( + accessDefinition, + this.getInstanceProps, + ssmEnvironmentEntries, + new UserPoolAccessPolicyFactory(authConstruct.resources.userPool) + ); + + authPolicyArbiter.arbitratePolicies(); + + return authConstructMixin; + }; +} + +const roleNameIsAuthRoleName = (roleName: string): roleName is AuthRoleName => { + return ( + roleName === 'authenticatedUserIamRole' || + roleName === 'unauthenticatedUserIamRole' + ); +}; + +/** + * Provide references to existing auth resources. + */ +export const referenceAuth = ( + props: AmplifyReferenceAuthProps +): ConstructFactory => { + return new AmplifyReferenceAuthFactory( + props, + // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors + new Error().stack + ); +}; diff --git a/packages/backend-auth/src/test-resources/sample_data.ts b/packages/backend-auth/src/test-resources/sample_data.ts new file mode 100644 index 0000000000..1ac099f65b --- /dev/null +++ b/packages/backend-auth/src/test-resources/sample_data.ts @@ -0,0 +1,448 @@ +import { IdentityPool as IdentityPoolType } from '@aws-sdk/client-cognito-identity'; +import { + GetUserPoolMfaConfigCommandOutput, + ListGroupsResponse, + ProviderDescription, + UserPoolClientType, + UserPoolType, +} from '@aws-sdk/client-cognito-identity-provider'; +import { ReferenceAuthInitializerProps } from '../lambda/reference_auth_initializer.js'; +/** + * Sample referenceAuth properties + */ +export const SampleInputProperties: ReferenceAuthInitializerProps = { + authRoleArn: 'arn:aws:iam::000000000000:role/service-role/ref-auth-role-1', + unauthRoleArn: 'arn:aws:iam::000000000000:role/service-role/ref-unauth-role1', + identityPoolId: 'us-east-1:sample-identity-pool-id', + userPoolClientId: 'sampleUserPoolClientId', + userPoolId: 'us-east-1_userpoolTest', + groups: { + ADMINS: 'arn:aws:iam::000000000000:role/sample-group-role', + }, + region: 'us-east-1', +}; +/** + * Sample response from describe user pool command + */ +export const UserPool: Readonly = { + Id: SampleInputProperties.userPoolId, + Name: 'ref-auth-userpool-1', + Policies: { + PasswordPolicy: { + MinimumLength: 10, + RequireUppercase: true, + RequireLowercase: true, + RequireNumbers: true, + RequireSymbols: false, + TemporaryPasswordValidityDays: 7, + }, + }, + DeletionProtection: 'ACTIVE', + LambdaConfig: {}, + SchemaAttributes: [ + { + Name: 'profile', + AttributeDataType: 'String', + DeveloperOnlyAttribute: false, + Mutable: true, + Required: false, + StringAttributeConstraints: { + MinLength: '0', + MaxLength: '2048', + }, + }, + { + Name: 'address', + AttributeDataType: 'String', + DeveloperOnlyAttribute: false, + Mutable: true, + Required: false, + StringAttributeConstraints: { + MinLength: '0', + MaxLength: '2048', + }, + }, + { + Name: 'birthdate', + AttributeDataType: 'String', + DeveloperOnlyAttribute: false, + Mutable: true, + Required: false, + StringAttributeConstraints: { + MinLength: '10', + MaxLength: '10', + }, + }, + { + Name: 'gender', + AttributeDataType: 'String', + DeveloperOnlyAttribute: false, + Mutable: true, + Required: false, + StringAttributeConstraints: { + MinLength: '0', + MaxLength: '2048', + }, + }, + { + Name: 'preferred_username', + AttributeDataType: 'String', + DeveloperOnlyAttribute: false, + Mutable: true, + Required: false, + StringAttributeConstraints: { + MinLength: '0', + MaxLength: '2048', + }, + }, + { + Name: 'updated_at', + AttributeDataType: 'Number', + DeveloperOnlyAttribute: false, + Mutable: true, + Required: false, + NumberAttributeConstraints: { + MinValue: '0', + }, + }, + { + Name: 'website', + AttributeDataType: 'String', + DeveloperOnlyAttribute: false, + Mutable: true, + Required: false, + StringAttributeConstraints: { + MinLength: '0', + MaxLength: '2048', + }, + }, + { + Name: 'picture', + AttributeDataType: 'String', + DeveloperOnlyAttribute: false, + Mutable: true, + Required: false, + StringAttributeConstraints: { + MinLength: '0', + MaxLength: '2048', + }, + }, + { + Name: 'identities', + AttributeDataType: 'String', + DeveloperOnlyAttribute: false, + Mutable: true, + Required: false, + StringAttributeConstraints: {}, + }, + { + Name: 'sub', + AttributeDataType: 'String', + DeveloperOnlyAttribute: false, + Mutable: false, + Required: true, + StringAttributeConstraints: { + MinLength: '1', + MaxLength: '2048', + }, + }, + { + Name: 'phone_number', + AttributeDataType: 'String', + DeveloperOnlyAttribute: false, + Mutable: true, + Required: false, + StringAttributeConstraints: { + MinLength: '0', + MaxLength: '2048', + }, + }, + { + Name: 'phone_number_verified', + AttributeDataType: 'Boolean', + DeveloperOnlyAttribute: false, + Mutable: true, + Required: false, + }, + { + Name: 'zoneinfo', + AttributeDataType: 'String', + DeveloperOnlyAttribute: false, + Mutable: true, + Required: false, + StringAttributeConstraints: { + MinLength: '0', + MaxLength: '2048', + }, + }, + { + // eslint-disable-next-line spellcheck/spell-checker + Name: 'custom:duplicateemail', + AttributeDataType: 'String', + DeveloperOnlyAttribute: false, + Mutable: true, + Required: false, + StringAttributeConstraints: {}, + }, + { + Name: 'locale', + AttributeDataType: 'String', + DeveloperOnlyAttribute: false, + Mutable: true, + Required: false, + StringAttributeConstraints: { + MinLength: '0', + MaxLength: '2048', + }, + }, + { + Name: 'email', + AttributeDataType: 'String', + DeveloperOnlyAttribute: false, + Mutable: true, + Required: true, + StringAttributeConstraints: { + MinLength: '0', + MaxLength: '2048', + }, + }, + { + Name: 'email_verified', + AttributeDataType: 'Boolean', + DeveloperOnlyAttribute: false, + Mutable: true, + Required: false, + }, + { + Name: 'given_name', + AttributeDataType: 'String', + DeveloperOnlyAttribute: false, + Mutable: true, + Required: false, + StringAttributeConstraints: { + MinLength: '0', + MaxLength: '2048', + }, + }, + { + Name: 'family_name', + AttributeDataType: 'String', + DeveloperOnlyAttribute: false, + Mutable: true, + Required: false, + StringAttributeConstraints: { + MinLength: '0', + MaxLength: '2048', + }, + }, + { + Name: 'middle_name', + AttributeDataType: 'String', + DeveloperOnlyAttribute: false, + Mutable: true, + Required: false, + StringAttributeConstraints: { + MinLength: '0', + MaxLength: '2048', + }, + }, + { + Name: 'name', + AttributeDataType: 'String', + DeveloperOnlyAttribute: false, + Mutable: true, + Required: false, + StringAttributeConstraints: { + MinLength: '0', + MaxLength: '2048', + }, + }, + { + Name: 'nickname', + AttributeDataType: 'String', + DeveloperOnlyAttribute: false, + Mutable: true, + Required: false, + StringAttributeConstraints: { + MinLength: '0', + MaxLength: '2048', + }, + }, + ], + AutoVerifiedAttributes: ['email'], + UsernameAttributes: ['email'], + VerificationMessageTemplate: { + DefaultEmailOption: 'CONFIRM_WITH_CODE', + }, + UserAttributeUpdateSettings: { + AttributesRequireVerificationBeforeUpdate: ['email'], + }, + MfaConfiguration: 'ON', + EstimatedNumberOfUsers: 0, + EmailConfiguration: { + EmailSendingAccount: 'COGNITO_DEFAULT', + }, + UserPoolTags: {}, + Domain: 'ref-auth-userpool-1', + AdminCreateUserConfig: { + AllowAdminCreateUserOnly: false, + UnusedAccountValidityDays: 7, + }, + UsernameConfiguration: { + CaseSensitive: false, + }, + Arn: `arn:aws:cognito-idp:us-east-1:000000000000:userpool/${SampleInputProperties.userPoolId}`, + AccountRecoverySetting: { + RecoveryMechanisms: [ + { + Priority: 1, + Name: 'verified_email', + }, + ], + }, +}; + +export const UserPoolGroups: Readonly = { + Groups: [ + { + GroupName: 'sample-group-name', + RoleArn: 'arn:aws:iam::000000000000:role/sample-group-role', + }, + ], +}; + +/** + * Sample data from get user pool mfa config + */ +export const MFAResponse: Readonly< + Omit +> = { + SoftwareTokenMfaConfiguration: { + Enabled: true, + }, + MfaConfiguration: 'ON', +}; + +/** + * Sample data from list identity providers + */ +export const IdentityProviders: Readonly = [ + { + ProviderName: 'Facebook', + ProviderType: 'Facebook', + }, + { + ProviderName: 'Google', + ProviderType: 'Google', + }, + { + ProviderName: 'LoginWithAmazon', + ProviderType: 'LoginWithAmazon', + }, +]; + +/** + * Sample data for describe identity pool + */ +export const IdentityPool: Readonly = { + IdentityPoolId: SampleInputProperties.identityPoolId, + IdentityPoolName: 'sample-identity-pool-name', + AllowUnauthenticatedIdentities: true, + AllowClassicFlow: false, + CognitoIdentityProviders: [ + { + ProviderName: `cognito-idp.us-east-1.amazonaws.com/${SampleInputProperties.userPoolId}`, + ClientId: SampleInputProperties.userPoolClientId, + ServerSideTokenCheck: false, + }, + ], + IdentityPoolTags: {}, +}; + +/** + * Sample data for get identity pool roles + */ +export const IdentityPoolRoles = { + IdentityPoolId: SampleInputProperties.identityPoolId, + Roles: { + authenticated: SampleInputProperties.authRoleArn, + unauthenticated: SampleInputProperties.unauthRoleArn, + }, +}; + +/** + * Sample data from describe user pool client + */ +export const UserPoolClient: Readonly = { + UserPoolId: SampleInputProperties.userPoolId, + ClientName: 'ref-auth-app-client-1', + ClientId: SampleInputProperties.userPoolClientId, + RefreshTokenValidity: 30, + AccessTokenValidity: 60, + IdTokenValidity: 60, + TokenValidityUnits: { + AccessToken: 'minutes', + IdToken: 'minutes', + RefreshToken: 'days', + }, + ReadAttributes: [ + 'address', + 'birthdate', + // eslint-disable-next-line spellcheck/spell-checker + 'custom:duplicateemail', + 'email', + 'email_verified', + 'family_name', + 'gender', + 'given_name', + 'locale', + 'middle_name', + 'name', + 'nickname', + 'phone_number', + 'phone_number_verified', + 'picture', + 'preferred_username', + 'profile', + 'updated_at', + 'website', + 'zoneinfo', + ], + WriteAttributes: [ + 'address', + 'birthdate', + // eslint-disable-next-line spellcheck/spell-checker + 'custom:duplicateemail', + 'email', + 'family_name', + 'gender', + 'given_name', + 'locale', + 'middle_name', + 'name', + 'nickname', + 'phone_number', + 'picture', + 'preferred_username', + 'profile', + 'updated_at', + 'website', + 'zoneinfo', + ], + ExplicitAuthFlows: ['ALLOW_REFRESH_TOKEN_AUTH', 'ALLOW_USER_SRP_AUTH'], + SupportedIdentityProviders: [ + 'COGNITO', + 'Facebook', + 'Google', + 'LoginWithAmazon', + ], + CallbackURLs: ['https://redirect.com', 'https://redirect2.com'], + LogoutURLs: ['https://anotherlogouturl.com', 'https://logouturl.com'], + AllowedOAuthFlows: ['code'], + AllowedOAuthScopes: ['email', 'openid', 'phone'], + AllowedOAuthFlowsUserPoolClient: true, + PreventUserExistenceErrors: 'ENABLED', + EnableTokenRevocation: true, + EnablePropagateAdditionalUserContextData: false, + AuthSessionValidity: 3, +}; diff --git a/packages/backend-auth/src/translate_auth_props.ts b/packages/backend-auth/src/translate_auth_props.ts index f4b20fff37..fad144ef6b 100644 --- a/packages/backend-auth/src/translate_auth_props.ts +++ b/packages/backend-auth/src/translate_auth_props.ts @@ -6,7 +6,10 @@ import { GoogleProviderProps, OidcProviderProps, } from '@aws-amplify/auth-construct'; -import { BackendSecretResolver } from '@aws-amplify/plugin-types'; +import { + BackendSecretResolver, + ConstructFactoryGetInstanceProps, +} from '@aws-amplify/plugin-types'; import { AmazonProviderFactoryProps, AppleProviderFactoryProps, @@ -16,6 +19,8 @@ import { GoogleProviderFactoryProps, OidcProviderFactoryProps, } from './types.js'; +import { IFunction } from 'aws-cdk-lib/aws-lambda'; +import { AmplifyAuthProps } from './factory.js'; /** * Translate an Auth factory's loginWith to its Auth construct counterpart. Backend secret fields will be resolved @@ -79,6 +84,52 @@ export const translateToAuthConstructLoginWith = ( return result; }; +/** + * Translates the senders property from AmplifyAuthProps to AuthProps format. + * @param senders - The senders object from AmplifyAuthProps. + * @param getInstanceProps - Properties used to get an instance of the sender. + * @returns The translated senders object in AuthProps format, or undefined if no valid sender is provided. + * @description + * This function handles the translation of the 'senders' property, specifically for email senders. + * If no senders are provided or if there's no email sender, it returns undefined. + * If the email sender has a 'getInstance' method, it retrieves the Lambda function and returns it. + * Otherwise, it returns the email sender as is. + */ +export const translateToAuthConstructSenders = ( + senders: AmplifyAuthProps['senders'] | undefined, + getInstanceProps: ConstructFactoryGetInstanceProps +): AuthProps['senders'] | undefined => { + if (!senders || !senders.email) { + return undefined; + } + + // Handle CustomEmailSender type + if ('handler' in senders.email) { + const lambda: IFunction = + 'getInstance' in senders.email.handler + ? senders.email.handler.getInstance(getInstanceProps).resources.lambda + : senders.email.handler; + + return { + email: { + handler: lambda, + kmsKeyArn: senders.email.kmsKeyArn, + }, + }; + } + + // Handle SES configuration + if ('fromEmail' in senders.email) { + return { + email: senders.email, + }; + } + + // If none of the above, return the email configuration as-is + return { + email: senders.email, + }; +}; const translateAmazonProps = ( backendSecretResolver: BackendSecretResolver, diff --git a/packages/backend-auth/src/types.ts b/packages/backend-auth/src/types.ts index 8b7c018feb..46c199f1a1 100644 --- a/packages/backend-auth/src/types.ts +++ b/packages/backend-auth/src/types.ts @@ -8,6 +8,7 @@ import { OidcProviderProps, } from '@aws-amplify/auth-construct'; import { + AmplifyFunction, BackendSecret, ConstructFactory, ConstructFactoryGetInstanceProps, @@ -15,6 +16,7 @@ import { ResourceAccessAcceptorFactory, ResourceProvider, } from '@aws-amplify/plugin-types'; +import { IFunction } from 'aws-cdk-lib/aws-lambda'; /** * This utility allows us to expand nested types in auto complete prompts. @@ -252,3 +254,11 @@ export type ActionIam = | 'updateDeviceStatus' | 'updateGroup' | 'updateUserAttributes'; + +/** + * CustomEmailSender type for configuring a custom Lambda function for email sending + */ +export type CustomEmailSender = { + handler: ConstructFactory | IFunction; + kmsKeyArn?: string; +}; diff --git a/packages/backend-auth/tsconfig.json b/packages/backend-auth/tsconfig.json index b98614a812..42a487d8e7 100644 --- a/packages/backend-auth/tsconfig.json +++ b/packages/backend-auth/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "rootDir": "src", "outDir": "lib" }, "references": [ { "path": "../auth-construct" }, + { "path": "../backend-output-schemas" }, { "path": "../backend-output-storage" }, { "path": "../plugin-types" }, { "path": "../backend-platform-test-stubs" }, diff --git a/packages/backend-data/CHANGELOG.md b/packages/backend-data/CHANGELOG.md index 85ba09de84..2e415bac26 100644 --- a/packages/backend-data/CHANGELOG.md +++ b/packages/backend-data/CHANGELOG.md @@ -1,5 +1,57 @@ # @aws-amplify/backend-data +## 1.2.1 + +### Patch Changes + +- f1db886: add resourceGroupName prop to function +- Updated dependencies [f1db886] + - @aws-amplify/plugin-types@1.5.0 + +## 1.2.0 + +### Minor Changes + +- 90a7c49: Add support for referenceAuth. + +### Patch Changes + +- Updated dependencies [90a7c49] + - @aws-amplify/plugin-types@1.4.0 + +## 1.1.7 + +### Patch Changes + +- 583a3f2: Fix detection of AmplifyErrors + +## 1.1.6 + +### Patch Changes + +- b56d344: update aws-cdk lib to ^2.158.0 +- Updated dependencies [b56d344] + - @aws-amplify/backend-output-storage@1.1.3 + - @aws-amplify/plugin-types@1.3.1 + +## 1.1.5 + +### Patch Changes + +- 0d6489d: Update data-schema-types +- Updated dependencies [5f46d8d] + - @aws-amplify/backend-output-schemas@1.4.0 + +## 1.1.4 + +### Patch Changes + +- ffc3b42: update data construct +- e648e8e: added main field to package.json so these packages are resolvable +- Updated dependencies [8dd7286] + - @aws-amplify/backend-output-storage@1.1.2 + - @aws-amplify/plugin-types@1.2.2 + ## 1.1.3 ### Patch Changes diff --git a/packages/backend-data/package.json b/packages/backend-data/package.json index 8d993abce7..ba3ed7e55a 100644 --- a/packages/backend-data/package.json +++ b/packages/backend-data/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/backend-data", - "version": "1.1.3", + "version": "1.2.1", "type": "module", "publishConfig": { "access": "public" @@ -20,18 +20,18 @@ "license": "Apache-2.0", "devDependencies": { "@aws-amplify/data-schema": "^1.0.0", - "@aws-amplify/backend-platform-test-stubs": "^0.3.4", - "@aws-amplify/platform-core": "^1.0.7" + "@aws-amplify/backend-platform-test-stubs": "^0.3.6", + "@aws-amplify/platform-core": "^1.2.1" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0" }, "dependencies": { - "@aws-amplify/backend-output-storage": "^1.1.1", - "@aws-amplify/backend-output-schemas": "^1.1.0", - "@aws-amplify/data-construct": "^1.9.6", - "@aws-amplify/plugin-types": "^1.2.1", - "@aws-amplify/data-schema-types": "^1.1.1" + "@aws-amplify/backend-output-storage": "^1.1.3", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/data-construct": "^1.10.1", + "@aws-amplify/plugin-types": "^1.5.0", + "@aws-amplify/data-schema-types": "^1.2.0" } } diff --git a/packages/backend-data/src/convert_authorization_modes.test.ts b/packages/backend-data/src/convert_authorization_modes.test.ts index f685414449..f728c7bd72 100644 --- a/packages/backend-data/src/convert_authorization_modes.test.ts +++ b/packages/backend-data/src/convert_authorization_modes.test.ts @@ -36,6 +36,7 @@ void describe('buildConstructFactoryProvidedAuthConfig', () => { userPool: 'ThisIsAUserPool', authenticatedUserIamRole: 'ThisIsAnAuthenticatedUserIamRole', unauthenticatedUserIamRole: 'ThisIsAnUnauthenticatedUserIamRole', + identityPoolId: 'us-fake-1:123123-123123', cfnResources: { cfnIdentityPool: { logicalId: 'IdentityPoolLogicalId', diff --git a/packages/backend-data/src/convert_authorization_modes.ts b/packages/backend-data/src/convert_authorization_modes.ts index fadfec4fbb..02df9d4a04 100644 --- a/packages/backend-data/src/convert_authorization_modes.ts +++ b/packages/backend-data/src/convert_authorization_modes.ts @@ -20,6 +20,7 @@ import { import { AuthResources, ConstructFactoryGetInstanceProps, + ReferenceAuthResources, ResourceProvider, } from '@aws-amplify/plugin-types'; import { AmplifyUserError } from '@aws-amplify/platform-core'; @@ -38,14 +39,14 @@ export type ProvidedAuthConfig = { * Function instance provider which uses the */ export const buildConstructFactoryProvidedAuthConfig = ( - authResourceProvider: ResourceProvider | undefined + authResourceProvider: + | ResourceProvider + | undefined ): ProvidedAuthConfig | undefined => { if (!authResourceProvider) return; - return { userPool: authResourceProvider.resources.userPool, - identityPoolId: - authResourceProvider.resources.cfnResources.cfnIdentityPool.ref, + identityPoolId: authResourceProvider.resources.identityPoolId, authenticatedUserRole: authResourceProvider.resources.authenticatedUserIamRole, unauthenticatedUserRole: diff --git a/packages/backend-data/src/factory.test.ts b/packages/backend-data/src/factory.test.ts index 526409bc89..51e2aabf45 100644 --- a/packages/backend-data/src/factory.test.ts +++ b/packages/backend-data/src/factory.test.ts @@ -85,6 +85,7 @@ const createConstructContainerWithUserPoolAuthRegistered = ( authenticatedUserIamRole: new Role(stack, 'testAuthRole', { assumedBy: new ServicePrincipal('test.amazon.com'), }), + identityPoolId: 'identityPoolId', cfnResources: { cfnUserPool: new CfnUserPool(stack, 'CfnUserPool', {}), cfnUserPoolClient: new CfnUserPoolClient(stack, 'CfnUserPoolClient', { @@ -535,25 +536,8 @@ void describe('DataFactory', () => { 'Fn::Join': [ '', [ - 'arn:', { - Ref: 'AWS::Partition', - }, - ':appsync:', - { - Ref: 'AWS::Region', - }, - ':', - { - Ref: 'AWS::AccountId', - }, - // eslint-disable-next-line spellcheck/spell-checker - ':apis/', - { - 'Fn::GetAtt': [ - 'amplifyDataGraphQLAPI42A6FA33', - 'ApiId', - ], + 'Fn::GetAtt': ['amplifyDataGraphQLAPI42A6FA33', 'Arn'], }, '/types/Query/*', ], @@ -563,25 +547,8 @@ void describe('DataFactory', () => { 'Fn::Join': [ '', [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':appsync:', { - Ref: 'AWS::Region', - }, - ':', - { - Ref: 'AWS::AccountId', - }, - // eslint-disable-next-line spellcheck/spell-checker - ':apis/', - { - 'Fn::GetAtt': [ - 'amplifyDataGraphQLAPI42A6FA33', - 'ApiId', - ], + 'Fn::GetAtt': ['amplifyDataGraphQLAPI42A6FA33', 'Arn'], }, '/types/Mutation/*', ], @@ -591,25 +558,8 @@ void describe('DataFactory', () => { 'Fn::Join': [ '', [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':appsync:', - { - Ref: 'AWS::Region', - }, - ':', - { - Ref: 'AWS::AccountId', - }, - // eslint-disable-next-line spellcheck/spell-checker - ':apis/', { - 'Fn::GetAtt': [ - 'amplifyDataGraphQLAPI42A6FA33', - 'ApiId', - ], + 'Fn::GetAtt': ['amplifyDataGraphQLAPI42A6FA33', 'Arn'], }, '/types/Subscription/*', ], @@ -717,22 +667,8 @@ void describe('DataFactory', () => { 'Fn::Join': [ '', [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':appsync:', - { - Ref: 'AWS::Region', - }, - ':', { - Ref: 'AWS::AccountId', - }, - // eslint-disable-next-line spellcheck/spell-checker - ':apis/', - { - 'Fn::GetAtt': ['amplifyDataGraphQLAPI42A6FA33', 'ApiId'], + 'Fn::GetAtt': ['amplifyDataGraphQLAPI42A6FA33', 'Arn'], }, '/types/Mutation/*', ], @@ -757,22 +693,8 @@ void describe('DataFactory', () => { 'Fn::Join': [ '', [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':appsync:', - { - Ref: 'AWS::Region', - }, - ':', - { - Ref: 'AWS::AccountId', - }, - // eslint-disable-next-line spellcheck/spell-checker - ':apis/', { - 'Fn::GetAtt': ['amplifyDataGraphQLAPI42A6FA33', 'ApiId'], + 'Fn::GetAtt': ['amplifyDataGraphQLAPI42A6FA33', 'Arn'], }, '/types/Query/*', ], diff --git a/packages/backend-data/src/factory.ts b/packages/backend-data/src/factory.ts index c40ad8db70..92436ec6b1 100644 --- a/packages/backend-data/src/factory.ts +++ b/packages/backend-data/src/factory.ts @@ -1,12 +1,14 @@ import { IConstruct } from 'constructs'; import { AmplifyFunction, + AmplifyResourceGroupName, AuthResources, BackendOutputStorageStrategy, ConstructContainerEntryGenerator, ConstructFactory, ConstructFactoryGetInstanceProps, GenerateContainerEntryProps, + ReferenceAuthResources, ResourceProvider, } from '@aws-amplify/plugin-types'; import { @@ -97,9 +99,9 @@ export class DataFactory implements ConstructFactory { this.props, buildConstructFactoryProvidedAuthConfig( props.constructContainer - .getConstructFactory>( - 'AuthResources' - ) + .getConstructFactory< + ResourceProvider + >('AuthResources') ?.getInstance(props) ), props, @@ -111,7 +113,7 @@ export class DataFactory implements ConstructFactory { } class DataGenerator implements ConstructContainerEntryGenerator { - readonly resourceGroupName = 'data'; + readonly resourceGroupName: AmplifyResourceGroupName = 'data'; private readonly name: string; constructor( @@ -184,7 +186,7 @@ class DataGenerator implements ConstructContainerEntryGenerator { this.props.authorizationModes ); } catch (error) { - if (error instanceof AmplifyError) { + if (AmplifyError.isAmplifyError(error)) { throw error; } throw new AmplifyUserError( diff --git a/packages/backend-deployer/CHANGELOG.md b/packages/backend-deployer/CHANGELOG.md index 3055dcdd90..2e71d10a85 100644 --- a/packages/backend-deployer/CHANGELOG.md +++ b/packages/backend-deployer/CHANGELOG.md @@ -1,5 +1,63 @@ # @aws-amplify/backend-deployer +## 1.1.9 + +### Patch Changes + +- 7f2f68b: Handle errors when checking CDK bootstrap. +- 12cf209: update error mapping to catch when Lambda layer ARN regions do not match function region +- Updated dependencies [90a7c49] + - @aws-amplify/plugin-types@1.4.0 + +## 1.1.8 + +### Patch Changes + +- 583a3f2: Fix detection of AmplifyErrors +- Updated dependencies [583a3f2] + - @aws-amplify/platform-core@1.2.0 + +## 1.1.7 + +### Patch Changes + +- 7bf0c64: reclassify as error, UnknownFault, Error: The security token included in the request is expired +- 889bdb7: Handle case where synthesis renders empty cdk assembly +- a191fe5: add stack is in a state and can not be updated to error mapper + +## 1.1.6 + +### Patch Changes + +- b56d344: update aws-cdk lib to ^2.158.0 +- Updated dependencies [b56d344] + - @aws-amplify/plugin-types@1.3.1 + +## 1.1.5 + +### Patch Changes + +- 93d419f: detect more generic CFN deployment failure errors +- 777c80d: detect transform errors with multiple errors +- b35f01d: detect generic CFN stack creation errors + +## 1.1.4 + +### Patch Changes + +- 98673b0: Improve type error regex + +## 1.1.3 + +### Patch Changes + +- e648e8e: added main field to package.json so these packages are resolvable +- c9c873c: throw ESBuild error with correct messages +- cbac105: Handle CDK version mismatch +- e648e8e: added main field to packages known to lack one +- Updated dependencies [8dd7286] + - @aws-amplify/plugin-types@1.2.2 + ## 1.1.2 ### Patch Changes diff --git a/packages/backend-deployer/package.json b/packages/backend-deployer/package.json index 6165fd7fa9..484de0b34a 100644 --- a/packages/backend-deployer/package.json +++ b/packages/backend-deployer/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/backend-deployer", - "version": "1.1.2", + "version": "1.1.9", "type": "module", "publishConfig": { "access": "public" @@ -19,13 +19,13 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/platform-core": "^1.0.6", - "@aws-amplify/plugin-types": "^1.2.1", + "@aws-amplify/platform-core": "^1.2.0", + "@aws-amplify/plugin-types": "^1.4.0", "execa": "^8.0.1", "tsx": "^4.6.1" }, "peerDependencies": { - "aws-cdk": "^2.152.0", + "aws-cdk": "^2.158.0", "typescript": "^5.0.0" } } diff --git a/packages/backend-deployer/src/cdk_deployer.ts b/packages/backend-deployer/src/cdk_deployer.ts index 586f68d746..4989b12601 100644 --- a/packages/backend-deployer/src/cdk_deployer.ts +++ b/packages/backend-deployer/src/cdk_deployer.ts @@ -86,7 +86,7 @@ export class CDKDeployer implements BackendDeployer { } catch (typeError: unknown) { if ( synthError && - typeError instanceof AmplifyError && + AmplifyError.isAmplifyError(typeError) && typeError.cause?.message.match( /Cannot find module '\$amplify\/env\/.*' or its corresponding type declarations/ ) @@ -199,14 +199,24 @@ export class CDKDeployer implements BackendDeployer { // However if the cdk process didn't run or produced no output, then we have nothing to go on with. So we throw // this error to aid in some debugging. if (aggregatedStderr.trim()) { + // If the string is more than 65KB, truncate and keep the last portion. // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors - throw new Error(aggregatedStderr); + throw new Error(this.truncateString(aggregatedStderr, 65000)); } else { throw error; } } }; + private truncateString = (str: string, size: number) => { + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + const encoded = encoder.encode(str); + return encoded.byteLength > size + ? '...truncated...' + decoder.decode(encoded.slice(-size)) + : str; + }; + private getAppCommand = () => this.packageManagerController.getCommand([ 'tsx', diff --git a/packages/backend-deployer/src/cdk_error_mapper.test.ts b/packages/backend-deployer/src/cdk_error_mapper.test.ts index 7e6e1415de..3623763951 100644 --- a/packages/backend-deployer/src/cdk_error_mapper.test.ts +++ b/packages/backend-deployer/src/cdk_error_mapper.test.ts @@ -22,6 +22,15 @@ const testErrorMappings = [ errorName: 'ExpiredTokenError', expectedDownstreamErrorMessage: 'ExpiredToken', }, + { + errorMessage: + 'Error: The security token included in the request is expired', + expectedTopLevelErrorMessage: + 'The security token included in the request is invalid.', + errorName: 'ExpiredTokenError', + expectedDownstreamErrorMessage: + 'Error: The security token included in the request is expired', + }, { errorMessage: 'Access Denied', expectedTopLevelErrorMessage: @@ -55,6 +64,17 @@ const testErrorMappings = [ EOL + ` at lookup(/some_random/path.js: 1: 3005)`, }, + { + errorMessage: `TypeError [ERR_INVALID_MODULE_SPECIFIER]: Invalid module ..../function/foo/resource.ts is not a valid package name imported from +/Users/foo/Desktop/amplify-app/amplify/storage/foo/resource.ts + at new NodeError (node:internal/errors:405:5)`, + expectedTopLevelErrorMessage: + 'Unable to build the Amplify backend definition.', + errorName: 'SyntaxError', + expectedDownstreamErrorMessage: `TypeError [ERR_INVALID_MODULE_SPECIFIER]: Invalid module ..../function/foo/resource.ts is not a valid package name imported from +/Users/foo/Desktop/amplify-app/amplify/storage/foo/resource.ts + at new NodeError (node:internal/errors:405:5)`, + }, { errorMessage: 'Has the environment been bootstrapped', expectedTopLevelErrorMessage: @@ -76,6 +96,26 @@ const testErrorMappings = [ errorName: 'BootstrapNotDetectedError', expectedDownstreamErrorMessage: 'Is this account bootstrapped', }, + { + errorMessage: + // eslint-disable-next-line spellcheck/spell-checker + "This CDK deployment requires bootstrap stack version '6', but during the confirmation via SSM parameter /cdk-bootstrap/hnb659fds/version the following error occurred: AccessDeniedException", + expectedTopLevelErrorMessage: + 'Unable to detect CDK bootstrap stack due to permission issues.', + errorName: 'BootstrapDetectionError', + expectedDownstreamErrorMessage: + // eslint-disable-next-line spellcheck/spell-checker + "This CDK deployment requires bootstrap stack version '6', but during the confirmation via SSM parameter /cdk-bootstrap/hnb659fds/version the following error occurred: AccessDeniedException", + }, + { + errorMessage: + "This CDK deployment requires bootstrap stack version '6', found '5'. Please run 'cdk bootstrap'.", + expectedTopLevelErrorMessage: + 'This AWS account and region has outdated CDK bootstrap stack.', + errorName: 'BootstrapOutdatedError', + expectedDownstreamErrorMessage: + "This CDK deployment requires bootstrap stack version '6', found '5'. Please run 'cdk bootstrap'.", + }, { errorMessage: 'Amplify Backend not found in amplify/backend.ts', expectedTopLevelErrorMessage: @@ -122,6 +162,18 @@ const testErrorMappings = [ errorName: 'SecretNotSetError', expectedDownstreamErrorMessage: undefined, }, + { + errorMessage: `[31m some-stack failed: The stack named some-stack failed to deploy: UPDATE_ROLLBACK_COMPLETE: Resource handler returned message: The code contains one or more errors. (Service: AppSync, Status Code: 400, Request ID: 12345) (RequestToken: 123, HandlerErrorCode: GeneralServiceException), Embedded stack was not successfully updated. Currently in UPDATE_ROLLBACK_IN_PROGRESS with reason: The following resource(s) failed to create: [resource1, resource2]. [39m`, + expectedTopLevelErrorMessage: 'The CloudFormation deployment has failed.', + errorName: 'CloudFormationDeploymentError', + expectedDownstreamErrorMessage: `The stack named some-stack failed to deploy: UPDATE_ROLLBACK_COMPLETE: Resource handler returned message: The code contains one or more errors. (Service: AppSync, Status Code: 400, Request ID: 12345) (RequestToken: 123, HandlerErrorCode: GeneralServiceException), Embedded stack was not successfully updated. Currently in UPDATE_ROLLBACK_IN_PROGRESS with reason: The following resource(s) failed to create: [resource1, resource2]. [39m`, + }, + { + errorMessage: `[31m some-stack failed: The stack named some-stack failed creation, it may need to be manually deleted from the AWS console: ROLLBACK_COMPLETE`, + expectedTopLevelErrorMessage: 'The CloudFormation deployment has failed.', + errorName: 'CloudFormationDeploymentError', + expectedDownstreamErrorMessage: `The stack named some-stack failed creation, it may need to be manually deleted from the AWS console: ROLLBACK_COMPLETE`, + }, { errorMessage: 'CFN error happened: Updates are not allowed for property: some property', @@ -169,6 +221,29 @@ const testErrorMappings = [ EOL + ` at /Users/user/work-space/amplify-app/amplify/data/resource.ts:16:0`, }, + { + errorMessage: + `✘ [ERROR] Could not resolve "$amplify/env/defaultNodeFunctions"` + + EOL + + EOL + + ` amplify/func-src/handler.ts:1:20:` + + EOL + + ` 1 │ ...t { env } from '$amplify/env/defaultNodeFunctions';` + + EOL + + `1 error`, + expectedTopLevelErrorMessage: + 'Unable to build the Amplify backend definition.', + errorName: 'ESBuildError', + expectedDownstreamErrorMessage: + `✘ [ERROR] Could not resolve "$amplify/env/defaultNodeFunctions"` + + EOL + + EOL + + ` amplify/func-src/handler.ts:1:20:` + + EOL + + ` 1 │ ...t { env } from '$amplify/env/defaultNodeFunctions';` + + EOL + + `1 error`, + }, { errorMessage: `Error [TransformError]: Transform failed with 1 error:` + @@ -181,6 +256,22 @@ const testErrorMappings = [ errorName: 'ESBuildError', expectedDownstreamErrorMessage: undefined, }, + { + errorMessage: + `Error [TransformError]: Transform failed with 2 errors:` + + EOL + + `/Users/user/work-space/amplify-app/amplify/auth/resource.ts:48:4: ERROR: Multiple exports with the same name auth` + + EOL + + `/Users/user/work-space/amplify-app/amplify/auth/resource.ts:48:4: ERROR: The symbol auth has already been declared` + + EOL + + ` at failureErrorWithLog (/Users/user/work-space/amplify-app/node_modules/tsx/node_modules/esbuild/lib/main.js:1472:15)`, + expectedTopLevelErrorMessage: + `/Users/user/work-space/amplify-app/amplify/auth/resource.ts:48:4: ERROR: Multiple exports with the same name auth` + + EOL + + `/Users/user/work-space/amplify-app/amplify/auth/resource.ts:48:4: ERROR: The symbol auth has already been declared`, + errorName: 'ESBuildError', + expectedDownstreamErrorMessage: undefined, + }, { errorMessage: `some rubbish before` + @@ -259,6 +350,162 @@ const testErrorMappings = [ errorName: 'FilePermissionsError', expectedDownstreamErrorMessage: `EACCES: permission denied, unlink '.amplify/artifacts/cdk.out/synth.lock'`, }, + { + errorMessage: `This CDK CLI is not compatible with the CDK library used by your application. Please upgrade the CLI to the latest version. + (Cloud assembly schema version mismatch: Maximum schema version supported is 36.0.0, but found 36.1.1)`, + expectedTopLevelErrorMessage: + "Installed 'aws-cdk' is not compatible with installed 'aws-cdk-lib'.", + errorName: 'CDKVersionMismatchError', + expectedDownstreamErrorMessage: `This CDK CLI is not compatible with the CDK library used by your application. Please upgrade the CLI to the latest version. + (Cloud assembly schema version mismatch: Maximum schema version supported is 36.0.0, but found 36.1.1)`, + }, + { + errorMessage: `[31m amplify-some-stack failed: ValidationError: Stack:stack-arn is in UPDATE_ROLLBACK_FAILED state and can not be updated.`, + expectedTopLevelErrorMessage: + 'The CloudFormation deployment failed due to amplify-some-stack being in UPDATE_ROLLBACK_FAILED state.', + errorName: 'CloudFormationDeploymentError', + expectedDownstreamErrorMessage: undefined, + }, + { + errorMessage: `ENOENT: no such file or directory, open '.amplify/artifacts/cdk.out/manifest.json'`, + expectedTopLevelErrorMessage: + 'The Amplify backend definition is missing `defineBackend` call.', + errorName: 'MissingDefineBackendError', + expectedDownstreamErrorMessage: undefined, + }, + { + errorMessage: `ENOENT: no such file or directory, open '.amplify\\artifacts\\cdk.out\\manifest.json'`, + expectedTopLevelErrorMessage: + 'The Amplify backend definition is missing `defineBackend` call.', + errorName: 'MissingDefineBackendError', + expectedDownstreamErrorMessage: undefined, + }, + { + errorMessage: `User: is not authorized to perform: lambda:GetLayerVersion on resource: because no resource-based policy allows the lambda:GetLayerVersion action`, + expectedTopLevelErrorMessage: 'Unable to get Lambda layer version', + errorName: 'GetLambdaLayerVersionError', + expectedDownstreamErrorMessage: undefined, + }, + { + // eslint-disable-next-line spellcheck/spell-checker + errorMessage: `Error: npm error code EJSONPARSE +npm error path /home/some-path/package.json +npm error JSON.parse Expected double-quoted property name in JSON at position 868 while parsing near ...sbuild\\: \\^0.20.2\\,\\n<<<<<<< HEAD\\n\\t\\t\\hl-j... +npm error JSON.parse Failed to parse JSON data. +npm error JSON.parse Note: package.json must be actual JSON, not just JavaScript. +npm error A complete log of this run can be found in: /home/some-path/.npm/_logs/2024-10-01T19_56_46_705Z-debug-0.log`, + expectedTopLevelErrorMessage: + 'The /home/some-path/package.json is not a valid JSON.', + errorName: 'InvalidPackageJsonError', + expectedDownstreamErrorMessage: undefined, + }, + { + errorMessage: `Error: some-stack failed: ValidationError: User: is not authorized to perform: ssm:GetParameters on resource: because no identity-based policy allows the ssm:GetParameters action`, + expectedTopLevelErrorMessage: + 'Unable to deploy due to insufficient permissions', + errorName: 'AccessDeniedError', + expectedDownstreamErrorMessage: undefined, + }, + { + errorMessage: + `Error: Transform failed with 1 error:` + + EOL + + `/Users/some-path/amplify/storage/resource.ts:1:2: ERROR: Expected identifier but found }` + + EOL + + `at failureErrorWithLog (/Users/some-path/esbuild/lib/main.js:123:45)` + + EOL + + `at /Users/some-path/esbuild/lib/main.js:678:90`, + expectedTopLevelErrorMessage: + '/Users/some-path/amplify/storage/resource.ts:1:2: ERROR: Expected identifier but found }', + errorName: 'ESBuildError', + expectedDownstreamErrorMessage: undefined, + }, + { + errorMessage: + `Error [TransformError]:` + + EOL + + `You installed esbuild for another platform than the one you're currently using. + This won't work because esbuild is written with native code and needs to + install a platform-specific binary executable.` + + EOL + + `Specifically the @esbuild/linux-arm64 package is present but this platform + needs the @esbuild/darwin-arm64 package instead. People often get into this + situation by installing esbuild on Windows or macOS and copying node_modules + into a Docker image that runs Linux, or by copying node_modules between + Windows and WSL environments.` + + EOL + + `If you are installing with npm, you can try not copying the node_modules + directory when you copy the files over, and running npm ci or npm install + on the destination platform after the copy. Or you could consider using yarn + instead of npm which has built-in support for installing a package on multiple + platforms simultaneously.` + + EOL + + `If you are installing with yarn, you can try listing both this platform and the + other platform in your .yarnrc.yml file using the supportedArchitectures + feature: https://yarnpkg.com/configuration/yarnrc/#supportedArchitectures + Keep in mind that this means multiple copies of esbuild will be present.` + + EOL + + // eslint-disable-next-line spellcheck/spell-checker + `Another alternative is to use the esbuild-wasm package instead, which works + the same way on all platforms. But it comes with a heavy performance cost and + can sometimes be 10x slower than the esbuild package, so you may also not want to do that.`, + expectedTopLevelErrorMessage: + `You installed esbuild for another platform than the one you're currently using. + This won't work because esbuild is written with native code and needs to + install a platform-specific binary executable.` + + EOL + + `Specifically the @esbuild/linux-arm64 package is present but this platform + needs the @esbuild/darwin-arm64 package instead. People often get into this + situation by installing esbuild on Windows or macOS and copying node_modules + into a Docker image that runs Linux, or by copying node_modules between + Windows and WSL environments.` + + EOL + + `If you are installing with npm, you can try not copying the node_modules + directory when you copy the files over, and running npm ci or npm install + on the destination platform after the copy. Or you could consider using yarn + instead of npm which has built-in support for installing a package on multiple + platforms simultaneously.` + + EOL + + `If you are installing with yarn, you can try listing both this platform and the + other platform in your .yarnrc.yml file using the supportedArchitectures + feature: https://yarnpkg.com/configuration/yarnrc/#supportedArchitectures + Keep in mind that this means multiple copies of esbuild will be present.` + + EOL + + // eslint-disable-next-line spellcheck/spell-checker + `Another alternative is to use the esbuild-wasm package instead, which works + the same way on all platforms. But it comes with a heavy performance cost and + can sometimes be 10x slower than the esbuild package, so you may also not want to do that.`, + errorName: 'ESBuildError', + expectedDownstreamErrorMessage: undefined, + }, + { + errorMessage: + `Error [TransformError]: The package esbuild-package could not be found, and is needed by esbuild.` + + EOL + + `If you are installing esbuild with npm, make sure that you don't specify the +--no-optional or --omit=optional flags. The optionalDependencies feature +of package.json is used by esbuild to install the correct binary executable +for your current platform. +` + + EOL + + `at generateBinPath (/Users/some-path/esbuild/lib/main.js:123:45)` + + EOL + + `at /Users/some-path/esbuild/lib/main.js:678:90`, + expectedTopLevelErrorMessage: + `The package esbuild-package could not be found, and is needed by esbuild.` + + EOL + + `If you are installing esbuild with npm, make sure that you don't specify the +--no-optional or --omit=optional flags. The optionalDependencies feature +of package.json is used by esbuild to install the correct binary executable +for your current platform. +` + + EOL + + `at generateBinPath (/Users/some-path/esbuild/lib/main.js:123:45)` + + EOL + + `at /Users/some-path/esbuild/lib/main.js:678:90`, + errorName: 'ESBuildError', + expectedDownstreamErrorMessage: undefined, + }, ]; void describe('invokeCDKCommand', { concurrency: 1 }, () => { @@ -274,8 +521,8 @@ void describe('invokeCDKCommand', { concurrency: 1 }, () => { const humanReadableError = cdkErrorMapper.getAmplifyError( new Error(errorMessage) ); - assert.equal(humanReadableError.message, expectedTopLevelErrorMessage); assert.equal(humanReadableError.name, expectedErrorName); + assert.equal(humanReadableError.message, expectedTopLevelErrorMessage); expectedDownstreamErrorMessage && assert.equal( humanReadableError.cause?.message, diff --git a/packages/backend-deployer/src/cdk_error_mapper.ts b/packages/backend-deployer/src/cdk_error_mapper.ts index a00715d08c..0b41aff219 100644 --- a/packages/backend-deployer/src/cdk_error_mapper.ts +++ b/packages/backend-deployer/src/cdk_error_mapper.ts @@ -40,16 +40,28 @@ export class CdkErrorMapper { if (matchGroups.groups) { for (const [key, value] of Object.entries(matchGroups.groups)) { const placeHolder = `{${key}}`; - if (matchingError.humanReadableErrorMessage.includes(placeHolder)) { + if ( + matchingError.humanReadableErrorMessage.includes(placeHolder) || + matchingError.resolutionMessage.includes(placeHolder) + ) { matchingError.humanReadableErrorMessage = matchingError.humanReadableErrorMessage.replace( placeHolder, value ); + + matchingError.resolutionMessage = + matchingError.resolutionMessage.replace(placeHolder, value); // reset the stderr dump in the underlying error underlyingError = undefined; } } + // remove any trailing EOL + matchingError.humanReadableErrorMessage = + matchingError.humanReadableErrorMessage.replace( + new RegExp(`${this.multiLineEolRegex}$`), + '' + ); } else { underlyingError.message = matchGroups[0]; } @@ -84,10 +96,12 @@ export class CdkErrorMapper { classification: AmplifyErrorClassification; }> => [ { - errorRegex: /ExpiredToken/, + errorRegex: + /ExpiredToken|Error: The security token included in the request is expired/, humanReadableErrorMessage: 'The security token included in the request is invalid.', - resolutionMessage: 'Ensure your local AWS credentials are valid.', + resolutionMessage: + "Please update your AWS credentials. You can do this by running `aws configure` or by updating your AWS credentials file. If you're using temporary credentials, you may need to obtain new ones.", errorName: 'ExpiredTokenError', classification: 'ERROR', }, @@ -110,9 +124,39 @@ export class CdkErrorMapper { errorName: 'BootstrapNotDetectedError', classification: 'ERROR', }, + { + errorRegex: + /This CDK deployment requires bootstrap stack version \S+, found \S+\. Please run 'cdk bootstrap'\./, + humanReadableErrorMessage: + 'This AWS account and region has outdated CDK bootstrap stack.', + resolutionMessage: + 'Run `cdk bootstrap aws://{YOUR_ACCOUNT_ID}/{YOUR_REGION}` locally to re-bootstrap.', + errorName: 'BootstrapOutdatedError', + classification: 'ERROR', + }, + { + errorRegex: + /This CDK deployment requires bootstrap stack version \S+, but during the confirmation via SSM parameter \S+ the following error occurred: AccessDeniedException/, + humanReadableErrorMessage: + 'Unable to detect CDK bootstrap stack due to permission issues.', + resolutionMessage: + "Ensure that AWS credentials have an IAM policy that grants read access to 'arn:aws:ssm:*:*:parameter/cdk-bootstrap/*' SSM parameters.", + errorName: 'BootstrapDetectionError', + classification: 'ERROR', + }, + { + errorRegex: + /This CDK CLI is not compatible with the CDK library used by your application\. Please upgrade the CLI to the latest version\./, + humanReadableErrorMessage: + "Installed 'aws-cdk' is not compatible with installed 'aws-cdk-lib'.", + resolutionMessage: + "Make sure that version of 'aws-cdk' is greater or equal to version of 'aws-cdk-lib'", + errorName: 'CDKVersionMismatchError', + classification: 'ERROR', + }, { errorRegex: new RegExp( - `(SyntaxError|ReferenceError|TypeError):((?:.|${this.multiLineEolRegex})*?at .*)` + `(SyntaxError|ReferenceError|TypeError)( \\[[A-Z_]+])?:((?:.|${this.multiLineEolRegex})*?at .*)` ), humanReadableErrorMessage: 'Unable to build the Amplify backend definition.', @@ -158,6 +202,25 @@ export class CdkErrorMapper { errorName: 'MultipleSandboxInstancesError', classification: 'ERROR', }, + { + errorRegex: + /User:(.*) is not authorized to perform: lambda:GetLayerVersion on resource:(.*) because no resource-based policy allows the lambda:GetLayerVersion action/, + humanReadableErrorMessage: 'Unable to get Lambda layer version', + resolutionMessage: + 'Make sure layer ARNs are correct and layer regions match function region', + errorName: 'GetLambdaLayerVersionError', + classification: 'ERROR', + }, + { + errorRegex: + /User:(.*) is not authorized to perform:(.*) on resource:(?.*) because no identity-based policy allows the (?.*) action/, + humanReadableErrorMessage: + 'Unable to deploy due to insufficient permissions', + resolutionMessage: + 'Ensure you have permissions to call {action} for {resource}', + errorName: 'AccessDeniedError', + classification: 'ERROR', + }, { // Also extracts the first line in the stack where the error happened errorRegex: new RegExp( @@ -171,8 +234,21 @@ export class CdkErrorMapper { classification: 'ERROR', }, { + // Also extracts the first line in the stack where the error happened + errorRegex: new RegExp( + `[✘X] \\[ERROR\\] ((?:.|${this.multiLineEolRegex})*error.*)` + ), + humanReadableErrorMessage: + 'Unable to build the Amplify backend definition.', + resolutionMessage: + 'Check your backend definition in the `amplify` folder for syntax and type errors.', + errorName: 'ESBuildError', + classification: 'ERROR', + }, + { + // If there are multiple errors, capture all lines containing the errors errorRegex: new RegExp( - `\\[TransformError\\]: Transform failed with .* error:${this.multiLineEolRegex}(?.*)` + `(\\[TransformError\\]|Error): Transform failed with .* error(s?):${this.multiLineEolRegex}(?(.*ERROR:.*${this.multiLineEolRegex})+)` ), humanReadableErrorMessage: '{esBuildErrorMessage}', resolutionMessage: @@ -180,6 +256,17 @@ export class CdkErrorMapper { errorName: 'ESBuildError', classification: 'ERROR', }, + { + // Captures other forms of transform error + errorRegex: new RegExp( + `Error \\[TransformError\\]:(${this.multiLineEolRegex}|\\s)?(?(.*(${this.multiLineEolRegex})?)+)` + ), + humanReadableErrorMessage: '{esBuildErrorMessage}', + resolutionMessage: + 'Make sure esbuild is installed and is compatible with the platform you are currently using.', + errorName: 'ESBuildError', + classification: 'ERROR', + }, { errorRegex: /Amplify Backend not found in/, humanReadableErrorMessage: @@ -218,6 +305,15 @@ export class CdkErrorMapper { errorName: 'CFNUpdateNotSupportedError', classification: 'ERROR', }, + { + errorRegex: new RegExp( + `npm error code EJSONPARSE${this.multiLineEolRegex}npm error path (?.*/package\\.json)${this.multiLineEolRegex}(npm error (.*)${this.multiLineEolRegex})*` + ), + humanReadableErrorMessage: 'The {filePath} is not a valid JSON.', + resolutionMessage: `Check package.json file and make sure it is a valid JSON.`, + errorName: 'InvalidPackageJsonError', + classification: 'ERROR', + }, { // Error: .* is printed to stderr during cdk synth // Also extracts the first line in the stack where the error happened @@ -232,6 +328,20 @@ export class CdkErrorMapper { errorName: 'BackendSynthError', classification: 'ERROR', }, + { + // This happens when 'defineBackend' call is missing in customer's app. + // 'defineBackend' creates CDK app in memory. If it's missing then no cdk.App exists in memory and nothing is rendered. + // During 'cdk synth' CDK CLI attempts to read CDK assembly after calling customer's app. + // But no files are rendered causing it to fail. + errorRegex: + /ENOENT: no such file or directory, open '\.amplify.artifacts.cdk\.out.manifest\.json'/, + humanReadableErrorMessage: + 'The Amplify backend definition is missing `defineBackend` call.', + resolutionMessage: + 'Check your backend definition in the `amplify` folder. Ensure that `amplify/backend.ts` contains `defineBackend` call.', + errorName: 'MissingDefineBackendError', + classification: 'ERROR', + }, { // "Catch all": the backend entry point file is referenced in the stack indicating a problem in customer code errorRegex: /amplify\/backend/, @@ -252,10 +362,20 @@ export class CdkErrorMapper { errorName: 'SecretNotSetError', classification: 'ERROR', }, + { + errorRegex: + /(?amplify-[a-z0-9-]+)(.*) failed: ValidationError: Stack:(.*) is in (?.*) state and can not be updated/, + humanReadableErrorMessage: + 'The CloudFormation deployment failed due to {stackName} being in {state} state.', + resolutionMessage: + 'Find more information in the CloudFormation AWS Console for this stack.', + errorName: 'CloudFormationDeploymentError', + classification: 'ERROR', + }, { // Note that the order matters, this should be the last as it captures generic CFN error errorRegex: new RegExp( - `Deployment failed: (.*)${this.multiLineEolRegex}` + `Deployment failed: (.*)${this.multiLineEolRegex}|The stack named (.*) failed (to deploy:|creation,) (.*)` ), humanReadableErrorMessage: 'The CloudFormation deployment has failed.', resolutionMessage: @@ -271,15 +391,20 @@ export type CDKDeploymentError = | 'BackendBuildError' | 'BackendSynthError' | 'BootstrapNotDetectedError' + | 'BootstrapDetectionError' + | 'BootstrapOutdatedError' | 'CDKResolveAWSAccountError' + | 'CDKVersionMismatchError' | 'CFNUpdateNotSupportedError' | 'CloudFormationDeploymentError' | 'FilePermissionsError' + | 'MissingDefineBackendError' | 'MultipleSandboxInstancesError' | 'ESBuildError' | 'ExpiredTokenError' | 'FileConventionError' - | 'FileConventionError' | 'ModuleNotFoundError' + | 'InvalidPackageJsonError' | 'SecretNotSetError' - | 'SyntaxError'; + | 'SyntaxError' + | 'GetLambdaLayerVersionError'; diff --git a/packages/backend-function/CHANGELOG.md b/packages/backend-function/CHANGELOG.md index 68161b9d38..12240777bd 100644 --- a/packages/backend-function/CHANGELOG.md +++ b/packages/backend-function/CHANGELOG.md @@ -1,5 +1,86 @@ # @aws-amplify/backend-function +## 1.8.0 + +### Minor Changes + +- f1db886: add resourceGroupName prop to function + +### Patch Changes + +- Updated dependencies [f1db886] + - @aws-amplify/plugin-types@1.5.0 + +## 1.7.5 + +### Patch Changes + +- 12cf209: update error mapping to catch when Lambda layer ARN regions do not match function region +- Updated dependencies [90a7c49] + - @aws-amplify/plugin-types@1.4.0 + +## 1.7.4 + +### Patch Changes + +- 4e97389: add validation if layer arn region does not match function region + +## 1.7.3 + +### Patch Changes + +- b56d344: update aws-cdk lib to ^2.158.0 +- Updated dependencies [b56d344] + - @aws-amplify/backend-output-storage@1.1.3 + - @aws-amplify/plugin-types@1.3.1 + +## 1.7.2 + +### Patch Changes + +- 601a2c1: dedupe environment variables in amplify env type generator + +## 1.7.1 + +### Patch Changes + +- bd4ff4d: Fix jsdocs that incorrectly state default memory settings +- Updated dependencies [5f46d8d] + - @aws-amplify/backend-output-schemas@1.4.0 + +## 1.7.0 + +### Minor Changes + +- 4720412: Add minify option to defineFunction + +## 1.6.0 + +### Minor Changes + +- f5d0ab4: adds support to reference existing layers in defineFunction + +## 1.5.0 + +### Minor Changes + +- 87dbf41: expose stack property for backend, function resource, storage resource, and auth resource + +### Patch Changes + +- Updated dependencies [87dbf41] + - @aws-amplify/plugin-types@1.3.0 + +## 1.4.1 + +### Patch Changes + +- e648e8e: added main field to package.json so these packages are resolvable +- c9c873c: throw ESBuild error with correct messages +- Updated dependencies [8dd7286] + - @aws-amplify/backend-output-storage@1.1.2 + - @aws-amplify/plugin-types@1.2.2 + ## 1.4.0 ### Minor Changes diff --git a/packages/backend-function/package.json b/packages/backend-function/package.json index bdb9e45982..412c6be0be 100644 --- a/packages/backend-function/package.json +++ b/packages/backend-function/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/backend-function", - "version": "1.4.0", + "version": "1.8.0", "type": "module", "publishConfig": { "access": "public" @@ -19,20 +19,20 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/backend-output-schemas": "^1.1.0", - "@aws-amplify/backend-output-storage": "^1.1.1", - "@aws-amplify/plugin-types": "^1.2.1", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/backend-output-storage": "^1.1.3", + "@aws-amplify/plugin-types": "^1.5.0", "execa": "^8.0.1" }, "devDependencies": { - "@aws-amplify/backend-platform-test-stubs": "^0.3.4", - "@aws-amplify/platform-core": "^1.1.0", + "@aws-amplify/backend-platform-test-stubs": "^0.3.6", + "@aws-amplify/platform-core": "^1.2.1", "@aws-sdk/client-ssm": "^3.624.0", "aws-sdk": "^2.1550.0", "uuid": "^9.0.1" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0" } } diff --git a/packages/backend-function/src/factory.test.ts b/packages/backend-function/src/factory.test.ts index 316271131e..95624b523b 100644 --- a/packages/backend-function/src/factory.test.ts +++ b/packages/backend-function/src/factory.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, it, mock } from 'node:test'; +import { after, beforeEach, describe, it, mock } from 'node:test'; import { App, Stack } from 'aws-cdk-lib'; import { ConstructFactoryGetInstanceProps, @@ -17,6 +17,8 @@ import { NodeVersion, defineFunction } from './factory.js'; import { lambdaWithDependencies } from './test-assets/lambda-with-dependencies/resource.js'; import { Runtime } from 'aws-cdk-lib/aws-lambda'; import { Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import fsp from 'fs/promises'; +import path from 'node:path'; const createStackAndSetContext = (): Stack => { const app = new App(); @@ -52,6 +54,14 @@ void describe('AmplifyFunctionFactory', () => { }; }); + after(async () => { + // clean up generated env files + await fsp.rm(path.join(process.cwd(), '.amplify'), { + recursive: true, + force: true, + }); + }); + void it('creates singleton function instance', () => { const functionFactory = defaultLambda; const instance1 = functionFactory.getInstance(getInstanceProps); @@ -59,10 +69,16 @@ void describe('AmplifyFunctionFactory', () => { assert.strictEqual(instance1, instance2); }); + void it('verifies stack property exists and is equal to function stack', () => { + const functionFactory = defaultLambda; + const lambda = functionFactory.getInstance(getInstanceProps); + assert.equal(lambda.stack, Stack.of(lambda.resources.lambda)); + }); + void it('resolves default name and entry when no args specified', () => { const functionFactory = defaultLambda; const lambda = functionFactory.getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.resourceCountIs('AWS::Lambda::Function', 1); template.hasResourceProperties('AWS::Lambda::Function', { Handler: 'index.handler', @@ -79,7 +95,7 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', }); const lambda = functionFactory.getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.resourceCountIs('AWS::Lambda::Function', 1); template.hasResourceProperties('AWS::Lambda::Function', { Handler: 'index.handler', @@ -96,7 +112,7 @@ void describe('AmplifyFunctionFactory', () => { name: 'myCoolLambda', }); const lambda = functionFactory.getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.resourceCountIs('AWS::Lambda::Function', 1); template.hasResourceProperties('AWS::Lambda::Function', { Handler: 'index.handler', @@ -113,7 +129,7 @@ void describe('AmplifyFunctionFactory', () => { name: 'myCoolLambda', }); const lambda = functionFactory.getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.resourceCountIs('AWS::Lambda::Function', 1); template.hasResourceProperties('AWS::Lambda::Function', { Tags: [{ Key: 'amplify:friendly-name', Value: 'myCoolLambda' }], @@ -137,7 +153,7 @@ void describe('AmplifyFunctionFactory', () => { void it('builds lambda with local and 3p dependencies', () => { const lambda = lambdaWithDependencies.getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); // There isn't a way to check the contents of the bundled lambda using the CDK Template utility // So we just check that the lambda was created properly in the CFN template. // There is an e2e test that validates proper lambda bundling @@ -159,7 +175,7 @@ void describe('AmplifyFunctionFactory', () => { }); const lambda = functionFactory.getInstance(getInstanceProps); lambda.addEnvironment('key1', 'value1'); - const stack = Stack.of(lambda.resources.lambda); + const stack = lambda.stack; const template = Template.fromStack(stack); template.resourceCountIs('AWS::Lambda::Function', 1); template.hasResourceProperties('AWS::Lambda::Function', { @@ -177,7 +193,7 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', timeoutSeconds: 10, }).getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.hasResourceProperties('AWS::Lambda::Function', { Timeout: 10, @@ -230,7 +246,7 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', memoryMB: 234, }).getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.hasResourceProperties('AWS::Lambda::Function', { MemorySize: 234, @@ -241,7 +257,7 @@ void describe('AmplifyFunctionFactory', () => { const lambda = defineFunction({ entry: './test-assets/default-lambda/handler.ts', }).getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.hasResourceProperties('AWS::Lambda::Function', { MemorySize: 512, @@ -294,7 +310,7 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', runtime: 16, }).getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.hasResourceProperties('AWS::Lambda::Function', { Runtime: Runtime.NODEJS_16_X.name, @@ -305,7 +321,7 @@ void describe('AmplifyFunctionFactory', () => { const lambda = defineFunction({ entry: './test-assets/default-lambda/handler.ts', }).getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.hasResourceProperties('AWS::Lambda::Function', { Runtime: Runtime.NODEJS_18_X.name, @@ -340,7 +356,7 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', schedule: 'every 5m', }).getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.hasResourceProperties('AWS::Events::Rule', { ScheduleExpression: 'cron(*/5 * * * ? *)', @@ -361,7 +377,7 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', schedule: '0 1 * * ?', }).getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.hasResourceProperties('AWS::Events::Rule', { ScheduleExpression: 'cron(0 1 * * ? *)', @@ -382,7 +398,7 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', schedule: ['0 1 * * ?', 'every 5m'], }).getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.resourceCountIs('AWS::Events::Rule', 2); @@ -399,12 +415,31 @@ void describe('AmplifyFunctionFactory', () => { const lambda = defineFunction({ entry: './test-assets/default-lambda/handler.ts', }).getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.resourceCountIs('AWS::Events::Rule', 0); }); }); + void describe('minify property', () => { + void it('sets minify to false', () => { + const lambda = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + bundling: { + minify: false, + }, + }).getInstance(getInstanceProps); + const template = Template.fromStack(lambda.stack); + // There isn't a way to check the contents of the bundled lambda using the CDK Template utility + // So we just check that the lambda was created properly in the CFN template. + // There is an e2e test that validates proper lambda bundling + template.resourceCountIs('AWS::Lambda::Function', 1); + template.hasResourceProperties('AWS::Lambda::Function', { + Handler: 'index.handler', + }); + }); + }); + void describe('resourceAccessAcceptor', () => { void it('attaches policy to execution role and configures ssm environment context', () => { const functionFactory = defineFunction({ @@ -412,7 +447,7 @@ void describe('AmplifyFunctionFactory', () => { name: 'myCoolLambda', }); const lambda = functionFactory.getInstance(getInstanceProps); - const stack = Stack.of(lambda.resources.lambda); + const stack = lambda.stack; const policy = new Policy(stack, 'testPolicy', { statements: [ new PolicyStatement({ @@ -503,9 +538,7 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', name: 'anotherName', }); - const functionStack = Stack.of( - functionFactory.getInstance(getInstanceProps).resources.lambda - ); + const functionStack = functionFactory.getInstance(getInstanceProps).stack; anotherFunction.getInstance(getInstanceProps); const template = Template.fromStack(functionStack); assert.equal( diff --git a/packages/backend-function/src/function_env_translator.test.ts b/packages/backend-function/src/function_env_translator.test.ts index c20535b61a..b2f6f96fa5 100644 --- a/packages/backend-function/src/function_env_translator.test.ts +++ b/packages/backend-function/src/function_env_translator.test.ts @@ -1,5 +1,5 @@ import { Construct } from 'constructs'; -import { describe, it } from 'node:test'; +import { after, describe, it } from 'node:test'; import { FunctionEnvironmentTranslator } from './function_env_translator.js'; import { BackendIdentifier, @@ -13,6 +13,8 @@ import { ParameterPathConversions } from '@aws-amplify/platform-core'; import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda'; import { Template } from 'aws-cdk-lib/assertions'; import { FunctionEnvironmentTypeGenerator } from './function_env_type_generator.js'; +import path from 'node:path'; +import fsp from 'fs/promises'; const testStack = {} as Construct; @@ -55,6 +57,14 @@ class TestBackendSecret implements BackendSecret { void describe('FunctionEnvironmentTranslator', () => { const backendResolver = new TestBackendSecretResolver(); + after(async () => { + // clean up generated env files + await fsp.rm(path.join(process.cwd(), '.amplify'), { + recursive: true, + force: true, + }); + }); + void it('translates env props that do not contain secrets', () => { const functionEnvProp = { TEST_VAR: 'testValue', diff --git a/packages/backend-function/src/function_env_type_generator.test.ts b/packages/backend-function/src/function_env_type_generator.test.ts index d988e06fec..64d5588bce 100644 --- a/packages/backend-function/src/function_env_type_generator.test.ts +++ b/packages/backend-function/src/function_env_type_generator.test.ts @@ -1,11 +1,20 @@ -import { describe, it, mock } from 'node:test'; +import { after, describe, it, mock } from 'node:test'; import fs from 'fs'; import fsp from 'fs/promises'; import { FunctionEnvironmentTypeGenerator } from './function_env_type_generator.js'; import assert from 'assert'; import { pathToFileURL } from 'url'; +import path from 'path'; void describe('FunctionEnvironmentTypeGenerator', () => { + after(async () => { + // clean up generated env files + await fsp.rm(path.join(process.cwd(), '.amplify'), { + recursive: true, + force: true, + }); + }); + void it('generates a type definition file', () => { const fsOpenSyncMock = mock.method(fs, 'openSync'); const fsWriteFileSyncMock = mock.method(fs, 'writeFileSync', () => null); @@ -69,4 +78,36 @@ void describe('FunctionEnvironmentTypeGenerator', () => { await fsp.rm(targetDirectory, { recursive: true, force: true }); }); + + void it('does not generate duplicate environment variables', () => { + const fsOpenSyncMock = mock.method(fs, 'openSync'); + const fsWriteFileSyncMock = mock.method(fs, 'writeFileSync', () => null); + fsOpenSyncMock.mock.mockImplementation(() => 0); + const functionEnvironmentTypeGenerator = + new FunctionEnvironmentTypeGenerator('testFunction'); + + functionEnvironmentTypeGenerator.generateTypedProcessEnvShim([ + 'TEST_ENV', + 'TEST_ENV', + 'ANOTHER_ENV', + ]); + + const generatedContent = + fsWriteFileSyncMock.mock.calls[0].arguments[1]?.toString() ?? ''; + + // Check TEST_ENV appears only once + assert.equal( + (generatedContent.match(/TEST_ENV: string;/g) || []).length, + 1, + 'TEST_ENV should appear only once' + ); + + // Check ANOTHER_ENV also appears + assert.ok( + generatedContent.includes('ANOTHER_ENV: string;'), + 'ANOTHER_ENV should be included' + ); + + mock.restoreAll(); + }); }); diff --git a/packages/backend-function/src/function_env_type_generator.ts b/packages/backend-function/src/function_env_type_generator.ts index ea650e26c9..d01a701a19 100644 --- a/packages/backend-function/src/function_env_type_generator.ts +++ b/packages/backend-function/src/function_env_type_generator.ts @@ -57,7 +57,11 @@ export class FunctionEnvironmentTypeGenerator { `/** Amplify backend environment variables available at runtime, this includes environment variables defined in \`defineFunction\` and by cross resource mechanisms */` ); declarations.push(`type ${amplifyBackendEnvVarTypeName} = {`); - amplifyBackendEnvVars.forEach((envName) => { + + // Use a Set to remove duplicates + const uniqueEnvVars = new Set(amplifyBackendEnvVars); + + uniqueEnvVars.forEach((envName) => { const declaration = `${this.indentation}${envName}: string;`; declarations.push(declaration); diff --git a/packages/backend-function/src/layer_parser.test.ts b/packages/backend-function/src/layer_parser.test.ts new file mode 100644 index 0000000000..19c3087364 --- /dev/null +++ b/packages/backend-function/src/layer_parser.test.ts @@ -0,0 +1,187 @@ +import { StackMetadataBackendOutputStorageStrategy } from '@aws-amplify/backend-output-storage'; +import { + ConstructContainerStub, + ResourceNameValidatorStub, + StackResolverStub, +} from '@aws-amplify/backend-platform-test-stubs'; +import { AmplifyUserError } from '@aws-amplify/platform-core'; +import { + ConstructFactoryGetInstanceProps, + ResourceNameValidator, +} from '@aws-amplify/plugin-types'; +import { App, Stack } from 'aws-cdk-lib'; +import { Template } from 'aws-cdk-lib/assertions'; +import assert from 'node:assert'; +import { after, beforeEach, describe, it } from 'node:test'; +import { defineFunction } from './factory.js'; +import path from 'node:path'; +import fsp from 'fs/promises'; + +const createStackAndSetContext = (): Stack => { + const app = new App(); + app.node.setContext('amplify-backend-name', 'testEnvName'); + app.node.setContext('amplify-backend-namespace', 'testBackendId'); + app.node.setContext('amplify-backend-type', 'branch'); + const stack = new Stack(app); + return stack; +}; + +void describe('AmplifyFunctionFactory - Layers', () => { + let rootStack: Stack; + let getInstanceProps: ConstructFactoryGetInstanceProps; + let resourceNameValidator: ResourceNameValidator; + + beforeEach(() => { + rootStack = createStackAndSetContext(); + + const constructContainer = new ConstructContainerStub( + new StackResolverStub(rootStack) + ); + + const outputStorageStrategy = new StackMetadataBackendOutputStorageStrategy( + rootStack + ); + + resourceNameValidator = new ResourceNameValidatorStub(); + + getInstanceProps = { + constructContainer, + outputStorageStrategy, + resourceNameValidator, + }; + }); + + after(async () => { + // clean up generated env files + await fsp.rm(path.join(process.cwd(), '.amplify'), { + recursive: true, + force: true, + }); + }); + + void it('sets a valid layer', () => { + const layerArn = 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer:1'; + const functionFactory = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'lambdaWithLayer', + layers: { + myLayer: layerArn, + }, + }); + const lambda = functionFactory.getInstance(getInstanceProps); + const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + + template.resourceCountIs('AWS::Lambda::Function', 1); + template.hasResourceProperties('AWS::Lambda::Function', { + Handler: 'index.handler', + Layers: [layerArn], + }); + }); + + void it('sets multiple valid layers', () => { + const layerArns = [ + 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-1:1', + 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-2:1', + ]; + const functionFactory = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'lambdaWithMultipleLayers', + layers: { + myLayer1: layerArns[0], + myLayer2: layerArns[1], + }, + }); + const lambda = functionFactory.getInstance(getInstanceProps); + const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + + template.resourceCountIs('AWS::Lambda::Function', 1); + template.hasResourceProperties('AWS::Lambda::Function', { + Handler: 'index.handler', + Layers: layerArns, + }); + }); + + void it('throws an error for an invalid layer ARN', () => { + const invalidLayerArn = 'invalid:arn'; + const functionFactory = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'lambdaWithInvalidLayer', + layers: { + invalidLayer: invalidLayerArn, + }, + }); + assert.throws( + () => functionFactory.getInstance(getInstanceProps), + (error: AmplifyUserError) => { + assert.strictEqual( + error.message, + `Invalid ARN format for layer: ${invalidLayerArn}` + ); + assert.ok(error.resolution); + return true; + } + ); + }); + + void it('throws an error for exceeding the maximum number of layers', () => { + const layerArns = [ + 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-1:1', + 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-2:1', + 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-3:1', + 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-4:1', + 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-5:1', + 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-6:1', + ]; + const layers: Record = layerArns.reduce( + (acc, arn, index) => { + acc[`layer${index + 1}`] = arn; + return acc; + }, + {} as Record + ); + + const functionFactory = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'lambdaWithTooManyLayers', + layers, + }); + + assert.throws( + () => functionFactory.getInstance(getInstanceProps), + (error: AmplifyUserError) => { + assert.strictEqual( + error.message, + `A maximum of 5 unique layers can be attached to a function.` + ); + assert.ok(error.resolution); + return true; + } + ); + }); + + void it('checks if only unique Arns are being used', () => { + const duplicateArn = + 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer:1'; + const functionFactory = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'lambdaWithDuplicateLayers', + layers: { + layer1: duplicateArn, + layer2: duplicateArn, + layer3: duplicateArn, + layer4: duplicateArn, + layer5: duplicateArn, + layer6: duplicateArn, + }, + }); + + const lambda = functionFactory.getInstance(getInstanceProps); + const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + + template.resourceCountIs('AWS::Lambda::Function', 1); + template.hasResourceProperties('AWS::Lambda::Function', { + Handler: 'index.handler', + Layers: [duplicateArn], + }); + }); +}); diff --git a/packages/backend-function/src/layer_parser.ts b/packages/backend-function/src/layer_parser.ts new file mode 100644 index 0000000000..f90ba68e21 --- /dev/null +++ b/packages/backend-function/src/layer_parser.ts @@ -0,0 +1,66 @@ +import { AmplifyUserError } from '@aws-amplify/platform-core'; + +/** + * Parses Lambda Layer ARNs for a function + */ +export class FunctionLayerArnParser { + private arnPattern = new RegExp( + 'arn:[a-zA-Z0-9-]+:lambda:[a-zA-Z0-9-]+:\\d{12}:layer:[a-zA-Z0-9-_]+:[0-9]+' + ); + + /** + * Parse the layers for a function + * @param layers - Layers to be attached to the function + * @param functionName - Name of the function + * @returns Valid layers for the function + * @throws AmplifyUserError if the layer ARN is invalid + * @throws AmplifyUserError if the number of layers exceeds the limit + */ + parseLayers( + layers: Record, + functionName: string + ): Record { + const validLayers: Record = {}; + const uniqueArns = new Set(); + + for (const [key, arn] of Object.entries(layers)) { + if (!this.isValidLayerArn(arn)) { + throw new AmplifyUserError('InvalidLayerArnFormatError', { + message: `Invalid ARN format for layer: ${arn}`, + resolution: `Update the layer ARN with the expected format: arn:aws:lambda:::layer:: for function: ${functionName}`, + }); + } + + // Add to validLayers and uniqueArns only if the ARN hasn't been added already + if (!uniqueArns.has(arn)) { + uniqueArns.add(arn); + validLayers[key] = arn; + } + } + + // Validate the number of unique layers + this.validateLayerCount(uniqueArns); + + return validLayers; + } + + /** + * Validate the ARN format for a Lambda Layer + */ + private isValidLayerArn(arn: string): boolean { + return this.arnPattern.test(arn); + } + + /** + * Validate the number of layers attached to a function + * @see https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html#function-configuration-deployment-and-execution + */ + private validateLayerCount(uniqueArns: Set): void { + if (uniqueArns.size > 5) { + throw new AmplifyUserError('MaximumLayersReachedError', { + message: 'A maximum of 5 unique layers can be attached to a function.', + resolution: 'Remove unused layers in your function', + }); + } + } +} diff --git a/packages/backend-output-schemas/API.md b/packages/backend-output-schemas/API.md index 41fbd8abed..23c5a41d89 100644 --- a/packages/backend-output-schemas/API.md +++ b/packages/backend-output-schemas/API.md @@ -6,6 +6,12 @@ import { z } from 'zod'; +// @public (undocumented) +export type AIConversationOutput = z.infer; + +// @public +export const aiConversationOutputKey = "AWS::Amplify::AI::Conversation"; + // @public (undocumented) export type AuthOutput = z.infer; @@ -127,6 +133,7 @@ export const unifiedBackendOutputSchema: z.ZodObject<{ oauthRedirectSignOut: z.ZodOptional; oauthClientId: z.ZodOptional; oauthResponseType: z.ZodOptional; + groups: z.ZodOptional; }, "strip", z.ZodTypeAny, { authRegion: string; userPoolId: string; @@ -147,6 +154,7 @@ export const unifiedBackendOutputSchema: z.ZodObject<{ oauthRedirectSignOut?: string | undefined; oauthClientId?: string | undefined; oauthResponseType?: string | undefined; + groups?: string | undefined; }, { authRegion: string; userPoolId: string; @@ -167,6 +175,7 @@ export const unifiedBackendOutputSchema: z.ZodObject<{ oauthRedirectSignOut?: string | undefined; oauthClientId?: string | undefined; oauthResponseType?: string | undefined; + groups?: string | undefined; }>; }, "strip", z.ZodTypeAny, { version: "1"; @@ -190,6 +199,7 @@ export const unifiedBackendOutputSchema: z.ZodObject<{ oauthRedirectSignOut?: string | undefined; oauthClientId?: string | undefined; oauthResponseType?: string | undefined; + groups?: string | undefined; }; }, { version: "1"; @@ -213,6 +223,7 @@ export const unifiedBackendOutputSchema: z.ZodObject<{ oauthRedirectSignOut?: string | undefined; oauthClientId?: string | undefined; oauthResponseType?: string | undefined; + groups?: string | undefined; }; }>]>>; "AWS::Amplify::GraphQL": z.ZodOptional]>>; + "AWS::Amplify::AI::Conversation": z.ZodOptional; + payload: z.ZodObject<{ + definedConversationHandlers: z.ZodString; + }, "strip", z.ZodTypeAny, { + definedConversationHandlers: string; + }, { + definedConversationHandlers: string; + }>; + }, "strip", z.ZodTypeAny, { + version: "1"; + payload: { + definedConversationHandlers: string; + }; + }, { + version: "1"; + payload: { + definedConversationHandlers: string; + }; + }>]>>; }, "strip", z.ZodTypeAny, { "AWS::Amplify::Platform"?: { version: "1"; @@ -376,6 +407,7 @@ export const unifiedBackendOutputSchema: z.ZodObject<{ oauthRedirectSignOut?: string | undefined; oauthClientId?: string | undefined; oauthResponseType?: string | undefined; + groups?: string | undefined; }; } | undefined; "AWS::Amplify::GraphQL"?: { @@ -405,6 +437,12 @@ export const unifiedBackendOutputSchema: z.ZodObject<{ definedFunctions: string; }; } | undefined; + "AWS::Amplify::AI::Conversation"?: { + version: "1"; + payload: { + definedConversationHandlers: string; + }; + } | undefined; }, { "AWS::Amplify::Platform"?: { version: "1"; @@ -441,6 +479,7 @@ export const unifiedBackendOutputSchema: z.ZodObject<{ oauthRedirectSignOut?: string | undefined; oauthClientId?: string | undefined; oauthResponseType?: string | undefined; + groups?: string | undefined; }; } | undefined; "AWS::Amplify::GraphQL"?: { @@ -470,8 +509,36 @@ export const unifiedBackendOutputSchema: z.ZodObject<{ definedFunctions: string; }; } | undefined; + "AWS::Amplify::AI::Conversation"?: { + version: "1"; + payload: { + definedConversationHandlers: string; + }; + } | undefined; }>; +// @public (undocumented) +export const versionedAIConversationOutputSchema: z.ZodDiscriminatedUnion<"version", [z.ZodObject<{ + version: z.ZodLiteral<"1">; + payload: z.ZodObject<{ + definedConversationHandlers: z.ZodString; + }, "strip", z.ZodTypeAny, { + definedConversationHandlers: string; + }, { + definedConversationHandlers: string; + }>; +}, "strip", z.ZodTypeAny, { + version: "1"; + payload: { + definedConversationHandlers: string; + }; +}, { + version: "1"; + payload: { + definedConversationHandlers: string; + }; +}>]>; + // @public (undocumented) export const versionedAuthOutputSchema: z.ZodDiscriminatedUnion<"version", [z.ZodObject<{ version: z.ZodLiteral<"1">; @@ -495,6 +562,7 @@ export const versionedAuthOutputSchema: z.ZodDiscriminatedUnion<"version", [z.Zo oauthRedirectSignOut: z.ZodOptional; oauthClientId: z.ZodOptional; oauthResponseType: z.ZodOptional; + groups: z.ZodOptional; }, "strip", z.ZodTypeAny, { authRegion: string; userPoolId: string; @@ -515,6 +583,7 @@ export const versionedAuthOutputSchema: z.ZodDiscriminatedUnion<"version", [z.Zo oauthRedirectSignOut?: string | undefined; oauthClientId?: string | undefined; oauthResponseType?: string | undefined; + groups?: string | undefined; }, { authRegion: string; userPoolId: string; @@ -535,6 +604,7 @@ export const versionedAuthOutputSchema: z.ZodDiscriminatedUnion<"version", [z.Zo oauthRedirectSignOut?: string | undefined; oauthClientId?: string | undefined; oauthResponseType?: string | undefined; + groups?: string | undefined; }>; }, "strip", z.ZodTypeAny, { version: "1"; @@ -558,6 +628,7 @@ export const versionedAuthOutputSchema: z.ZodDiscriminatedUnion<"version", [z.Zo oauthRedirectSignOut?: string | undefined; oauthClientId?: string | undefined; oauthResponseType?: string | undefined; + groups?: string | undefined; }; }, { version: "1"; @@ -581,6 +652,7 @@ export const versionedAuthOutputSchema: z.ZodDiscriminatedUnion<"version", [z.Zo oauthRedirectSignOut?: string | undefined; oauthClientId?: string | undefined; oauthResponseType?: string | undefined; + groups?: string | undefined; }; }>]>; diff --git a/packages/backend-output-schemas/CHANGELOG.md b/packages/backend-output-schemas/CHANGELOG.md index ff823df4ef..0212c02ffd 100644 --- a/packages/backend-output-schemas/CHANGELOG.md +++ b/packages/backend-output-schemas/CHANGELOG.md @@ -1,5 +1,23 @@ # @aws-amplify/backend-output-schemas +## 1.4.0 + +### Minor Changes + +- 5f46d8d: add user groups to outputs + +## 1.3.0 + +### Minor Changes + +- 0a5e51c: Stream conversation logs in sandbox + +## 1.2.1 + +### Patch Changes + +- d538ecc: add storage access rules to outputs + ## 1.2.0 ### Minor Changes diff --git a/packages/backend-output-schemas/package.json b/packages/backend-output-schemas/package.json index e72ee388c3..93e0106dcb 100644 --- a/packages/backend-output-schemas/package.json +++ b/packages/backend-output-schemas/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/backend-output-schemas", - "version": "1.2.0", + "version": "1.4.0", "type": "commonjs", "publishConfig": { "access": "public" diff --git a/packages/backend-output-schemas/src/ai/conversation/index.ts b/packages/backend-output-schemas/src/ai/conversation/index.ts new file mode 100644 index 0000000000..29b0d41397 --- /dev/null +++ b/packages/backend-output-schemas/src/ai/conversation/index.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; +import { aiConversationOutputSchema as aiConversationOutputSchemaV1 } from './v1'; + +export const versionedAIConversationOutputSchema = z.discriminatedUnion( + 'version', + [ + aiConversationOutputSchemaV1, + // this is where additional function major version schemas would go + ] +); + +export type AIConversationOutput = z.infer< + typeof versionedAIConversationOutputSchema +>; diff --git a/packages/backend-output-schemas/src/ai/conversation/v1.ts b/packages/backend-output-schemas/src/ai/conversation/v1.ts new file mode 100644 index 0000000000..cb50b29cda --- /dev/null +++ b/packages/backend-output-schemas/src/ai/conversation/v1.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +export const aiConversationOutputSchema = z.object({ + version: z.literal('1'), + payload: z.object({ + definedConversationHandlers: z.string(), // JSON array as string + }), +}); diff --git a/packages/backend-output-schemas/src/auth/v1.ts b/packages/backend-output-schemas/src/auth/v1.ts index af4d65cc8c..a17c2cd237 100644 --- a/packages/backend-output-schemas/src/auth/v1.ts +++ b/packages/backend-output-schemas/src/auth/v1.ts @@ -27,5 +27,6 @@ export const authOutputSchema = z.object({ oauthRedirectSignOut: z.string().optional(), oauthClientId: z.string().optional(), oauthResponseType: z.string().optional(), + groups: z.string().optional(), // JSON array as string }), }); diff --git a/packages/backend-output-schemas/src/index.ts b/packages/backend-output-schemas/src/index.ts index ec7cef0209..11bfb14cd2 100644 --- a/packages/backend-output-schemas/src/index.ts +++ b/packages/backend-output-schemas/src/index.ts @@ -5,6 +5,7 @@ import { versionedStorageOutputSchema } from './storage/index.js'; import { versionedStackOutputSchema } from './stack/index.js'; import { versionedCustomOutputSchema } from './custom'; import { versionedFunctionOutputSchema } from './function/index.js'; +import { versionedAIConversationOutputSchema } from './ai/conversation/index.js'; /** * The auth, graphql and storage exports here are duplicated from the submodule exports in the package.json file @@ -84,6 +85,20 @@ export * from './function/index.js'; */ export const functionOutputKey = 'AWS::Amplify::Function'; +/** + * ---------- AI conversation exports ---------- + */ + +/** + * re-export the AI conversation output schema + */ +export * from './ai/conversation/index.js'; + +/** + * Expected key that AI conversation output is stored under + */ +export const aiConversationOutputKey = 'AWS::Amplify::AI::Conversation'; + /** * ---------- Unified exports ---------- */ @@ -99,6 +114,7 @@ export const unifiedBackendOutputSchema = z.object({ [storageOutputKey]: versionedStorageOutputSchema.optional(), [customOutputKey]: versionedCustomOutputSchema.optional(), [functionOutputKey]: versionedFunctionOutputSchema.optional(), + [aiConversationOutputKey]: versionedAIConversationOutputSchema.optional(), }); /** * This type is a subset of the BackendOutput type that is exposed by the platform. diff --git a/packages/backend-output-schemas/src/storage/v1.ts b/packages/backend-output-schemas/src/storage/v1.ts index 5095714a81..0827b34d69 100644 --- a/packages/backend-output-schemas/src/storage/v1.ts +++ b/packages/backend-output-schemas/src/storage/v1.ts @@ -1,9 +1,29 @@ import { z } from 'zod'; +const storageAccessActionEnum = z.enum([ + 'read', + 'get', + 'list', + 'write', + 'delete', +]); + +const pathSchema = z.record( + z.string(), + z.object({ + guest: z.array(storageAccessActionEnum).optional(), + authenticated: z.array(storageAccessActionEnum).optional(), + groups: z.array(storageAccessActionEnum).optional(), + entity: z.array(storageAccessActionEnum).optional(), + resource: z.array(storageAccessActionEnum).optional(), + }) +); + const bucketSchema = z.object({ name: z.string(), bucketName: z.string(), storageRegion: z.string(), + paths: pathSchema.optional(), }); export const storageOutputSchema = z.object({ diff --git a/packages/backend-output-storage/CHANGELOG.md b/packages/backend-output-storage/CHANGELOG.md index 51e28f0faa..89eac4993f 100644 --- a/packages/backend-output-storage/CHANGELOG.md +++ b/packages/backend-output-storage/CHANGELOG.md @@ -1,5 +1,21 @@ # @aws-amplify/backend-output-storage +## 1.1.3 + +### Patch Changes + +- b56d344: update aws-cdk lib to ^2.158.0 +- Updated dependencies [b56d344] + - @aws-amplify/plugin-types@1.3.1 + +## 1.1.2 + +### Patch Changes + +- 8dd7286: fixed errors in plugin-types and cli-core along with any extraneous dependencies in other packages +- Updated dependencies [8dd7286] + - @aws-amplify/plugin-types@1.2.2 + ## 1.1.1 ### Patch Changes diff --git a/packages/backend-output-storage/package.json b/packages/backend-output-storage/package.json index a854f673d9..839a1351da 100644 --- a/packages/backend-output-storage/package.json +++ b/packages/backend-output-storage/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/backend-output-storage", - "version": "1.1.1", + "version": "1.1.3", "type": "commonjs", "publishConfig": { "access": "public" @@ -21,9 +21,9 @@ "dependencies": { "@aws-amplify/backend-output-schemas": "^1.2.0", "@aws-amplify/platform-core": "^1.0.6", - "@aws-amplify/plugin-types": "^1.2.1" + "@aws-amplify/plugin-types": "^1.3.1" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0" + "aws-cdk-lib": "^2.158.0" } } diff --git a/packages/backend-platform-test-stubs/CHANGELOG.md b/packages/backend-platform-test-stubs/CHANGELOG.md index 6c1a63bd46..436f8411ba 100644 --- a/packages/backend-platform-test-stubs/CHANGELOG.md +++ b/packages/backend-platform-test-stubs/CHANGELOG.md @@ -1,5 +1,21 @@ # @aws-amplify/backend-platform-test-stubs +## 0.3.6 + +### Patch Changes + +- b56d344: update aws-cdk lib to ^2.158.0 +- Updated dependencies [b56d344] + - @aws-amplify/plugin-types@1.3.1 + +## 0.3.5 + +### Patch Changes + +- 8dd7286: fixed errors in plugin-types and cli-core along with any extraneous dependencies in other packages +- Updated dependencies [8dd7286] + - @aws-amplify/plugin-types@1.2.2 + ## 0.3.4 ### Patch Changes diff --git a/packages/backend-platform-test-stubs/package.json b/packages/backend-platform-test-stubs/package.json index 409cb64910..4d8ab06e8f 100644 --- a/packages/backend-platform-test-stubs/package.json +++ b/packages/backend-platform-test-stubs/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/backend-platform-test-stubs", - "version": "0.3.4", + "version": "0.3.6", "type": "module", "private": true, "exports": { @@ -16,8 +16,8 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/plugin-types": "^1.2.1", - "aws-cdk-lib": "^2.152.0", + "@aws-amplify/plugin-types": "^1.3.1", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0" } } diff --git a/packages/backend-secret/CHANGELOG.md b/packages/backend-secret/CHANGELOG.md index ddf0cac618..3ff634508c 100644 --- a/packages/backend-secret/CHANGELOG.md +++ b/packages/backend-secret/CHANGELOG.md @@ -1,5 +1,33 @@ # @aws-amplify/backend-secret +## 1.1.5 + +### Patch Changes + +- 255ca18: Handle parameter not found error while deleting secret + +## 1.1.4 + +### Patch Changes + +- f87cc87: fix: internally paginate list secret calls + +## 1.1.3 + +### Patch Changes + +- dce0518: Handle parameter not found error + +## 1.1.2 + +### Patch Changes + +- e648e8e: added main field to package.json so these packages are resolvable +- 0ff73ec: add ExpiredToken in the list of credentials error +- e648e8e: added main field to packages known to lack one +- Updated dependencies [8dd7286] + - @aws-amplify/plugin-types@1.2.2 + ## 1.1.1 ### Patch Changes diff --git a/packages/backend-secret/package.json b/packages/backend-secret/package.json index 56835c984d..8d3f372b9c 100644 --- a/packages/backend-secret/package.json +++ b/packages/backend-secret/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/backend-secret", - "version": "1.1.1", + "version": "1.1.5", "type": "module", "publishConfig": { "access": "public" @@ -19,7 +19,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/plugin-types": "^1.1.1", + "@aws-amplify/plugin-types": "^1.2.2", "@aws-amplify/platform-core": "^1.0.5", "@aws-sdk/client-ssm": "^3.624.0" }, diff --git a/packages/backend-secret/src/ssm_secret.test.ts b/packages/backend-secret/src/ssm_secret.test.ts index c33da91cc1..fbde647a4d 100644 --- a/packages/backend-secret/src/ssm_secret.test.ts +++ b/packages/backend-secret/src/ssm_secret.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, it, mock } from 'node:test'; import { GetParameterCommandOutput, + GetParametersByPathCommandInput, GetParametersByPathCommandOutput, InternalServerError, ParameterNotFound, @@ -306,6 +307,7 @@ void describe('SSMSecret', () => { assert.deepStrictEqual( mockGetParametersByPath.mock.calls[0].arguments[0], { + NextToken: undefined, Path: testBranchPath, WithDecryption: true, } @@ -337,6 +339,7 @@ void describe('SSMSecret', () => { assert.deepStrictEqual( mockGetParametersByPath.mock.calls[0].arguments[0], { + NextToken: undefined, Path: testSharedPath, WithDecryption: true, } @@ -344,6 +347,68 @@ void describe('SSMSecret', () => { assert.deepEqual(secrets, [testSecretListItem]); }); + void it('lists all secrets by internally paginating calls', async () => { + const mockGetParametersByPath = mock.method( + ssmClient, + 'getParametersByPath', + (input: GetParametersByPathCommandInput) => { + let nextToken: string | undefined = undefined; + if (!input.NextToken) { + nextToken = '1'; + } else if (input.NextToken === '1') { + nextToken = '2'; + } else if (input.NextToken === '2') { + nextToken = undefined; + } + return Promise.resolve({ + NextToken: nextToken, + Parameters: [ + { + Name: testSharedSecretFullNamePath.concat( + input.NextToken ?? '' + ), + Value: testSecretValue, + Version: testSecretVersion, + LastModifiedDate: testSecretLastUpdated, + }, + ], + } as GetParametersByPathCommandOutput); + } + ); + + const secrets = await ssmSecretClient.listSecrets(testBackendId); + assert.deepStrictEqual(mockGetParametersByPath.mock.calls.length, 3); + assert.deepStrictEqual( + mockGetParametersByPath.mock.calls[0].arguments[0], + { + NextToken: undefined, + Path: testSharedPath, + WithDecryption: true, + } + ); + assert.deepStrictEqual( + mockGetParametersByPath.mock.calls[1].arguments[0], + { + NextToken: '1', + Path: testSharedPath, + WithDecryption: true, + } + ); + assert.deepStrictEqual( + mockGetParametersByPath.mock.calls[2].arguments[0], + { + NextToken: '2', + Path: testSharedPath, + WithDecryption: true, + } + ); + assert.deepEqual(secrets, [ + { ...testSecretListItem, name: testSecretName }, + { ...testSecretListItem, name: testSecretName + '1' }, + { ...testSecretListItem, name: testSecretName + '2' }, + ]); + }); + void it('lists an empty list', async () => { const mockGetParametersByPath = mock.method( ssmClient, @@ -359,6 +424,7 @@ void describe('SSMSecret', () => { assert.deepStrictEqual( mockGetParametersByPath.mock.calls[0].arguments[0], { + NextToken: undefined, Path: testBranchPath, WithDecryption: true, } diff --git a/packages/backend-secret/src/ssm_secret.ts b/packages/backend-secret/src/ssm_secret.ts index c8c3421975..cd5b44563f 100644 --- a/packages/backend-secret/src/ssm_secret.ts +++ b/packages/backend-secret/src/ssm_secret.ts @@ -68,24 +68,29 @@ export class SSMSecretClient implements SecretClient { const result: SecretListItem[] = []; try { - const resp = await this.ssmClient.getParametersByPath({ - Path: path, - WithDecryption: true, - }); + let nextToken: string | undefined; + do { + const resp = await this.ssmClient.getParametersByPath({ + Path: path, + WithDecryption: true, + NextToken: nextToken, + }); - resp.Parameters?.forEach((param) => { - if (!param.Name || !param.Value) { - return; - } - const secretName = param.Name.split('/').pop(); - if (secretName) { - result.push({ - name: secretName, - version: param.Version, - lastUpdated: param.LastModifiedDate, - }); - } - }); + resp.Parameters?.forEach((param) => { + if (!param.Name || !param.Value) { + return; + } + const secretName = param.Name.split('/').pop(); + if (secretName) { + result.push({ + name: secretName, + version: param.Version, + lastUpdated: param.LastModifiedDate, + }); + } + }); + nextToken = resp.NextToken; + } while (nextToken); return result; } catch (err) { throw SecretError.createInstance(err as Error); diff --git a/packages/backend-secret/src/ssm_secret_with_amplify_error_handling.test.ts b/packages/backend-secret/src/ssm_secret_with_amplify_error_handling.test.ts index 93e42d479b..88c29dd598 100644 --- a/packages/backend-secret/src/ssm_secret_with_amplify_error_handling.test.ts +++ b/packages/backend-secret/src/ssm_secret_with_amplify_error_handling.test.ts @@ -62,6 +62,58 @@ void describe('getSecretClientWithAmplifyErrorHandling', () => { ); }); + void it('throws AmplifyUserError if getSecret fails due to ParameterNotFound error', async (context) => { + const notFoundError = new Error('Parameter not found error'); + notFoundError.name = 'ParameterNotFound'; + const secretsError = SecretError.createInstance(notFoundError); + context.mock.method(rawSecretClient, 'getSecret', () => { + throw secretsError; + }); + const secretName = 'testSecretName'; + await assert.rejects( + () => + classUnderTest.getSecret( + { + namespace: 'testSandboxId', + name: 'testSandboxName', + type: 'sandbox', + }, + { + name: secretName, + } + ), + new AmplifyUserError('SSMParameterNotFoundError', { + message: `Failed to get ${secretName} secret. ParameterNotFound: Parameter not found error`, + resolution: `Make sure that ${secretName} has been set. See https://docs.amplify.aws/react/deploy-and-host/fullstack-branching/secrets-and-vars/.`, + }) + ); + }); + + void it('throws AmplifyUserError if removeSecret fails due to ParameterNotFound error', async (context) => { + const notFoundError = new Error('Parameter not found error'); + notFoundError.name = 'ParameterNotFound'; + const secretsError = SecretError.createInstance(notFoundError); + context.mock.method(rawSecretClient, 'removeSecret', () => { + throw secretsError; + }); + const secretName = 'testSecretName'; + await assert.rejects( + () => + classUnderTest.removeSecret( + { + namespace: 'testSandboxId', + name: 'testSandboxName', + type: 'sandbox', + }, + secretName + ), + new AmplifyUserError('SSMParameterNotFoundError', { + message: `Failed to remove ${secretName} secret. ParameterNotFound: Parameter not found error`, + resolution: `Make sure that ${secretName} has been set. See https://docs.amplify.aws/react/deploy-and-host/fullstack-branching/secrets-and-vars/.`, + }) + ); + }); + void it('throws AmplifyFault if listSecrets fails due to a non-SSM exception other than expired credentials', async (context) => { const underlyingError = new Error('some secret error'); const secretsError = SecretError.createInstance(underlyingError); diff --git a/packages/backend-secret/src/ssm_secret_with_amplify_error_handling.ts b/packages/backend-secret/src/ssm_secret_with_amplify_error_handling.ts index 85e771db1c..7d18d1c489 100644 --- a/packages/backend-secret/src/ssm_secret_with_amplify_error_handling.ts +++ b/packages/backend-secret/src/ssm_secret_with_amplify_error_handling.ts @@ -29,7 +29,7 @@ export class SSMSecretClientWithAmplifyErrorHandling implements SecretClient { secretIdentifier ); } catch (e) { - throw this.translateToAmplifyError(e, 'Get'); + throw this.translateToAmplifyError(e, 'Get', secretIdentifier); } }; @@ -69,11 +69,15 @@ export class SSMSecretClientWithAmplifyErrorHandling implements SecretClient { secretName ); } catch (e) { - throw this.translateToAmplifyError(e, 'Remove'); + throw this.translateToAmplifyError(e, 'Remove', { name: secretName }); } }; - private translateToAmplifyError = (error: unknown, apiName: string) => { + private translateToAmplifyError = ( + error: unknown, + apiName: string, + secretIdentifier?: SecretIdentifier + ) => { if (error instanceof SecretError && error.cause) { if ( [ @@ -83,6 +87,7 @@ export class SSMSecretClientWithAmplifyErrorHandling implements SecretClient { 'ExpiredTokenException', 'ExpiredToken', 'CredentialsProviderError', + 'IncompleteSignatureException', 'InvalidSignatureException', ].includes(error.cause.name) ) { @@ -94,6 +99,18 @@ export class SSMSecretClientWithAmplifyErrorHandling implements SecretClient { 'Make sure your AWS credentials are set up correctly, refreshed and have necessary permissions to call SSM service', }); } + if ( + error.cause.name === 'ParameterNotFound' && + (apiName === 'Get' || apiName === 'Remove') && + secretIdentifier + ) { + return new AmplifyUserError('SSMParameterNotFoundError', { + message: `Failed to ${apiName.toLowerCase()} ${ + secretIdentifier.name + } secret. ${error.cause.name}: ${error.cause?.message}`, + resolution: `Make sure that ${secretIdentifier.name} has been set. See https://docs.amplify.aws/react/deploy-and-host/fullstack-branching/secrets-and-vars/.`, + }); + } let downstreamException: Error = error; if ( !(error.cause instanceof SSMServiceException) && diff --git a/packages/backend-storage/API.md b/packages/backend-storage/API.md index e7472b5770..aa65ada47b 100644 --- a/packages/backend-storage/API.md +++ b/packages/backend-storage/API.md @@ -14,6 +14,7 @@ import { IBucket } from 'aws-cdk-lib/aws-s3'; import { ResourceAccessAcceptor } from '@aws-amplify/plugin-types'; import { ResourceAccessAcceptorFactory } from '@aws-amplify/plugin-types'; import { ResourceProvider } from '@aws-amplify/plugin-types'; +import { StackProvider } from '@aws-amplify/plugin-types'; import { StorageOutput } from '@aws-amplify/backend-output-schemas'; // @public (undocumented) @@ -34,7 +35,7 @@ export type AmplifyStorageProps = { export type AmplifyStorageTriggerEvent = 'onDelete' | 'onUpload'; // @public -export const defineStorage: (props: AmplifyStorageFactoryProps) => ConstructFactory>; +export const defineStorage: (props: AmplifyStorageFactoryProps) => ConstructFactory & StackProvider>; // @public export type EntityId = 'identity'; diff --git a/packages/backend-storage/CHANGELOG.md b/packages/backend-storage/CHANGELOG.md index 2be32cc749..e1f09d6eb5 100644 --- a/packages/backend-storage/CHANGELOG.md +++ b/packages/backend-storage/CHANGELOG.md @@ -1,5 +1,50 @@ # @aws-amplify/backend-storage +## 1.2.3 + +### Patch Changes + +- f1db886: add resourceGroupName prop to function +- Updated dependencies [f1db886] + - @aws-amplify/plugin-types@1.5.0 + +## 1.2.2 + +### Patch Changes + +- b56d344: update aws-cdk lib to ^2.158.0 +- Updated dependencies [b56d344] + - @aws-amplify/backend-output-storage@1.1.3 + - @aws-amplify/plugin-types@1.3.1 + +## 1.2.1 + +### Patch Changes + +- d538ecc: add storage access rules to outputs +- Updated dependencies [d538ecc] + - @aws-amplify/backend-output-schemas@1.2.1 + +## 1.2.0 + +### Minor Changes + +- 87dbf41: expose stack property for backend, function resource, storage resource, and auth resource + +### Patch Changes + +- Updated dependencies [87dbf41] + - @aws-amplify/plugin-types@1.3.0 + +## 1.1.3 + +### Patch Changes + +- e648e8e: added main field to package.json so these packages are resolvable +- Updated dependencies [8dd7286] + - @aws-amplify/backend-output-storage@1.1.2 + - @aws-amplify/plugin-types@1.2.2 + ## 1.1.2 ### Patch Changes diff --git a/packages/backend-storage/package.json b/packages/backend-storage/package.json index c1bcb05b33..c56134608f 100644 --- a/packages/backend-storage/package.json +++ b/packages/backend-storage/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/backend-storage", - "version": "1.1.2", + "version": "1.2.3", "type": "module", "publishConfig": { "access": "public" @@ -19,16 +19,16 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/backend-output-schemas": "^1.2.0", - "@aws-amplify/backend-output-storage": "^1.1.1", - "@aws-amplify/plugin-types": "^1.2.1" + "@aws-amplify/backend-output-schemas": "^1.2.1", + "@aws-amplify/backend-output-storage": "^1.1.3", + "@aws-amplify/plugin-types": "^1.5.0" }, "devDependencies": { - "@aws-amplify/backend-platform-test-stubs": "^0.3.4", - "@aws-amplify/platform-core": "^1.0.6" + "@aws-amplify/backend-platform-test-stubs": "^0.3.6", + "@aws-amplify/platform-core": "^1.2.1" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0" } } diff --git a/packages/backend-storage/src/access_builder.ts b/packages/backend-storage/src/access_builder.ts index 129c8b64ee..7309e1efa7 100644 --- a/packages/backend-storage/src/access_builder.ts +++ b/packages/backend-storage/src/access_builder.ts @@ -5,6 +5,7 @@ import { ResourceProvider, } from '@aws-amplify/plugin-types'; import { StorageAccessBuilder } from './types.js'; +import { entityIdSubstitution } from './constants.js'; export const roleAccessBuilder: StorageAccessBuilder = { authenticated: { @@ -69,7 +70,7 @@ export const roleAccessBuilder: StorageAccessBuilder = { }, ], actions, - idSubstitution: '${cognito-identity.amazonaws.com:sub}', + idSubstitution: entityIdSubstitution, }), }), resource: (other) => ({ diff --git a/packages/backend-storage/src/constants.ts b/packages/backend-storage/src/constants.ts index 8ee0e17bd5..7588e60997 100644 --- a/packages/backend-storage/src/constants.ts +++ b/packages/backend-storage/src/constants.ts @@ -1 +1,2 @@ export const entityIdPathToken = '{entity_id}'; +export const entityIdSubstitution = '${cognito-identity.amazonaws.com:sub}'; diff --git a/packages/backend-storage/src/construct.ts b/packages/backend-storage/src/construct.ts index 1cbdb71670..4308b3ceca 100644 --- a/packages/backend-storage/src/construct.ts +++ b/packages/backend-storage/src/construct.ts @@ -12,6 +12,7 @@ import { ConstructFactory, FunctionResources, ResourceProvider, + StackProvider, } from '@aws-amplify/plugin-types'; import { StorageOutput } from '@aws-amplify/backend-output-schemas'; import { RemovalPolicy, Stack } from 'aws-cdk-lib'; @@ -19,6 +20,7 @@ import { AttributionMetadataStorage } from '@aws-amplify/backend-output-storage' import { fileURLToPath } from 'node:url'; import { IFunction } from 'aws-cdk-lib/aws-lambda'; import { S3EventSourceV2 } from 'aws-cdk-lib/aws-lambda-event-sources'; +import { StorageAccessDefinitionOutput } from './private_types.js'; // Be very careful editing this value. It is the string that is used to attribute stacks to Amplify Storage in BI metrics const storageStackType = 'storage-S3'; @@ -77,11 +79,13 @@ export type StorageResources = { */ export class AmplifyStorage extends Construct - implements ResourceProvider + implements ResourceProvider, StackProvider { + readonly stack: Stack; readonly resources: StorageResources; readonly isDefault: boolean; readonly name: string; + accessDefinition: StorageAccessDefinitionOutput; /** * Create a new AmplifyStorage instance */ @@ -89,6 +93,7 @@ export class AmplifyStorage super(scope, id); this.isDefault = props.isDefault || false; this.name = props.name; + this.stack = Stack.of(scope); const bucketProps: BucketProps = { versioned: props.versioned || false, @@ -143,4 +148,11 @@ export class AmplifyStorage new S3EventSourceV2(this.resources.bucket, { events }) ); }; + + /** + * Add access definitions to storage + */ + addAccessDefinition = (accessOutput: StorageAccessDefinitionOutput) => { + this.accessDefinition = accessOutput; + }; } diff --git a/packages/backend-storage/src/factory.test.ts b/packages/backend-storage/src/factory.test.ts index d3f425a6d2..0ffd504d67 100644 --- a/packages/backend-storage/src/factory.test.ts +++ b/packages/backend-storage/src/factory.test.ts @@ -149,6 +149,16 @@ void describe('AmplifyStorageFactory', () => { }; }); + void it('verifies stack property exists and is equal to storage stack', () => { + const storageConstruct = defineStorage({ name: 'testName' }).getInstance( + getInstanceProps + ); + assert.equal( + storageConstruct.stack, + Stack.of(storageConstruct.resources.bucket) + ); + }); + void it('if more than one default bucket, throw', () => { storageFactory = defineStorage({ name: 'testName', isDefault: true }); storageFactory2 = defineStorage({ name: 'testName2', isDefault: true }); diff --git a/packages/backend-storage/src/factory.ts b/packages/backend-storage/src/factory.ts index 49eff9ba1e..4c5212c964 100644 --- a/packages/backend-storage/src/factory.ts +++ b/packages/backend-storage/src/factory.ts @@ -3,6 +3,7 @@ import { ConstructFactory, ConstructFactoryGetInstanceProps, ResourceProvider, + StackProvider, } from '@aws-amplify/plugin-types'; import * as path from 'path'; import { AmplifyStorage, StorageResources } from './construct.js'; @@ -74,5 +75,5 @@ export class AmplifyStorageFactory */ export const defineStorage = ( props: AmplifyStorageFactoryProps -): ConstructFactory> => +): ConstructFactory & StackProvider> => new AmplifyStorageFactory(props, new Error().stack); diff --git a/packages/backend-storage/src/private_types.ts b/packages/backend-storage/src/private_types.ts index 8c7e53f27a..8daaf2af48 100644 --- a/packages/backend-storage/src/private_types.ts +++ b/packages/backend-storage/src/private_types.ts @@ -15,3 +15,9 @@ export type StorageError = * StorageAction type intended to be used after mapping "read" to "get" and "list" */ export type InternalStorageAction = Exclude; + +/** + * Storage access types intended to be used to map storage access to storage outputs + */ +export type StorageAccessConfig = Record; +export type StorageAccessDefinitionOutput = Record; diff --git a/packages/backend-storage/src/storage_access_orchestrator.test.ts b/packages/backend-storage/src/storage_access_orchestrator.test.ts index ffcc031de3..75cecc30ba 100644 --- a/packages/backend-storage/src/storage_access_orchestrator.test.ts +++ b/packages/backend-storage/src/storage_access_orchestrator.test.ts @@ -4,7 +4,7 @@ import { ConstructFactoryGetInstanceProps } from '@aws-amplify/plugin-types'; import { App, Stack } from 'aws-cdk-lib'; import { Bucket } from 'aws-cdk-lib/aws-s3'; import assert from 'node:assert'; -import { entityIdPathToken } from './constants.js'; +import { entityIdPathToken, entityIdSubstitution } from './constants.js'; import { StorageAccessPolicyFactory } from './storage_access_policy_factory.js'; import { StorageAccessDefinition } from './types.js'; @@ -78,7 +78,8 @@ void describe('StorageAccessOrchestrator', () => { storageAccessPolicyFactory ); - storageAccessOrchestrator.orchestrateStorageAccess(); + const storageAccessDefinitionOutput = + storageAccessOrchestrator.orchestrateStorageAccess(); assert.equal(acceptResourceAccessMock.mock.callCount(), 1); assert.deepStrictEqual( acceptResourceAccessMock.mock.calls[0].arguments[0].document.toJSON(), @@ -102,6 +103,11 @@ void describe('StorageAccessOrchestrator', () => { acceptResourceAccessMock.mock.calls[0].arguments[1], ssmEnvironmentEntriesStub ); + assert.deepStrictEqual(storageAccessDefinitionOutput, { + 'test/prefix/*': { + acceptor: ['get', 'write'], + }, + }); }); void it('handles multiple permissions for the same resource access acceptor', () => { @@ -132,7 +138,8 @@ void describe('StorageAccessOrchestrator', () => { storageAccessPolicyFactory ); - storageAccessOrchestrator.orchestrateStorageAccess(); + const storageAccessDefinitionOutput = + storageAccessOrchestrator.orchestrateStorageAccess(); assert.equal(acceptResourceAccessMock.mock.callCount(), 1); assert.deepStrictEqual( acceptResourceAccessMock.mock.calls[0].arguments[0].document.toJSON(), @@ -164,6 +171,14 @@ void describe('StorageAccessOrchestrator', () => { acceptResourceAccessMock.mock.calls[0].arguments[1], ssmEnvironmentEntriesStub ); + assert.deepStrictEqual(storageAccessDefinitionOutput, { + 'test/prefix/*': { + acceptor: ['get', 'write', 'delete'], + }, + 'another/prefix/*': { + acceptor: ['get'], + }, + }); }); void it('handles multiple resource access acceptors', () => { @@ -204,7 +219,8 @@ void describe('StorageAccessOrchestrator', () => { storageAccessPolicyFactory ); - storageAccessOrchestrator.orchestrateStorageAccess(); + const storageAccessDefinitionOutput = + storageAccessOrchestrator.orchestrateStorageAccess(); assert.equal(acceptResourceAccessMock1.mock.callCount(), 1); assert.deepStrictEqual( acceptResourceAccessMock1.mock.calls[0].arguments[0].document.toJSON(), @@ -259,6 +275,15 @@ void describe('StorageAccessOrchestrator', () => { acceptResourceAccessMock2.mock.calls[0].arguments[1], ssmEnvironmentEntriesStub ); + assert.deepStrictEqual(storageAccessDefinitionOutput, { + 'test/prefix/*': { + acceptor1: ['get', 'write', 'delete'], + acceptor2: ['get'], + }, + 'another/prefix/*': { + acceptor2: ['get', 'delete'], + }, + }); }); void it('replaces owner placeholder in s3 prefix', () => { @@ -274,7 +299,7 @@ void describe('StorageAccessOrchestrator', () => { acceptResourceAccess: acceptResourceAccessMock, }), ], - idSubstitution: '{testOwnerSub}', + idSubstitution: entityIdSubstitution, uniqueDefinitionIdValidations: accessDefinitionTestDefaults('acceptor') .uniqueDefinitionIdValidations, @@ -286,7 +311,8 @@ void describe('StorageAccessOrchestrator', () => { storageAccessPolicyFactory ); - storageAccessOrchestrator.orchestrateStorageAccess(); + const storageAccessDefinitionOutput = + storageAccessOrchestrator.orchestrateStorageAccess(); assert.equal(acceptResourceAccessMock.mock.callCount(), 1); assert.deepStrictEqual( acceptResourceAccessMock.mock.calls[0].arguments[0].document.toJSON(), @@ -295,12 +321,12 @@ void describe('StorageAccessOrchestrator', () => { { Action: 's3:GetObject', Effect: 'Allow', - Resource: `${bucket.bucketArn}/test/{testOwnerSub}/*`, + Resource: `${bucket.bucketArn}/test/${entityIdSubstitution}/*`, }, { Action: 's3:PutObject', Effect: 'Allow', - Resource: `${bucket.bucketArn}/test/{testOwnerSub}/*`, + Resource: `${bucket.bucketArn}/test/${entityIdSubstitution}/*`, }, ], Version: '2012-10-17', @@ -310,6 +336,11 @@ void describe('StorageAccessOrchestrator', () => { acceptResourceAccessMock.mock.calls[0].arguments[1], ssmEnvironmentEntriesStub ); + assert.deepStrictEqual(storageAccessDefinitionOutput, { + [`test/${entityIdSubstitution}/*`]: { + acceptor: ['get', 'write'], + }, + }); }); void it('denies parent actions on a subpath by default', () => { @@ -347,7 +378,8 @@ void describe('StorageAccessOrchestrator', () => { storageAccessPolicyFactory ); - storageAccessOrchestrator.orchestrateStorageAccess(); + const storageAccessDefinitionOutput = + storageAccessOrchestrator.orchestrateStorageAccess(); assert.equal(acceptResourceAccessMock1.mock.callCount(), 1); assert.deepStrictEqual( acceptResourceAccessMock1.mock.calls[0].arguments[0].document.toJSON(), @@ -396,6 +428,14 @@ void describe('StorageAccessOrchestrator', () => { Version: '2012-10-17', } ); + assert.deepStrictEqual(storageAccessDefinitionOutput, { + 'foo/*': { + acceptor1: ['get', 'write'], + }, + 'foo/bar/*': { + acceptor2: ['get'], + }, + }); }); void it('combines owner rules for same resource access acceptor', () => { @@ -411,7 +451,7 @@ void describe('StorageAccessOrchestrator', () => { { actions: ['write', 'delete'], getResourceAccessAcceptors: [authenticatedResourceAccessAcceptor], - idSubstitution: '{idSub}', + idSubstitution: entityIdSubstitution, uniqueDefinitionIdValidations: accessDefinitionTestDefaults('auth-with-id') .uniqueDefinitionIdValidations, @@ -428,7 +468,8 @@ void describe('StorageAccessOrchestrator', () => { storageAccessPolicyFactory ); - storageAccessOrchestrator.orchestrateStorageAccess(); + const storageAccessDefinitionOutput = + storageAccessOrchestrator.orchestrateStorageAccess(); assert.equal(acceptResourceAccessMock.mock.callCount(), 1); assert.deepStrictEqual( acceptResourceAccessMock.mock.calls[0].arguments[0].document.toJSON(), @@ -437,17 +478,17 @@ void describe('StorageAccessOrchestrator', () => { { Action: 's3:PutObject', Effect: 'Allow', - Resource: `${bucket.bucketArn}/foo/{idSub}/*`, + Resource: `${bucket.bucketArn}/foo/${entityIdSubstitution}/*`, }, { Action: 's3:DeleteObject', Effect: 'Allow', - Resource: `${bucket.bucketArn}/foo/{idSub}/*`, + Resource: `${bucket.bucketArn}/foo/${entityIdSubstitution}/*`, }, { Action: 's3:GetObject', Effect: 'Allow', - Resource: `${bucket.bucketArn}/foo/*/*`, + Resource: `${bucket.bucketArn}/foo/*`, }, ], Version: '2012-10-17', @@ -457,6 +498,14 @@ void describe('StorageAccessOrchestrator', () => { acceptResourceAccessMock.mock.calls[0].arguments[1], ssmEnvironmentEntriesStub ); + assert.deepStrictEqual(storageAccessDefinitionOutput, { + 'foo/*': { + auth: ['get'], + }, + [`foo/${entityIdSubstitution}/*`]: { + 'auth-with-id': ['write', 'delete'], + }, + }); }); void it('handles multiple resource access acceptors on multiple prefixes', () => { @@ -488,7 +537,7 @@ void describe('StorageAccessOrchestrator', () => { { actions: ['get'], getResourceAccessAcceptors: [getResourceAccessAcceptorStub2], - idSubstitution: '{idSub}', + idSubstitution: entityIdSubstitution, uniqueDefinitionIdValidations: accessDefinitionTestDefaults('stub2') .uniqueDefinitionIdValidations, @@ -509,7 +558,7 @@ void describe('StorageAccessOrchestrator', () => { { actions: ['get', 'write', 'delete'], getResourceAccessAcceptors: [getResourceAccessAcceptorStub2], - idSubstitution: '{idSub}', + idSubstitution: entityIdSubstitution, uniqueDefinitionIdValidations: accessDefinitionTestDefaults('stub2') .uniqueDefinitionIdValidations, @@ -526,7 +575,8 @@ void describe('StorageAccessOrchestrator', () => { storageAccessPolicyFactory ); - storageAccessOrchestrator.orchestrateStorageAccess(); + const storageAccessDefinitionOutput = + storageAccessOrchestrator.orchestrateStorageAccess(); assert.equal(acceptResourceAccessMock1.mock.callCount(), 1); assert.deepStrictEqual( acceptResourceAccessMock1.mock.calls[0].arguments[0].document.toJSON(), @@ -537,7 +587,7 @@ void describe('StorageAccessOrchestrator', () => { Effect: 'Allow', Resource: [ `${bucket.bucketArn}/foo/*`, - `${bucket.bucketArn}/other/*/*`, + `${bucket.bucketArn}/other/*`, ], }, { @@ -577,23 +627,40 @@ void describe('StorageAccessOrchestrator', () => { Effect: 'Allow', Resource: [ `${bucket.bucketArn}/foo/bar/*`, - `${bucket.bucketArn}/other/{idSub}/*`, + `${bucket.bucketArn}/other/${entityIdSubstitution}/*`, ], }, { Action: 's3:PutObject', Effect: 'Allow', - Resource: `${bucket.bucketArn}/other/{idSub}/*`, + Resource: `${bucket.bucketArn}/other/${entityIdSubstitution}/*`, }, { Action: 's3:DeleteObject', Effect: 'Allow', - Resource: `${bucket.bucketArn}/other/{idSub}/*`, + Resource: `${bucket.bucketArn}/other/${entityIdSubstitution}/*`, }, ], Version: '2012-10-17', } ); + assert.deepStrictEqual(storageAccessDefinitionOutput, { + 'foo/*': { + stub1: ['get', 'write'], + }, + 'foo/bar/*': { + stub2: ['get'], + }, + 'foo/baz/*': { + stub1: ['get'], + }, + 'other/*': { + stub1: ['get'], + }, + [`other/${entityIdSubstitution}/*`]: { + stub2: ['get', 'write', 'delete'], + }, + }); }); void it('throws validation error for multiple rules on the same resource access acceptor', () => { @@ -658,7 +725,8 @@ void describe('StorageAccessOrchestrator', () => { storageAccessPolicyFactory ); - storageAccessOrchestrator.orchestrateStorageAccess(); + const storageAccessDefinitionOutput = + storageAccessOrchestrator.orchestrateStorageAccess(); assert.equal(acceptResourceAccessMock.mock.callCount(), 1); assert.deepStrictEqual( acceptResourceAccessMock.mock.calls[0].arguments[0].document.toJSON(), @@ -695,6 +763,14 @@ void describe('StorageAccessOrchestrator', () => { acceptResourceAccessMock.mock.calls[0].arguments[1], ssmEnvironmentEntriesStub ); + assert.deepStrictEqual(storageAccessDefinitionOutput, { + 'foo/bar/*': { + auth: ['get', 'list'], + }, + 'other/baz/*': { + auth: ['get', 'list'], + }, + }); }); }); }); diff --git a/packages/backend-storage/src/storage_access_orchestrator.ts b/packages/backend-storage/src/storage_access_orchestrator.ts index e4b4c0e22a..35d2553ab4 100644 --- a/packages/backend-storage/src/storage_access_orchestrator.ts +++ b/packages/backend-storage/src/storage_access_orchestrator.ts @@ -8,11 +8,15 @@ import { StorageAccessGenerator, StoragePath, } from './types.js'; -import { entityIdPathToken } from './constants.js'; +import { entityIdPathToken, entityIdSubstitution } from './constants.js'; import { StorageAccessPolicyFactory } from './storage_access_policy_factory.js'; import { validateStorageAccessPaths as _validateStorageAccessPaths } from './validate_storage_access_paths.js'; import { roleAccessBuilder as _roleAccessBuilder } from './access_builder.js'; -import { InternalStorageAction, StorageError } from './private_types.js'; +import { + InternalStorageAction, + StorageAccessConfig, + StorageError, +} from './private_types.js'; import { AmplifyUserError } from '@aws-amplify/platform-core'; /* some types internal to this file to improve readability */ @@ -88,12 +92,26 @@ export class StorageAccessOrchestrator { // verify that the paths in the access definition are valid this.validateStorageAccessPaths(Object.keys(storageAccessDefinition)); + const storageOutputAccessDefinition: Record = + {}; + // iterate over the access definition and group permissions by ResourceAccessAcceptor Object.entries(storageAccessDefinition).forEach( ([s3Prefix, accessPermissions]) => { const uniqueDefinitionIdSet = new Set(); // iterate over all of the access definitions for a given prefix accessPermissions.forEach((permission) => { + const accessConfig: StorageAccessConfig = {}; + // replace "read" with "get" and "list" in actions + const replaceReadWithGetAndList = permission.actions.flatMap( + (action) => (action === 'read' ? ['get', 'list'] : [action]) + ) as InternalStorageAction[]; + + // ensure the actions list has no duplicates + const noDuplicateActions = Array.from( + new Set(replaceReadWithGetAndList) + ); + // iterate over all uniqueDefinitionIdValidations and ensure uniqueness within this path prefix permission.uniqueDefinitionIdValidations.forEach( ({ uniqueDefinitionId, validationErrorOptions }) => { @@ -105,24 +123,21 @@ export class StorageAccessOrchestrator { } else { uniqueDefinitionIdSet.add(uniqueDefinitionId); } + + accessConfig[uniqueDefinitionId] = noDuplicateActions; } ); // make the owner placeholder substitution in the s3 prefix - const prefix = s3Prefix.replaceAll( - entityIdPathToken, + const prefix = placeholderSubstitution( + s3Prefix, permission.idSubstitution - ) as StoragePath; - - // replace "read" with "get" and "list" in actions - const replaceReadWithGetAndList = permission.actions.flatMap( - (action) => (action === 'read' ? ['get', 'list'] : [action]) - ) as InternalStorageAction[]; - - // ensure the actions list has no duplicates - const noDuplicateActions = Array.from( - new Set(replaceReadWithGetAndList) ); + storageOutputAccessDefinition[prefix] = { + ...storageOutputAccessDefinition[prefix], + ...accessConfig, + }; + // set an entry that maps this permission to each resource acceptor permission.getResourceAccessAcceptors.forEach( (getResourceAccessAcceptor) => { @@ -139,6 +154,8 @@ export class StorageAccessOrchestrator { // iterate over the access map entries and invoke each ResourceAccessAcceptor to accept the permissions this.attachPolicies(this.ssmEnvironmentEntries); + + return storageOutputAccessDefinition; }; /** @@ -195,7 +212,11 @@ export class StorageAccessOrchestrator { const allPaths = Array.from(this.prefixDenyMap.keys()); allPaths.forEach((storagePath) => { const parent = findParent(storagePath, allPaths); - if (!parent) { + // do not add to prefix deny map if there is no parent or the path is a subpath with entity id + if ( + !parent || + parent === storagePath.replaceAll(`${entityIdSubstitution}/`, '') + ) { return; } // if a parent path is defined, invoke the denyByDefault callback on this subpath for all policies that exist on the parent path @@ -258,6 +279,26 @@ export class StorageAccessOrchestratorFactory { ); } +/** + * Performs the owner placeholder substitution in the s3 prefix + */ +const placeholderSubstitution = ( + s3Prefix: string, + idSubstitution: string +): StoragePath => { + const prefix = s3Prefix.replaceAll( + entityIdPathToken, + idSubstitution + ) as StoragePath; + + // for owner paths where prefix ends with '/*/*' remove the last wildcard + if (prefix.endsWith('/*/*')) { + return prefix.slice(0, -2) as StoragePath; + } + + return prefix as StoragePath; +}; + /** * Returns the element in paths that is a prefix of path, if any * Note that there can only be one at this point because of upstream validation diff --git a/packages/backend-storage/src/storage_container_entry_generator.ts b/packages/backend-storage/src/storage_container_entry_generator.ts index 90fbd59bf3..441b54bb86 100644 --- a/packages/backend-storage/src/storage_container_entry_generator.ts +++ b/packages/backend-storage/src/storage_container_entry_generator.ts @@ -1,4 +1,5 @@ import { + AmplifyResourceGroupName, ConstructContainerEntryGenerator, ConstructFactoryGetInstanceProps, GenerateContainerEntryProps, @@ -17,7 +18,7 @@ import { TagName } from '@aws-amplify/platform-core'; export class StorageContainerEntryGenerator implements ConstructContainerEntryGenerator { - readonly resourceGroupName = 'storage'; + readonly resourceGroupName: AmplifyResourceGroupName = 'storage'; /** * Initialize with context from storage factory @@ -78,7 +79,9 @@ export class StorageContainerEntryGenerator ); // the orchestrator generates policies according to the accessDefinition and attaches the policies to appropriate roles - storageAccessOrchestrator.orchestrateStorageAccess(); + const storageAccessOutput = + storageAccessOrchestrator.orchestrateStorageAccess(); + amplifyStorage.addAccessDefinition(storageAccessOutput); return amplifyStorage; }; diff --git a/packages/backend-storage/src/storage_outputs_aspect.test.ts b/packages/backend-storage/src/storage_outputs_aspect.test.ts index 7487f0864d..cccf5ba94c 100644 --- a/packages/backend-storage/src/storage_outputs_aspect.test.ts +++ b/packages/backend-storage/src/storage_outputs_aspect.test.ts @@ -7,6 +7,7 @@ import { StorageOutput } from '@aws-amplify/backend-output-schemas'; import { App, Stack } from 'aws-cdk-lib'; import { IConstruct } from 'constructs'; import { AmplifyUserError } from '@aws-amplify/platform-core'; +import { StorageAccessDefinitionOutput } from './private_types.js'; void describe('StorageOutputsAspect', () => { let app: App; @@ -129,6 +130,49 @@ void describe('StorageOutputsAspect', () => { }) ); }); + + void it('should add access paths if the storage has access rules configured', () => { + const accessDefinition = { + 'path/*': { + authenticated: ['get', 'list', 'write', 'delete'], + guest: ['get', 'list'], + }, + }; + const node = new AmplifyStorage(stack, 'test', { name: 'testName' }); + node.addAccessDefinition( + accessDefinition as StorageAccessDefinitionOutput + ); + aspect = new StorageOutputsAspect(outputStorageStrategy); + aspect.visit(node); + + assert.equal( + addBackendOutputEntryMock.mock.calls[0].arguments[0], + 'AWS::Amplify::Storage' + ); + assert.equal( + addBackendOutputEntryMock.mock.calls[0].arguments[1].payload + .storageRegion, + Stack.of(node).region + ); + assert.equal( + addBackendOutputEntryMock.mock.calls[0].arguments[1].payload.bucketName, + node.resources.bucket.bucketName + ); + assert.equal( + appendToBackendOutputListMock.mock.calls[0].arguments[0], + 'AWS::Amplify::Storage' + ); + assert.equal( + appendToBackendOutputListMock.mock.calls[0].arguments[1].payload + .buckets, + JSON.stringify({ + name: node.name, + bucketName: node.resources.bucket.bucketName, + storageRegion: Stack.of(node).region, + paths: accessDefinition, + }) + ); + }); }); void describe('Validate', () => { diff --git a/packages/backend-storage/src/storage_outputs_aspect.ts b/packages/backend-storage/src/storage_outputs_aspect.ts index 9e9a9fcfa0..dd448937a5 100644 --- a/packages/backend-storage/src/storage_outputs_aspect.ts +++ b/packages/backend-storage/src/storage_outputs_aspect.ts @@ -7,6 +7,7 @@ import { BackendOutputStorageStrategy } from '@aws-amplify/plugin-types'; import { IAspect, Stack } from 'aws-cdk-lib'; import { IConstruct } from 'constructs'; import { AmplifyStorage } from './construct.js'; +import { StorageAccessDefinitionOutput } from './private_types.js'; /** * Aspect to store the storage outputs in the backend @@ -95,16 +96,22 @@ export class StorageOutputsAspect implements IAspect { }, }); } - + const bucketsPayload: Record< + string, + string | StorageAccessDefinitionOutput + > = { + name: node.name, + bucketName: node.resources.bucket.bucketName, + storageRegion: Stack.of(node).region, + }; + if (node.accessDefinition) { + bucketsPayload.paths = node.accessDefinition; + } // both default and non-default buckets should have the name, bucket name, and storage region stored in `buckets` field outputStorageStrategy.appendToBackendOutputList(storageOutputKey, { version: '1', payload: { - buckets: JSON.stringify({ - name: node.name, - bucketName: node.resources.bucket.bucketName, - storageRegion: Stack.of(node).region, - }), + buckets: JSON.stringify(bucketsPayload), }, }); }; diff --git a/packages/backend-storage/src/types.ts b/packages/backend-storage/src/types.ts index 39f0a4dead..caf7abc1c0 100644 --- a/packages/backend-storage/src/types.ts +++ b/packages/backend-storage/src/types.ts @@ -41,17 +41,29 @@ export type StorageAccessBuilder = { /** * Configure storage access for authenticated users. Requires `defineAuth` in the backend definition. * @see https://docs.amplify.aws/gen2/build-a-backend/storage/#authenticated-user-access + * + * When configuring access for paths with the `{entity_id}` token, the token is replaced with a wildcard (`*`). + * For a path like `media/profile-pictures/{entity_id}/*`, this means access is configured for authenticated users for any file within + * `media/profile-pictures/*`. */ authenticated: StorageActionBuilder; /** * Configure storage access for guest (unauthenticated) users. Requires `defineAuth` in the backend definition. * @see https://docs.amplify.aws/gen2/build-a-backend/storage/#guest-user-access + * + * When configuring access for paths with the `{entity_id}` token, the token is replaced with a wildcard (`*`). + * For a path like `media/profile-pictures/{entity_id}/*`, this means access is configured for guest users for any file within + * `media/profile-pictures/*`. */ guest: StorageActionBuilder; /** * Configure storage access for User Pool groups. Requires `defineAuth` with groups config in the backend definition. * @see https://docs.amplify.aws/gen2/build-a-backend/storage/#user-group-access * @param groupName The User Pool group name to configure access for + * + * When configuring access for paths with the `{entity_id}` token, the token is replaced with a wildcard (`*`). + * For a path like `media/profile-pictures/{entity_id}/*`, this means access is configured for that specific group for any file within + * `media/profile-pictures/*`. */ groups: (groupNames: string[]) => StorageActionBuilder; /** @@ -64,6 +76,10 @@ export type StorageAccessBuilder = { * Grant other resources in the Amplify backend access to storage. * @see https://docs.amplify.aws/gen2/build-a-backend/storage/#grant-function-access * @param other The target resource to grant access to. Currently only the return value of `defineFunction` is supported. + * + * When configuring access for paths with the `{entity_id}` token, the token is replaced with a wildcard (`*`). + * For a path like `media/profile-pictures/{entity_id}/*`, this means access is configured for resources for any file within + * `media/profile-pictures/*`. */ resource: ( other: ConstructFactory diff --git a/packages/backend/API.md b/packages/backend/API.md index 1cb83ee09f..c9d790427f 100644 --- a/packages/backend/API.md +++ b/packages/backend/API.md @@ -26,6 +26,7 @@ import { defineStorage } from '@aws-amplify/backend-storage'; import { FunctionResources } from '@aws-amplify/plugin-types'; import { GenerateContainerEntryProps } from '@aws-amplify/plugin-types'; import { ImportPathVerifier } from '@aws-amplify/plugin-types'; +import { referenceAuth } from '@aws-amplify/backend-auth'; import { ResourceAccessAcceptorFactory } from '@aws-amplify/plugin-types'; import { ResourceProvider } from '@aws-amplify/plugin-types'; import { SsmEnvironmentEntriesGenerator } from '@aws-amplify/plugin-types'; @@ -49,6 +50,7 @@ export type Backend = BackendBase & { export type BackendBase = { createStack: (name: string) => Stack; addOutput: (clientConfigPart: DeepPartialAmplifyGeneratedConfigs) => void; + stack: Stack; }; export { BackendOutputEntry } @@ -89,6 +91,8 @@ export { GenerateContainerEntryProps } export { ImportPathVerifier } +export { referenceAuth } + export { ResourceProvider } // @public diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index cc029c0cb0..4add5492b2 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,175 @@ # @aws-amplify/backend +## 1.8.0 + +### Minor Changes + +- f1db886: add resourceGroupName prop to function + +### Patch Changes + +- 97697a9: set removal policy for resources to destroy in sandbox deployments +- Updated dependencies [f1db886] +- Updated dependencies [71ef398] + - @aws-amplify/backend-function@1.8.0 + - @aws-amplify/backend-storage@1.2.3 + - @aws-amplify/plugin-types@1.5.0 + - @aws-amplify/backend-auth@1.4.1 + - @aws-amplify/backend-data@1.2.1 + - @aws-amplify/platform-core@1.2.1 + +## 1.7.0 + +### Minor Changes + +- 90a7c49: Add support for referenceAuth. + +### Patch Changes + +- 12cf209: update error mapping to catch when Lambda layer ARN regions do not match function region +- Updated dependencies [90a7c49] +- Updated dependencies [12cf209] + - @aws-amplify/backend-auth@1.4.0 + - @aws-amplify/backend-data@1.2.0 + - @aws-amplify/plugin-types@1.4.0 + - @aws-amplify/backend-function@1.7.5 + +## 1.6.2 + +### Patch Changes + +- 583a3f2: Fix detection of AmplifyErrors +- Updated dependencies [583a3f2] + - @aws-amplify/platform-core@1.2.0 + - @aws-amplify/backend-data@1.1.7 + +## 1.6.1 + +### Patch Changes + +- 4e97389: add validation if layer arn region does not match function region +- Updated dependencies [d0d8d4e] +- Updated dependencies [4e97389] + - @aws-amplify/client-config@1.5.2 + - @aws-amplify/backend-function@1.7.4 + +## 1.6.0 + +### Minor Changes + +- 11d62fe: Add support for custom Lambda function email senders in Auth construct + +### Patch Changes + +- b56d344: update aws-cdk lib to ^2.158.0 +- Updated dependencies [11d62fe] +- Updated dependencies [b56d344] + - @aws-amplify/backend-auth@1.3.0 + - @aws-amplify/backend-output-storage@1.1.3 + - @aws-amplify/backend-function@1.7.3 + - @aws-amplify/backend-storage@1.2.2 + - @aws-amplify/client-config@1.5.1 + - @aws-amplify/backend-data@1.1.6 + - @aws-amplify/plugin-types@1.3.1 + +## 1.5.2 + +### Patch Changes + +- 601a2c1: dedupe environment variables in amplify env type generator +- Updated dependencies [601a2c1] + - @aws-amplify/backend-function@1.7.2 + +## 1.5.1 + +### Patch Changes + +- 5f46d8d: add user groups to outputs +- Updated dependencies [0d6489d] +- Updated dependencies [bd4ff4d] +- Updated dependencies [5f46d8d] + - @aws-amplify/backend-data@1.1.5 + - @aws-amplify/backend-function@1.7.1 + - @aws-amplify/backend-output-schemas@1.4.0 + - @aws-amplify/client-config@1.5.0 + +## 1.5.0 + +### Minor Changes + +- 4720412: Add minify option to defineFunction + +### Patch Changes + +- Updated dependencies [f87cc87] +- Updated dependencies [4720412] + - @aws-amplify/backend-secret@1.1.4 + - @aws-amplify/backend-function@1.7.0 + +## 1.4.0 + +### Minor Changes + +- f5d0ab4: adds support to reference existing layers in defineFunction + +### Patch Changes + +- Updated dependencies [f5d0ab4] + - @aws-amplify/backend-function@1.6.0 + +## 1.3.2 + +### Patch Changes + +- 0a5e51c: Stream conversation logs in sandbox +- Updated dependencies [0a5e51c] + - @aws-amplify/backend-output-schemas@1.3.0 + +## 1.3.1 + +### Patch Changes + +- d538ecc: add storage access rules to outputs +- Updated dependencies [d538ecc] + - @aws-amplify/client-config@1.4.0 + - @aws-amplify/backend-output-schemas@1.2.1 + - @aws-amplify/backend-storage@1.2.1 + +## 1.3.0 + +### Minor Changes + +- 87dbf41: expose stack property for backend, function resource, storage resource, and auth resource + +### Patch Changes + +- Updated dependencies [87dbf41] +- Updated dependencies [87dbf41] + - @aws-amplify/backend-function@1.5.0 + - @aws-amplify/backend-auth@1.2.0 + - @aws-amplify/backend-storage@1.2.0 + - @aws-amplify/plugin-types@1.3.0 + +## 1.2.2 + +### Patch Changes + +- e648e8e: added main field to package.json so these packages are resolvable +- Updated dependencies [ffc3b42] +- Updated dependencies [e648e8e] +- Updated dependencies [0ff73ec] +- Updated dependencies [c9c873c] +- Updated dependencies [8dd7286] +- Updated dependencies [e648e8e] + - @aws-amplify/backend-data@1.1.4 + - @aws-amplify/backend-function@1.4.1 + - @aws-amplify/backend-storage@1.1.3 + - @aws-amplify/backend-secret@1.1.2 + - @aws-amplify/client-config@1.3.1 + - @aws-amplify/backend-auth@1.1.5 + - @aws-amplify/backend-output-storage@1.1.2 + - @aws-amplify/plugin-types@1.2.2 + ## 1.2.1 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index a411301bfe..b436de0b74 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/backend", - "version": "1.2.1", + "version": "1.8.0", "type": "module", "publishConfig": { "access": "public" @@ -26,21 +26,21 @@ "license": "Apache-2.0", "dependencies": { "@aws-amplify/data-schema": "^1.0.0", - "@aws-amplify/backend-auth": "^1.1.2", - "@aws-amplify/backend-function": "^1.4.0", - "@aws-amplify/backend-data": "^1.1.3", - "@aws-amplify/backend-output-schemas": "^1.2.0", - "@aws-amplify/backend-output-storage": "^1.1.1", - "@aws-amplify/backend-secret": "^1.0.1", - "@aws-amplify/backend-storage": "^1.1.1", - "@aws-amplify/client-config": "^1.3.0", - "@aws-amplify/platform-core": "^1.1.0", - "@aws-amplify/plugin-types": "^1.2.1", + "@aws-amplify/backend-auth": "^1.4.1", + "@aws-amplify/backend-function": "^1.8.0", + "@aws-amplify/backend-data": "^1.2.1", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/backend-output-storage": "^1.1.3", + "@aws-amplify/backend-secret": "^1.1.4", + "@aws-amplify/backend-storage": "^1.2.3", + "@aws-amplify/client-config": "^1.5.2", + "@aws-amplify/platform-core": "^1.2.1", + "@aws-amplify/plugin-types": "^1.5.0", "@aws-sdk/client-amplify": "^3.624.0", "lodash.snakecase": "^4.1.1" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0" }, "devDependencies": { diff --git a/packages/backend/src/backend.ts b/packages/backend/src/backend.ts index a4e767ada2..bd2d5d3d2f 100644 --- a/packages/backend/src/backend.ts +++ b/packages/backend/src/backend.ts @@ -12,6 +12,7 @@ export type BackendBase = { addOutput: ( clientConfigPart: DeepPartialAmplifyGeneratedConfigs ) => void; + stack: Stack; }; // Type that allows construct factories to be defined using any keys except those used in BackendHelpers diff --git a/packages/backend/src/backend_factory.test.ts b/packages/backend/src/backend_factory.test.ts index 4016e50b78..17567b85a8 100644 --- a/packages/backend/src/backend_factory.test.ts +++ b/packages/backend/src/backend_factory.test.ts @@ -148,6 +148,11 @@ void describe('Backend', () => { ); }); + void it('verifies stack property exists and is equivalent to rootStack', () => { + const backend = new BackendFactory({}, rootStack); + assert.equal(rootStack, backend.stack); + }); + void it('stores attribution metadata in root stack', () => { new BackendFactory({}, rootStack); const rootStackTemplate = Template.fromStack(rootStack); @@ -191,7 +196,7 @@ void describe('Backend', () => { const backend = new BackendFactory({}, rootStack); const clientConfigPartial: DeepPartialAmplifyGeneratedConfigs = { - version: '1.1', + version: '1.3', custom: { someCustomOutput: 'someCustomOutputValue', }, diff --git a/packages/backend/src/backend_factory.ts b/packages/backend/src/backend_factory.ts index d9bf2f545a..3a7218b3aa 100644 --- a/packages/backend/src/backend_factory.ts +++ b/packages/backend/src/backend_factory.ts @@ -33,7 +33,7 @@ const rootStackTypeIdentifier = 'root'; // Client config version that is used by `backend.addOutput()` const DEFAULT_CLIENT_CONFIG_VERSION_FOR_BACKEND_ADD_OUTPUT = - ClientConfigVersionOption.V1_1; + ClientConfigVersionOption.V1_3; /** * Factory that collects and instantiates all the Amplify backend constructs @@ -49,6 +49,7 @@ export class BackendFactory< [K in keyof T]: ReturnType; }; + readonly stack; private readonly stackResolver: StackResolver; private readonly customOutputsAccumulator: CustomOutputsAccumulator; /** @@ -56,6 +57,7 @@ export class BackendFactory< * If no CDK App is specified a new one is created */ constructor(constructFactories: T, stack: Stack = createDefaultStack()) { + this.stack = stack; new AttributionMetadataStorage().storeAttributionMetadata( stack, rootStackTypeIdentifier, @@ -157,5 +159,6 @@ export const defineBackend = ( ...backend.resources, createStack: backend.createStack, addOutput: backend.addOutput, + stack: backend.stack, }; }; diff --git a/packages/backend/src/engine/amplify_stack.test.ts b/packages/backend/src/engine/amplify_stack.test.ts index 707cb7f537..a24b898f57 100644 --- a/packages/backend/src/engine/amplify_stack.test.ts +++ b/packages/backend/src/engine/amplify_stack.test.ts @@ -4,11 +4,19 @@ import { AmplifyStack } from './amplify_stack.js'; import { Template } from 'aws-cdk-lib/assertions'; import assert from 'node:assert'; import { FederatedPrincipal, Role } from 'aws-cdk-lib/aws-iam'; +import { BackendIdentifier } from '@aws-amplify/plugin-types'; +import { Bucket } from 'aws-cdk-lib/aws-s3'; + +const branchBackendId: BackendIdentifier = { + namespace: 'testId', + name: 'testBranch', + type: 'branch', +}; void describe('AmplifyStack', () => { void it('renames nested stack logical IDs to non-redundant value', () => { const app = new App(); - const rootStack = new AmplifyStack(app, 'test-id'); + const rootStack = new AmplifyStack(app, branchBackendId); new NestedStack(rootStack, 'testName'); const rootStackTemplate = Template.fromStack(rootStack); @@ -23,7 +31,7 @@ void describe('AmplifyStack', () => { void it('allows roles with properly configured cognito trust policies', () => { const app = new App(); - const rootStack = new AmplifyStack(app, 'test-id'); + const rootStack = new AmplifyStack(app, branchBackendId); new Role(rootStack, 'correctRole', { assumedBy: new FederatedPrincipal( 'cognito-identity.amazonaws.com', @@ -43,7 +51,7 @@ void describe('AmplifyStack', () => { void it('throws on roles with cognito trust policy missing amr condition', () => { const app = new App(); - const rootStack = new AmplifyStack(app, 'test-id'); + const rootStack = new AmplifyStack(app, branchBackendId); new Role(rootStack, 'missingAmrCondition', { assumedBy: new FederatedPrincipal( 'cognito-identity.amazonaws.com', @@ -64,7 +72,7 @@ void describe('AmplifyStack', () => { void it('throws on roles with cognito trust policy missing aud condition', () => { const app = new App(); - const rootStack = new AmplifyStack(app, 'test-id'); + const rootStack = new AmplifyStack(app, branchBackendId); new Role(rootStack, 'missingAudCondition', { assumedBy: new FederatedPrincipal( 'cognito-identity.amazonaws.com', @@ -82,4 +90,32 @@ void describe('AmplifyStack', () => { 'Cannot create a Role trust policy with Cognito that does not have a StringEquals condition for cognito-identity.amazonaws.com:aud', }); }); + + void it('keeps default removal policy of retain for resources in branch deployments', () => { + const app = new App(); + const rootStack = new AmplifyStack(app, branchBackendId); + // bucket has default removal policy to retain + new Bucket(rootStack, 'testBucket', { enforceSSL: true }); + const template = Template.fromStack(rootStack); + + template.hasResource('AWS::S3::Bucket', { + DeletionPolicy: 'Retain', + }); + }); + + void it('sets removal policy to destroy for resources in sandbox deployments', () => { + const app = new App(); + const rootStack = new AmplifyStack(app, { + namespace: 'testId', + name: 'testSandbox', + type: 'sandbox', + }); + // bucket has default removal policy to retain + new Bucket(rootStack, 'testBucket', { enforceSSL: true }); + const template = Template.fromStack(rootStack); + + template.hasResource('AWS::S3::Bucket', { + DeletionPolicy: 'Delete', + }); + }); }); diff --git a/packages/backend/src/engine/amplify_stack.ts b/packages/backend/src/engine/amplify_stack.ts index 4c7475658b..03691563aa 100644 --- a/packages/backend/src/engine/amplify_stack.ts +++ b/packages/backend/src/engine/amplify_stack.ts @@ -1,5 +1,16 @@ -import { AmplifyFault } from '@aws-amplify/platform-core'; -import { Aspects, CfnElement, IAspect, Stack } from 'aws-cdk-lib'; +import { + AmplifyFault, + BackendIdentifierConversions, +} from '@aws-amplify/platform-core'; +import { BackendIdentifier } from '@aws-amplify/plugin-types'; +import { + Aspects, + CfnElement, + CfnResource, + IAspect, + RemovalPolicy, + Stack, +} from 'aws-cdk-lib'; import { Role } from 'aws-cdk-lib/aws-iam'; import { Construct, IConstruct } from 'constructs'; @@ -10,9 +21,13 @@ export class AmplifyStack extends Stack { /** * Default constructor */ - constructor(scope: Construct, id: string) { - super(scope, id); + constructor(scope: Construct, backendId: BackendIdentifier) { + super(scope, BackendIdentifierConversions.toStackName(backendId)); Aspects.of(this).add(new CognitoRoleTrustPolicyValidator()); + + if (backendId.type === 'sandbox') { + Aspects.of(this).add(new SandboxRemovalPolicyDestroyAspect()); + } } /** * Overrides Stack.allocateLogicalId to prevent redundant nested stack logical IDs @@ -93,3 +108,12 @@ class CognitoRoleTrustPolicyValidator implements IAspect { } }; } + +// This aspect sets removal policy of all resources to destroy for sandbox deployments +class SandboxRemovalPolicyDestroyAspect implements IAspect { + visit(node: IConstruct): void { + if (CfnResource.isCfnResource(node)) { + node.applyRemovalPolicy(RemovalPolicy.DESTROY); + } + } +} diff --git a/packages/backend/src/engine/custom_outputs_accumulator.test.ts b/packages/backend/src/engine/custom_outputs_accumulator.test.ts index 6962175e4c..77c4e4896d 100644 --- a/packages/backend/src/engine/custom_outputs_accumulator.test.ts +++ b/packages/backend/src/engine/custom_outputs_accumulator.test.ts @@ -59,11 +59,11 @@ void describe('Custom outputs accumulator', () => { ); const configPart1: DeepPartialAmplifyGeneratedConfigs = { - version: '1.1', + version: '1.3', custom: { output1: 'val1' }, }; const configPart2: DeepPartialAmplifyGeneratedConfigs = { - version: '1.1', + version: '1.3', custom: { output2: 'val2' }, }; accumulator.addOutput(configPart1); @@ -115,7 +115,7 @@ void describe('Custom outputs accumulator', () => { assert.throws( () => - accumulator.addOutput({ version: '1.1', custom: { output1: 'val1' } }), + accumulator.addOutput({ version: '1.3', custom: { output1: 'val1' } }), (error: AmplifyUserError) => { assert.strictEqual( error.message, diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 46adc6515e..5150f93a8e 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -17,7 +17,7 @@ export { defineData } from '@aws-amplify/backend-data'; export { type ClientSchema, a } from '@aws-amplify/data-schema'; // auth -export { defineAuth } from '@aws-amplify/backend-auth'; +export { defineAuth, referenceAuth } from '@aws-amplify/backend-auth'; // storage export { defineStorage } from '@aws-amplify/backend-storage'; diff --git a/packages/backend/src/project_environment_main_stack_creator.ts b/packages/backend/src/project_environment_main_stack_creator.ts index fbadb59b57..04e3d091c5 100644 --- a/packages/backend/src/project_environment_main_stack_creator.ts +++ b/packages/backend/src/project_environment_main_stack_creator.ts @@ -2,7 +2,6 @@ import { BackendIdentifier, MainStackCreator } from '@aws-amplify/plugin-types'; import { Construct } from 'constructs'; import { Stack, Tags } from 'aws-cdk-lib'; import { AmplifyStack } from './engine/amplify_stack.js'; -import { BackendIdentifierConversions } from '@aws-amplify/platform-core'; /** * Creates stacks that are tied to a given project environment via an SSM parameter @@ -22,10 +21,7 @@ export class ProjectEnvironmentMainStackCreator implements MainStackCreator { */ getOrCreateMainStack = (): Stack => { if (this.mainStack === undefined) { - this.mainStack = new AmplifyStack( - this.scope, - BackendIdentifierConversions.toStackName(this.backendId) - ); + this.mainStack = new AmplifyStack(this.scope, this.backendId); } const deploymentType = this.backendId.type; diff --git a/packages/cli-core/CHANGELOG.md b/packages/cli-core/CHANGELOG.md index a99721d821..290f6fb5c1 100644 --- a/packages/cli-core/CHANGELOG.md +++ b/packages/cli-core/CHANGELOG.md @@ -1,5 +1,17 @@ # @aws-amplify/cli-core +## 1.2.0 + +### Minor Changes + +- c3c3057: update ctrl+c behavior to always print guidance to delete and exit with no prompt + +## 1.1.3 + +### Patch Changes + +- 8dd7286: fixed errors in plugin-types and cli-core along with any extraneous dependencies in other packages + ## 1.1.2 ### Patch Changes diff --git a/packages/cli-core/package.json b/packages/cli-core/package.json index b752e10763..0d1e6f022d 100644 --- a/packages/cli-core/package.json +++ b/packages/cli-core/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/cli-core", - "version": "1.1.2", + "version": "1.2.0", "type": "module", "publishConfig": { "access": "public" diff --git a/packages/cli-core/src/package-manager-controller/package_manager_controller_base.ts b/packages/cli-core/src/package-manager-controller/package_manager_controller_base.ts index d722ef84f5..4d9786b391 100644 --- a/packages/cli-core/src/package-manager-controller/package_manager_controller_base.ts +++ b/packages/cli-core/src/package-manager-controller/package_manager_controller_base.ts @@ -137,6 +137,7 @@ export abstract class PackageManagerControllerBase /** * allowsSignalPropagation - Determines if the package manager allows the process * signals such as SIGINT to be propagated to the underlying node process. + * @deprecated */ allowsSignalPropagation = () => true; diff --git a/packages/cli-core/src/package-manager-controller/pnpm_package_manager_controller.ts b/packages/cli-core/src/package-manager-controller/pnpm_package_manager_controller.ts index 321ad9cd26..5eb13ea7de 100644 --- a/packages/cli-core/src/package-manager-controller/pnpm_package_manager_controller.ts +++ b/packages/cli-core/src/package-manager-controller/pnpm_package_manager_controller.ts @@ -32,10 +32,4 @@ export class PnpmPackageManagerController extends PackageManagerControllerBase { existsSync ); } - - /** - * Pnpm doesn't handle the node process gracefully during the SIGINT life cycle. - * See: https://github.com/pnpm/pnpm/issues/7374 - */ - allowsSignalPropagation = () => false; } diff --git a/packages/cli-core/src/package-manager-controller/yarn_classic_package_manager_controller.ts b/packages/cli-core/src/package-manager-controller/yarn_classic_package_manager_controller.ts index 2f7a837e76..48a4330ed9 100644 --- a/packages/cli-core/src/package-manager-controller/yarn_classic_package_manager_controller.ts +++ b/packages/cli-core/src/package-manager-controller/yarn_classic_package_manager_controller.ts @@ -37,12 +37,6 @@ export class YarnClassicPackageManagerController extends PackageManagerControlle await this.addTypescript(targetDir); await super.initializeTsConfig(targetDir); }; - /** - * - * Yarn doesn't respect the SIGINT life cycle and exits immediately leaving - * the node process hanging. See: https://github.com/yarnpkg/yarn/issues/8895 - */ - allowsSignalPropagation = () => false; private addTypescript = async (targetDir: string) => { await this.executeWithDebugLogger( diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index ad5515725e..8c97e85b34 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,105 @@ # @aws-amplify/backend-cli +## 1.4.2 + +### Patch Changes + +- f08abe4: Handle case when AWS region is configured as blank string +- Updated dependencies [443e2ff] +- Updated dependencies [7f2f68b] +- Updated dependencies [90a7c49] +- Updated dependencies [12cf209] + - @aws-amplify/model-generator@1.0.9 + - @aws-amplify/backend-deployer@1.1.9 + - @aws-amplify/plugin-types@1.4.0 + +## 1.4.1 + +### Patch Changes + +- 583a3f2: Fix detection of AmplifyErrors +- Updated dependencies [583a3f2] + - @aws-amplify/platform-core@1.2.0 + - @aws-amplify/backend-deployer@1.1.8 + - @aws-amplify/sandbox@1.2.5 + +## 1.4.0 + +### Minor Changes + +- c3c3057: update ctrl+c behavior to always print guidance to delete and exit with no prompt + +### Patch Changes + +- Updated dependencies [c3c3057] +- Updated dependencies [b56d344] +- Updated dependencies [b56d344] + - @aws-amplify/cli-core@1.2.0 + - @aws-amplify/backend-deployer@1.1.6 + - @aws-amplify/schema-generator@1.2.5 + - @aws-amplify/client-config@1.5.1 + - @aws-amplify/plugin-types@1.3.1 + - @aws-amplify/sandbox@1.2.4 + +## 1.3.0 + +### Minor Changes + +- b2057f9: adds shorthand argument for version and help + +### Patch Changes + +- 5f46d8d: add user groups to outputs +- Updated dependencies [5f46d8d] + - @aws-amplify/backend-output-schemas@1.4.0 + - @aws-amplify/client-config@1.5.0 + +## 1.2.9 + +### Patch Changes + +- d538ecc: add storage access rules to outputs +- Updated dependencies [d538ecc] + - @aws-amplify/client-config@1.4.0 + - @aws-amplify/backend-output-schemas@1.2.1 + +## 1.2.8 + +### Patch Changes + +- 9e11e5d: correctly handle stack argument for generate schema command +- 7aad5e8: update fallback for backend id resolvers if stack, app id, or branch are in args +- Updated dependencies [e325044] +- Updated dependencies [87dbf41] +- Updated dependencies [f6b1943] + - @aws-amplify/schema-generator@1.2.4 + - @aws-amplify/model-generator@1.0.8 + - @aws-amplify/form-generator@1.0.3 + - @aws-amplify/plugin-types@1.3.0 + +## 1.2.7 + +### Patch Changes + +- e648e8e: added main field to package.json so these packages are resolvable +- 8dd7286: fixed errors in plugin-types and cli-core along with any extraneous dependencies in other packages +- Updated dependencies [e648e8e] +- Updated dependencies [0ff73ec] +- Updated dependencies [c9c873c] +- Updated dependencies [cbac105] +- Updated dependencies [8dd7286] +- Updated dependencies [e648e8e] + - @aws-amplify/deployed-backend-client@1.4.1 + - @aws-amplify/backend-deployer@1.1.3 + - @aws-amplify/schema-generator@1.2.3 + - @aws-amplify/model-generator@1.0.7 + - @aws-amplify/backend-secret@1.1.2 + - @aws-amplify/form-generator@1.0.2 + - @aws-amplify/client-config@1.3.1 + - @aws-amplify/sandbox@1.2.2 + - @aws-amplify/plugin-types@1.2.2 + - @aws-amplify/cli-core@1.1.3 + ## 1.2.6 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index bd40712e75..fb5fc6913a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/backend-cli", - "version": "1.2.6", + "version": "1.4.2", "description": "Command line interface for various Amplify tools", "bin": { "ampx": "lib/ampx.js", @@ -31,18 +31,18 @@ }, "homepage": "https://github.com/aws-amplify/amplify-backend#readme", "dependencies": { - "@aws-amplify/backend-deployer": "^1.1.1", - "@aws-amplify/backend-output-schemas": "^1.1.0", - "@aws-amplify/backend-secret": "^1.1.0", - "@aws-amplify/cli-core": "^1.1.2", - "@aws-amplify/client-config": "^1.2.1", - "@aws-amplify/deployed-backend-client": "^1.3.0", - "@aws-amplify/form-generator": "^1.0.1", - "@aws-amplify/model-generator": "^1.0.5", - "@aws-amplify/platform-core": "^1.0.5", - "@aws-amplify/plugin-types": "^1.2.1", - "@aws-amplify/sandbox": "^1.2.0", - "@aws-amplify/schema-generator": "^1.2.1", + "@aws-amplify/backend-deployer": "^1.1.9", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/backend-secret": "^1.1.2", + "@aws-amplify/cli-core": "^1.2.0", + "@aws-amplify/client-config": "^1.5.1", + "@aws-amplify/deployed-backend-client": "^1.4.1", + "@aws-amplify/form-generator": "^1.0.3", + "@aws-amplify/model-generator": "^1.0.9", + "@aws-amplify/platform-core": "^1.2.0", + "@aws-amplify/plugin-types": "^1.4.0", + "@aws-amplify/sandbox": "^1.2.5", + "@aws-amplify/schema-generator": "^1.2.5", "@aws-sdk/client-amplify": "^3.624.0", "@aws-sdk/client-cloudformation": "^3.624.0", "@aws-sdk/client-s3": "^3.624.0", diff --git a/packages/cli/src/backend-identifier/backend_identifier_resolver.test.ts b/packages/cli/src/backend-identifier/backend_identifier_resolver.test.ts index 7493b02270..dba0e76709 100644 --- a/packages/cli/src/backend-identifier/backend_identifier_resolver.test.ts +++ b/packages/cli/src/backend-identifier/backend_identifier_resolver.test.ts @@ -3,38 +3,82 @@ import { describe, it } from 'node:test'; import { AppBackendIdentifierResolver } from './backend_identifier_resolver.js'; void describe('BackendIdentifierResolver', () => { - void it('returns an App Name and Branch identifier', async () => { - const backendIdResolver = new AppBackendIdentifierResolver({ - resolve: () => Promise.resolve('testAppName'), + void describe('resolveDeployedBackendIdentifier', () => { + void it('returns an App Name and Branch identifier', async () => { + const backendIdResolver = new AppBackendIdentifierResolver({ + resolve: () => Promise.resolve('testAppName'), + }); + assert.deepStrictEqual( + await backendIdResolver.resolveDeployedBackendIdentifier({ + branch: 'test', + }), + { + appName: 'testAppName', + branchName: 'test', + } + ); }); - assert.deepStrictEqual( - await backendIdResolver.resolve({ branch: 'test' }), - { - appName: 'testAppName', - branchName: 'test', - } - ); - }); - void it('returns a App Id identifier', async () => { - const backendIdResolver = new AppBackendIdentifierResolver({ - resolve: () => Promise.resolve('testAppName'), - }); - const actual = await backendIdResolver.resolve({ - appId: 'my-id', - branch: 'my-branch', + void it('returns a App Id identifier', async () => { + const backendIdResolver = new AppBackendIdentifierResolver({ + resolve: () => Promise.resolve('testAppName'), + }); + const actual = await backendIdResolver.resolveDeployedBackendIdentifier({ + appId: 'my-id', + branch: 'my-branch', + }); + assert.deepStrictEqual(actual, { + namespace: 'my-id', + name: 'my-branch', + type: 'branch', + }); }); - assert.deepStrictEqual(actual, { - namespace: 'my-id', - name: 'my-branch', - type: 'branch', + void it('returns a Stack name identifier', async () => { + const backendIdResolver = new AppBackendIdentifierResolver({ + resolve: () => Promise.resolve('testAppName'), + }); + assert.deepEqual( + await backendIdResolver.resolveDeployedBackendIdentifier({ + stack: 'my-stack', + }), + { + stackName: 'my-stack', + } + ); }); }); - void it('returns a Stack name identifier', async () => { - const backendIdResolver = new AppBackendIdentifierResolver({ - resolve: () => Promise.resolve('testAppName'), + + void describe('resolveDeployedBackendIdToBackendId', () => { + void it('returns backend identifier from App Name and Branch identifier', async () => { + const backendIdResolver = new AppBackendIdentifierResolver({ + resolve: () => Promise.resolve('testAppName'), + }); + assert.deepEqual( + await backendIdResolver.resolveBackendIdentifier({ + appId: 'testAppName', + branch: 'test', + }), + { + namespace: 'testAppName', + name: 'test', + type: 'branch', + } + ); }); - assert.deepEqual(await backendIdResolver.resolve({ stack: 'my-stack' }), { - stackName: 'my-stack', + void it('returns backend identifier from Stack identifier', async () => { + const backendIdResolver = new AppBackendIdentifierResolver({ + resolve: () => Promise.resolve('testAppName'), + }); + assert.deepEqual( + await backendIdResolver.resolveBackendIdentifier({ + stack: 'amplify-reasonableName-userName-branch-testHash', + }), + { + namespace: 'reasonableName', + name: 'userName', + type: 'branch', + hash: 'testHash', + } + ); }); }); }); diff --git a/packages/cli/src/backend-identifier/backend_identifier_resolver.ts b/packages/cli/src/backend-identifier/backend_identifier_resolver.ts index 1f0aeed51e..0b74437a57 100644 --- a/packages/cli/src/backend-identifier/backend_identifier_resolver.ts +++ b/packages/cli/src/backend-identifier/backend_identifier_resolver.ts @@ -1,5 +1,7 @@ import { DeployedBackendIdentifier } from '@aws-amplify/deployed-backend-client'; import { NamespaceResolver } from './local_namespace_resolver.js'; +import { BackendIdentifier } from '@aws-amplify/plugin-types'; +import { BackendIdentifierConversions } from '@aws-amplify/platform-core'; export type BackendIdentifierParameters = { stack?: string; @@ -8,9 +10,12 @@ export type BackendIdentifierParameters = { }; export type BackendIdentifierResolver = { - resolve: ( + resolveDeployedBackendIdentifier: ( args: BackendIdentifierParameters ) => Promise; + resolveBackendIdentifier: ( + args: BackendIdentifierParameters + ) => Promise; }; /** @@ -22,7 +27,7 @@ export class AppBackendIdentifierResolver implements BackendIdentifierResolver { * Instantiates BackendIdentifierResolver */ constructor(private readonly namespaceResolver: NamespaceResolver) {} - resolve = async ( + resolveDeployedBackendIdentifier = async ( args: BackendIdentifierParameters ): Promise => { if (args.stack) { @@ -41,4 +46,25 @@ export class AppBackendIdentifierResolver implements BackendIdentifierResolver { } return undefined; }; + resolveBackendIdentifier = async ( + args: BackendIdentifierParameters + ): Promise => { + if (args.stack) { + return BackendIdentifierConversions.fromStackName(args.stack); + } else if (args.appId && args.branch) { + return { + namespace: args.appId, + name: args.branch, + type: 'branch', + }; + } else if (args.branch) { + return { + namespace: await this.namespaceResolver.resolve(), + name: args.branch, + type: 'branch', + }; + } + + return undefined; + }; } diff --git a/packages/cli/src/backend-identifier/backend_identifier_with_sandbox_fallback.test.ts b/packages/cli/src/backend-identifier/backend_identifier_with_sandbox_fallback.test.ts index acb95b9d1c..0509b4e80b 100644 --- a/packages/cli/src/backend-identifier/backend_identifier_with_sandbox_fallback.test.ts +++ b/packages/cli/src/backend-identifier/backend_identifier_with_sandbox_fallback.test.ts @@ -15,7 +15,7 @@ void it('if backend identifier resolves without error, the resolved id is return defaultResolver, sandboxResolver ); - const resolvedId = await backendIdResolver.resolve({ + const resolvedId = await backendIdResolver.resolveDeployedBackendIdentifier({ appId: 'hello', branch: 'world', }); @@ -26,7 +26,7 @@ void it('if backend identifier resolves without error, the resolved id is return }); }); -void it('uses the sandbox id if the default identifier resolver fails', async () => { +void it('uses the sandbox id if the default identifier resolver fails and there is no stack, appId or branch in args', async () => { const appName = 'testAppName'; const namespaceResolver = { resolve: () => Promise.resolve(appName), @@ -45,10 +45,37 @@ void it('uses the sandbox id if the default identifier resolver fails', async () defaultResolver, sandboxResolver ); - const resolvedId = await backendIdResolver.resolve({}); + const resolvedId = await backendIdResolver.resolveDeployedBackendIdentifier( + {} + ); assert.deepEqual(resolvedId, { namespace: appName, type: 'sandbox', name: 'test-user', }); }); + +void it('does not use sandbox id if the default identifier resolver fails and there is stack, appId or branch in args', async () => { + const appName = 'testAppName'; + const namespaceResolver = { + resolve: () => Promise.resolve(appName), + }; + + const defaultResolver = new AppBackendIdentifierResolver(namespaceResolver); + const username = 'test-user'; + const sandboxResolver = new SandboxBackendIdResolver( + namespaceResolver, + () => + ({ + username, + } as never) + ); + const backendIdResolver = new BackendIdentifierResolverWithFallback( + defaultResolver, + sandboxResolver + ); + const resolvedId = await backendIdResolver.resolveDeployedBackendIdentifier({ + appId: 'testAppName', + }); + assert.deepEqual(resolvedId, undefined); +}); diff --git a/packages/cli/src/backend-identifier/backend_identifier_with_sandbox_fallback.ts b/packages/cli/src/backend-identifier/backend_identifier_with_sandbox_fallback.ts index c7c632412a..f1bcbd2808 100644 --- a/packages/cli/src/backend-identifier/backend_identifier_with_sandbox_fallback.ts +++ b/packages/cli/src/backend-identifier/backend_identifier_with_sandbox_fallback.ts @@ -18,12 +18,25 @@ export class BackendIdentifierResolverWithFallback private fallbackResolver: SandboxBackendIdResolver ) {} /** - * resolves the backend id, falling back to the sandbox id if there is an error + * resolves to deployed backend id, falling back to the sandbox id if stack or appId and branch inputs are not provided */ - resolve = async (args: BackendIdentifierParameters) => { - return ( - (await this.defaultResolver.resolve(args)) ?? - (await this.fallbackResolver.resolve()) - ); + resolveDeployedBackendIdentifier = async ( + args: BackendIdentifierParameters + ) => { + if (args.stack || args.appId || args.branch) { + return this.defaultResolver.resolveDeployedBackendIdentifier(args); + } + + return this.fallbackResolver.resolve(); + }; + /** + * Resolves deployed backend id to backend id, falling back to the sandbox id if stack or appId and branch inputs are not provided + */ + resolveBackendIdentifier = async (args: BackendIdentifierParameters) => { + if (args.stack || args.appId || args.branch) { + return this.defaultResolver.resolveBackendIdentifier(args); + } + + return this.fallbackResolver.resolve(); }; } diff --git a/packages/cli/src/command_middleware.test.ts b/packages/cli/src/command_middleware.test.ts index 313b4a2003..386e4c7ab2 100644 --- a/packages/cli/src/command_middleware.test.ts +++ b/packages/cli/src/command_middleware.test.ts @@ -122,6 +122,19 @@ void describe('commandMiddleware', () => { } }); + void it('throws error if region is blank', async () => { + process.env.AWS_REGION = ''; + delete process.env.AWS_DEFAULT_REGION; + try { + await commandMiddleware.ensureAwsCredentialAndRegion( + {} as ArgumentsCamelCase<{ profile: string | undefined }> + ); + assert.fail('expect to throw error'); + } catch (err) { + assert.match((err as Error).message, /The AWS region is blank/); + } + }); + void it('throws error if a profile is provided and no other credential providers', async () => { try { await commandMiddleware.ensureAwsCredentialAndRegion({ diff --git a/packages/cli/src/command_middleware.ts b/packages/cli/src/command_middleware.ts index a06d288f74..c1da30912d 100644 --- a/packages/cli/src/command_middleware.ts +++ b/packages/cli/src/command_middleware.ts @@ -60,8 +60,9 @@ export class CommandMiddleware { } // Check region. + let region: string | undefined = undefined; try { - await loadConfig(NODE_REGION_CONFIG_OPTIONS, { + region = await loadConfig(NODE_REGION_CONFIG_OPTIONS, { ignoreCache: true, })(); } catch (err) { @@ -77,6 +78,13 @@ export class CommandMiddleware { err as Error ); } + if (!region.trim()) { + throw new AmplifyUserError('InvalidCredentialError', { + message: 'The AWS region is blank', + resolution: + 'Ensure that a valid AWS region is provided in profile configuration or AWS_REGION environment variable.', + }); + } }; /** diff --git a/packages/cli/src/commands/generate/forms/generate_forms_command.test.ts b/packages/cli/src/commands/generate/forms/generate_forms_command.test.ts index b22f770e26..d5bcd498c7 100644 --- a/packages/cli/src/commands/generate/forms/generate_forms_command.test.ts +++ b/packages/cli/src/commands/generate/forms/generate_forms_command.test.ts @@ -242,7 +242,14 @@ void describe('generate forms command', () => { void it('throws user error if the stack deployment is currently in progress', async () => { const fakeSandboxId = 'my-fake-app-my-fake-username'; const backendIdResolver = { - resolve: mock.fn(() => + resolveDeployedBackendIdentifier: mock.fn(() => + Promise.resolve({ + namespace: fakeSandboxId, + name: fakeSandboxId, + type: 'sandbox', + }) + ), + resolveBackendIdentifier: mock.fn(() => Promise.resolve({ namespace: fakeSandboxId, name: fakeSandboxId, @@ -289,7 +296,14 @@ void describe('generate forms command', () => { void it('throws user error if the stack does not exist', async () => { const fakeSandboxId = 'my-fake-app-my-fake-username'; const backendIdResolver = { - resolve: mock.fn(() => + resolveDeployedBackendIdentifier: mock.fn(() => + Promise.resolve({ + namespace: fakeSandboxId, + name: fakeSandboxId, + type: 'sandbox', + }) + ), + resolveBackendIdentifier: mock.fn(() => Promise.resolve({ namespace: fakeSandboxId, name: fakeSandboxId, @@ -333,7 +347,14 @@ void describe('generate forms command', () => { void it('throws user error if credentials are expired when getting backend outputs', async () => { const fakeSandboxId = 'my-fake-app-my-fake-username'; const backendIdResolver = { - resolve: mock.fn(() => + resolveDeployedBackendIdentifier: mock.fn(() => + Promise.resolve({ + namespace: fakeSandboxId, + name: fakeSandboxId, + type: 'sandbox', + }) + ), + resolveBackendIdentifier: mock.fn(() => Promise.resolve({ namespace: fakeSandboxId, name: fakeSandboxId, @@ -380,7 +401,14 @@ void describe('generate forms command', () => { void it('throws user error if access is denied when getting backend outputs', async () => { const fakeSandboxId = 'my-fake-app-my-fake-username'; const backendIdResolver = { - resolve: mock.fn(() => + resolveDeployedBackendIdentifier: mock.fn(() => + Promise.resolve({ + namespace: fakeSandboxId, + name: fakeSandboxId, + type: 'sandbox', + }) + ), + resolveBackendIdentifier: mock.fn(() => Promise.resolve({ namespace: fakeSandboxId, name: fakeSandboxId, diff --git a/packages/cli/src/commands/generate/forms/generate_forms_command.ts b/packages/cli/src/commands/generate/forms/generate_forms_command.ts index 63afe6aaea..34667029dc 100644 --- a/packages/cli/src/commands/generate/forms/generate_forms_command.ts +++ b/packages/cli/src/commands/generate/forms/generate_forms_command.ts @@ -52,7 +52,9 @@ export class GenerateFormsCommand } getBackendIdentifier = async (args: GenerateFormsCommandOptions) => { - return await this.backendIdentifierResolver.resolve(args); + return await this.backendIdentifierResolver.resolveDeployedBackendIdentifier( + args + ); }; /** @@ -61,12 +63,17 @@ export class GenerateFormsCommand handler = async ( args: ArgumentsCamelCase ): Promise => { - const backendIdentifier = await this.backendIdentifierResolver.resolve( - args - ); + const backendIdentifier = + await this.backendIdentifierResolver.resolveDeployedBackendIdentifier( + args + ); if (!backendIdentifier) { - throw new Error('Could not resolve the backend identifier'); + throw new AmplifyUserError('BackendIdentifierResolverError', { + message: 'Could not resolve the backend identifier.', + resolution: + 'Ensure stack name or Amplify App ID and branch specified are correct and exists, then re-run this command.', + }); } const backendOutputClient = this.backendOutputClientBuilder(); diff --git a/packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_command.ts b/packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_command.ts index 287f3deb37..ec8e4a6ed7 100644 --- a/packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_command.ts +++ b/packages/cli/src/commands/generate/graphql-client-code/generate_graphql_client_code_command.ts @@ -89,9 +89,10 @@ export class GenerateGraphqlClientCodeCommand handler = async ( args: ArgumentsCamelCase ): Promise => { - const backendIdentifier = await this.backendIdentifierResolver.resolve( - args - ); + const backendIdentifier = + await this.backendIdentifierResolver.resolveDeployedBackendIdentifier( + args + ); const out = this.getOutDir(args); const format = args.format ?? GenerateApiCodeFormat.GRAPHQL_CODEGEN; const formatParams = this.formatParamBuilders[format](args); diff --git a/packages/cli/src/commands/generate/outputs/generate_outputs_command.test.ts b/packages/cli/src/commands/generate/outputs/generate_outputs_command.test.ts index 80333f072c..84b4a0f8e6 100644 --- a/packages/cli/src/commands/generate/outputs/generate_outputs_command.test.ts +++ b/packages/cli/src/commands/generate/outputs/generate_outputs_command.test.ts @@ -74,7 +74,7 @@ void describe('generate outputs command', () => { assert.equal(generateClientConfigMock.mock.callCount(), 1); assert.deepEqual( generateClientConfigMock.mock.calls[0].arguments[1], - '1.1' // default version + '1.3' // default version ); assert.deepEqual( generateClientConfigMock.mock.calls[0].arguments[2], @@ -97,7 +97,7 @@ void describe('generate outputs command', () => { assert.equal(generateClientConfigMock.mock.callCount(), 1); assert.deepEqual( generateClientConfigMock.mock.calls[0].arguments[1], - '1.1' // default version + '1.3' // default version ); assert.deepStrictEqual( generateClientConfigMock.mock.calls[0].arguments[2], @@ -118,7 +118,7 @@ void describe('generate outputs command', () => { namespace: 'app_id', type: 'branch', }, - '1.1', + '1.3', '/foo/bar', undefined, ] @@ -136,7 +136,7 @@ void describe('generate outputs command', () => { { stackName: 'stack_name', }, - '1.1', + '1.3', '/foo/bar', undefined, ] @@ -154,7 +154,7 @@ void describe('generate outputs command', () => { { stackName: 'stack_name', }, - '1.1', + '1.3', 'foo/bar', undefined, ] @@ -172,7 +172,7 @@ void describe('generate outputs command', () => { { stackName: 'stack_name', }, - '1.1', + '1.3', 'foo/bar', ClientConfigFormat.DART, ] diff --git a/packages/cli/src/commands/generate/outputs/generate_outputs_command.ts b/packages/cli/src/commands/generate/outputs/generate_outputs_command.ts index 2feb82e7ee..0edb427a00 100644 --- a/packages/cli/src/commands/generate/outputs/generate_outputs_command.ts +++ b/packages/cli/src/commands/generate/outputs/generate_outputs_command.ts @@ -8,6 +8,7 @@ import { import { BackendIdentifierResolver } from '../../../backend-identifier/backend_identifier_resolver.js'; import { ClientConfigGeneratorAdapter } from '../../../client-config/client_config_generator_adapter.js'; import { ArgumentsKebabCase } from '../../../kebab_case.js'; +import { AmplifyUserError } from '@aws-amplify/platform-core'; export type GenerateOutputsCommandOptions = ArgumentsKebabCase; @@ -54,12 +55,17 @@ export class GenerateOutputsCommand handler = async ( args: ArgumentsCamelCase ): Promise => { - const backendIdentifier = await this.backendIdentifierResolver.resolve( - args - ); + const backendIdentifier = + await this.backendIdentifierResolver.resolveDeployedBackendIdentifier( + args + ); if (!backendIdentifier) { - throw new Error('Could not resolve the backend identifier'); + throw new AmplifyUserError('BackendIdentifierResolverError', { + message: 'Could not resolve the backend identifier.', + resolution: + 'Ensure stack name or Amplify App ID and branch specified are correct and exists, then re-run this command.', + }); } await this.clientConfigGenerator.generateClientConfigToFile( diff --git a/packages/cli/src/commands/generate/schema-from-database/generate_schema_command.test.ts b/packages/cli/src/commands/generate/schema-from-database/generate_schema_command.test.ts index 1144459a6f..6fbb0ffdf8 100644 --- a/packages/cli/src/commands/generate/schema-from-database/generate_schema_command.test.ts +++ b/packages/cli/src/commands/generate/schema-from-database/generate_schema_command.test.ts @@ -117,9 +117,15 @@ void describe('generate graphql-client-code command', () => { void it('generates and writes schema for stack', async () => { await commandRunner.runCommand( - 'schema-from-database --stack stack_name --connection-uri-secret CONN_STRING --out schema.rds.ts' + 'schema-from-database --stack amplify-reasonableName-userName-sandbox-testHash --connection-uri-secret CONN_STRING --out schema.rds.ts' ); assert.equal(secretClientGetSecret.mock.callCount(), 1); + assert.deepEqual(secretClientGetSecret.mock.calls[0].arguments[0], { + namespace: 'reasonableName', + name: 'userName', + type: 'sandbox', + hash: 'testHash', + }); assert.equal(schemaGeneratorGenerateMethod.mock.callCount(), 1); assert.deepEqual(schemaGeneratorGenerateMethod.mock.calls[0].arguments[0], { connectionUri: { diff --git a/packages/cli/src/commands/generate/schema-from-database/generate_schema_command.ts b/packages/cli/src/commands/generate/schema-from-database/generate_schema_command.ts index 56c79d3cbd..a0896a7b30 100644 --- a/packages/cli/src/commands/generate/schema-from-database/generate_schema_command.ts +++ b/packages/cli/src/commands/generate/schema-from-database/generate_schema_command.ts @@ -2,9 +2,8 @@ import { ArgumentsCamelCase, Argv, CommandModule } from 'yargs'; import { BackendIdentifierResolver } from '../../../backend-identifier/backend_identifier_resolver.js'; import { ArgumentsKebabCase } from '../../../kebab_case.js'; import { SecretClient } from '@aws-amplify/backend-secret'; -import { BackendIdentifier } from '@aws-amplify/plugin-types'; import { SchemaGenerator } from '@aws-amplify/schema-generator'; -import { AmplifyFault } from '@aws-amplify/platform-core'; +import { AmplifyUserError } from '@aws-amplify/platform-core'; const DEFAULT_OUTPUT = 'amplify/data/schema.sql.ts'; @@ -54,13 +53,14 @@ export class GenerateSchemaCommand handler = async ( args: ArgumentsCamelCase ): Promise => { - const backendIdentifier = await this.backendIdentifierResolver.resolve( - args - ); + const backendIdentifier = + await this.backendIdentifierResolver.resolveBackendIdentifier(args); if (!backendIdentifier) { - throw new AmplifyFault('BackendIdentifierFault', { - message: 'Could not resolve the backend identifier', + throw new AmplifyUserError('BackendIdentifierResolverError', { + message: 'Could not resolve the backend identifier.', + resolution: + 'Ensure stack name or Amplify App ID and branch specified are correct and exists, then re-run this command.', }); } @@ -68,7 +68,7 @@ export class GenerateSchemaCommand const outputFile = args.out as string; const connectionUriSecret = await this.secretClient.getSecret( - backendIdentifier as BackendIdentifier, + backendIdentifier, { name: connectionUriSecretName, } @@ -77,12 +77,9 @@ export class GenerateSchemaCommand const sslCertSecretName = args.sslCertSecret as string; let sslCertSecret; if (sslCertSecretName) { - sslCertSecret = await this.secretClient.getSecret( - backendIdentifier as BackendIdentifier, - { - name: sslCertSecretName, - } - ); + sslCertSecret = await this.secretClient.getSecret(backendIdentifier, { + name: sslCertSecretName, + }); } await this.schemaGenerator.generate({ diff --git a/packages/cli/src/commands/sandbox/sandbox-delete/sandbox_delete_command.test.ts b/packages/cli/src/commands/sandbox/sandbox-delete/sandbox_delete_command.test.ts index 62e80f946c..c919cd6141 100644 --- a/packages/cli/src/commands/sandbox/sandbox-delete/sandbox_delete_command.test.ts +++ b/packages/cli/src/commands/sandbox/sandbox-delete/sandbox_delete_command.test.ts @@ -1,10 +1,5 @@ import { beforeEach, describe, it, mock } from 'node:test'; -import { - AmplifyPrompter, - PackageManagerControllerFactory, - format, - printer, -} from '@aws-amplify/cli-core'; +import { AmplifyPrompter, format, printer } from '@aws-amplify/cli-core'; import yargs, { CommandModule } from 'yargs'; import { TestCommandRunner } from '../../../test-utils/command_runner.js'; import assert from 'node:assert'; @@ -50,8 +45,7 @@ void describe('sandbox delete command', () => { sandboxFactory, [sandboxDeleteCommand, createSandboxSecretCommand()], clientConfigGeneratorAdapterMock, - commandMiddleware, - new PackageManagerControllerFactory().getPackageManagerController() + commandMiddleware ); const parser = yargs().command(sandboxCommand as unknown as CommandModule); commandRunner = new TestCommandRunner(parser); diff --git a/packages/cli/src/commands/sandbox/sandbox_command.test.ts b/packages/cli/src/commands/sandbox/sandbox_command.test.ts index 7b653d877c..a05bfa3168 100644 --- a/packages/cli/src/commands/sandbox/sandbox_command.test.ts +++ b/packages/cli/src/commands/sandbox/sandbox_command.test.ts @@ -8,7 +8,7 @@ import { TestCommandError, TestCommandRunner, } from '../../test-utils/command_runner.js'; -import { AmplifyPrompter, format, printer } from '@aws-amplify/cli-core'; +import { format, printer } from '@aws-amplify/cli-core'; import { EventHandler, SandboxCommand } from './sandbox_command.js'; import { createSandboxCommand } from './sandbox_command_factory.js'; import { SandboxDeleteCommand } from './sandbox-delete/sandbox_delete_command.js'; @@ -20,7 +20,6 @@ import { import { createSandboxSecretCommand } from './sandbox-secret/sandbox_secret_command_factory.js'; import { ClientConfigGeneratorAdapter } from '../../client-config/client_config_generator_adapter.js'; import { CommandMiddleware } from '../../command_middleware.js'; -import { PackageManagerController } from '@aws-amplify/plugin-types'; import { AmplifyError } from '@aws-amplify/platform-core'; mock.method(fsp, 'mkdir', () => Promise.resolve()); @@ -54,11 +53,6 @@ void describe('sandbox command', () => { ); const sandboxProfile = 'test-sandbox'; - const allowsSignalPropagationMock = mock.fn(() => true); - const packageManagerControllerMock = { - allowsSignalPropagation: allowsSignalPropagationMock, - } as unknown as PackageManagerController; - beforeEach(async () => { const sandboxFactory = new SandboxSingletonFactory( () => @@ -80,7 +74,6 @@ void describe('sandbox command', () => { [sandboxDeleteCommand, createSandboxSecretCommand()], clientConfigGeneratorAdapterMock, commandMiddleware, - packageManagerControllerMock, () => ({ successfulDeployment: [clientConfigGenerationMock], successfulDeletion: [clientConfigDeletionMock], @@ -128,7 +121,7 @@ void describe('sandbox command', () => { () => commandRunner.runCommand(`sandbox --identifier ${invalidIdentifier}`), // invalid identifier (err: TestCommandError) => { - assert.ok(err.error instanceof AmplifyError); + assert.ok(AmplifyError.isAmplifyError(err.error)); assert.strictEqual( err.error.message, 'Invalid --identifier provided: invalid@' @@ -189,118 +182,7 @@ void describe('sandbox command', () => { ); }); - void it('asks to delete the sandbox environment when users send ctrl-C and say yes to delete', async (contextual) => { - // Mock process and extract the sigint handler after calling the sandbox command - const processSignal = contextual.mock.method(process, 'on', () => { - /* no op */ - }); - const sandboxStartMock = contextual.mock.method( - sandbox, - 'start', - async () => Promise.resolve() - ); - - const sandboxDeleteMock = contextual.mock.method(sandbox, 'delete', () => - Promise.resolve() - ); - - // User said yes to delete - contextual.mock.method(AmplifyPrompter, 'yesOrNo', () => - Promise.resolve(true) - ); - - await commandRunner.runCommand('sandbox'); - - // Similar to the later 0ms timeout. Without this tests in github action are failing - // but working locally - await new Promise((resolve) => setTimeout(resolve, 0)); - const sigIntHandlerFn = processSignal.mock.calls[0].arguments[1]; - if (sigIntHandlerFn) sigIntHandlerFn(); - - // I can't find any open node:test or yargs issues that would explain why this is necessary - // but for some reason the mock call count does not update without this 0ms wait - await new Promise((resolve) => setTimeout(resolve, 0)); - assert.equal(sandboxStartMock.mock.callCount(), 1); - assert.equal(sandboxDeleteMock.mock.callCount(), 1); - }); - - void it('asks to delete the sandbox environment when users send ctrl-C and say yes to delete with profile', async (contextual) => { - // Mock process and extract the sigint handler after calling the sandbox command - const processSignal = contextual.mock.method(process, 'on', () => { - /* no op */ - }); - const sandboxStartMock = contextual.mock.method( - sandbox, - 'start', - async () => Promise.resolve() - ); - - const sandboxDeleteMock = contextual.mock.method(sandbox, 'delete', () => - Promise.resolve() - ); - - // User said yes to delete - contextual.mock.method(AmplifyPrompter, 'yesOrNo', () => - Promise.resolve(true) - ); - - const profile = 'test_profile'; - await commandRunner.runCommand(`sandbox --profile ${profile}`); - - // Similar to the later 0ms timeout. Without this tests in github action are failing - // but working locally - await new Promise((resolve) => setTimeout(resolve, 0)); - const sigIntHandlerFn = processSignal.mock.calls[0].arguments[1]; - if (sigIntHandlerFn) sigIntHandlerFn(); - - // I can't find any open node:test or yargs issues that would explain why this is necessary - // but for some reason the mock call count does not update without this 0ms wait - await new Promise((resolve) => setTimeout(resolve, 0)); - assert.equal(sandboxStartMock.mock.callCount(), 1); - assert.equal(sandboxDeleteMock.mock.callCount(), 1); - assert.deepStrictEqual(sandboxDeleteMock.mock.calls[0].arguments[0], { - identifier: undefined, - profile, - }); - }); - - void it('asks to delete the sandbox environment when users send ctrl-C and say no to delete', async (contextual) => { - // Mock process and extract the sigint handler after calling the sandbox command - const processSignal = contextual.mock.method(process, 'on', () => { - /* no op */ - }); - const sandboxStartMock = contextual.mock.method( - sandbox, - 'start', - async () => Promise.resolve() - ); - - const sandboxDeleteMock = contextual.mock.method( - sandbox, - 'delete', - async () => Promise.resolve() - ); - - // User said no to delete - contextual.mock.method(AmplifyPrompter, 'yesOrNo', () => - Promise.resolve(false) - ); - - await commandRunner.runCommand('sandbox'); - - // Similar to the previous test's 0ms timeout. Without this tests in github action are failing - // but working locally - await new Promise((resolve) => setTimeout(resolve, 0)); - const sigIntHandlerFn = processSignal.mock.calls[0].arguments[1]; - if (sigIntHandlerFn) sigIntHandlerFn(); - - assert.equal(sandboxStartMock.mock.callCount(), 1); - assert.equal(sandboxDeleteMock.mock.callCount(), 0); - }); - - void it('Does not prompt for deleting the sandbox if package manager does not allow signal propagation', async (contextual) => { - allowsSignalPropagationMock.mock.mockImplementationOnce(() => false); - + void it('Prints stopping sandbox and instructions to delete sandbox when users send ctrl+c', async (contextual) => { // Mock process and extract the sigint handler after calling the sandbox command const processSignal = contextual.mock.method(process, 'on', () => { /* no op */ @@ -371,7 +253,6 @@ void describe('sandbox command', () => { [], clientConfigGeneratorAdapterMock, commandMiddleware, - packageManagerControllerMock, undefined ); const parser = yargs().command(sandboxCommand as unknown as CommandModule); @@ -427,15 +308,15 @@ void describe('sandbox command', () => { ); }); - void it('sandbox creates an empty client config file if one does not already exist for version 1.1', async (contextual) => { + void it('sandbox creates an empty client config file if one does not already exist for version 1.3', async (contextual) => { contextual.mock.method(fs, 'existsSync', () => false); const writeFileMock = contextual.mock.method(fsp, 'writeFile', () => true); - await commandRunner.runCommand('sandbox --outputs-version 1.1'); + await commandRunner.runCommand('sandbox --outputs-version 1.3'); assert.equal(sandboxStartMock.mock.callCount(), 1); assert.equal(writeFileMock.mock.callCount(), 1); assert.deepStrictEqual( writeFileMock.mock.calls[0].arguments[1], - `{\n "version": "1.1"\n}` + `{\n "version": "1.3"\n}` ); assert.deepStrictEqual( writeFileMock.mock.calls[0].arguments[0], diff --git a/packages/cli/src/commands/sandbox/sandbox_command.ts b/packages/cli/src/commands/sandbox/sandbox_command.ts index 356d0a8eb6..37c069286b 100644 --- a/packages/cli/src/commands/sandbox/sandbox_command.ts +++ b/packages/cli/src/commands/sandbox/sandbox_command.ts @@ -1,7 +1,7 @@ import { ArgumentsCamelCase, Argv, CommandModule } from 'yargs'; import fs from 'fs'; import fsp from 'fs/promises'; -import { AmplifyPrompter, format, printer } from '@aws-amplify/cli-core'; +import { format, printer } from '@aws-amplify/cli-core'; import { SandboxFunctionStreamingOptions, SandboxSingletonFactory, @@ -20,7 +20,6 @@ import { ClientConfigGeneratorAdapter } from '../../client-config/client_config_ import { CommandMiddleware } from '../../command_middleware.js'; import { SandboxCommandGlobalOptions } from './option_types.js'; import { ArgumentsKebabCase } from '../../kebab_case.js'; -import { PackageManagerController } from '@aws-amplify/plugin-types'; import { AmplifyUserError } from '@aws-amplify/platform-core'; export type SandboxCommandOptionsKebabCase = ArgumentsKebabCase< @@ -81,7 +80,6 @@ export class SandboxCommand private readonly sandboxSubCommands: CommandModule[], private clientConfigGeneratorAdapter: ClientConfigGeneratorAdapter, private commandMiddleware: CommandMiddleware, - private readonly packageManagerController: PackageManagerController, private readonly sandboxEventHandlerCreator?: SandboxEventHandlerCreator ) { this.command = 'sandbox'; @@ -276,23 +274,11 @@ export class SandboxCommand }; sigIntHandler = async () => { - if (!this.packageManagerController.allowsSignalPropagation()) { - printer.print( - `Stopping the sandbox process. To delete the sandbox, run ${format.normalizeAmpxCommand( - 'sandbox delete' - )}` - ); - return; - } - const answer = await AmplifyPrompter.yesOrNo({ - message: - 'Would you like to delete all the resources in your sandbox environment (This cannot be undone)?', - defaultValue: false, - }); - if (answer) - await ( - await this.sandboxFactory.getInstance() - ).delete({ identifier: this.sandboxIdentifier, profile: this.profile }); + printer.print( + `Stopping the sandbox process. To delete the sandbox, run ${format.normalizeAmpxCommand( + 'sandbox delete' + )}` + ); }; private validateDirectory = async (option: string, dir: string) => { diff --git a/packages/cli/src/commands/sandbox/sandbox_command_factory.ts b/packages/cli/src/commands/sandbox/sandbox_command_factory.ts index 07afc979d0..ef5454a931 100644 --- a/packages/cli/src/commands/sandbox/sandbox_command_factory.ts +++ b/packages/cli/src/commands/sandbox/sandbox_command_factory.ts @@ -16,11 +16,7 @@ import { } from '@aws-amplify/platform-core'; import { SandboxEventHandlerFactory } from './sandbox_event_handler_factory.js'; import { CommandMiddleware } from '../../command_middleware.js'; -import { - PackageManagerControllerFactory, - format, - printer, -} from '@aws-amplify/cli-core'; +import { format, printer } from '@aws-amplify/cli-core'; import { S3Client } from '@aws-sdk/client-s3'; import { AmplifyClient } from '@aws-sdk/client-amplify'; import { CloudFormationClient } from '@aws-sdk/client-cloudformation'; @@ -70,7 +66,6 @@ export const createSandboxCommand = (): CommandModule< [new SandboxDeleteCommand(sandboxFactory), createSandboxSecretCommand()], clientConfigGeneratorAdapter, commandMiddleWare, - new PackageManagerControllerFactory().getPackageManagerController(), eventHandlerFactory.getSandboxEventHandlers ); }; diff --git a/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.test.ts b/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.test.ts index 2a3366612c..e977b00f51 100644 --- a/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.test.ts +++ b/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.test.ts @@ -23,7 +23,7 @@ void describe('sandbox_event_handler_factory', () => { } as unknown as ClientConfigGeneratorAdapter; const clientConfigLifecycleHandler = new ClientConfigLifecycleHandler( clientConfigGeneratorAdapterMock, - '1.1', + '1.3', 'test-out', ClientConfigFormat.JSON ); @@ -73,7 +73,7 @@ void describe('sandbox_event_handler_factory', () => { namespace: 'test', name: 'name', }, - '1.1', + '1.3', 'test-out', 'json', ]); @@ -185,7 +185,7 @@ void describe('sandbox_event_handler_factory', () => { namespace: 'test', name: 'name', }, - '1.1', + '1.3', 'test-out', 'json', ]); diff --git a/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.ts b/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.ts index 858008c76e..5d03b11c2c 100644 --- a/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.ts +++ b/packages/cli/src/commands/sandbox/sandbox_event_handler_factory.ts @@ -64,7 +64,7 @@ export class SandboxEventHandlerFactory { return; } const deployError = args[0]; - if (deployError && deployError instanceof AmplifyError) { + if (deployError && AmplifyError.isAmplifyError(deployError)) { await usageDataEmitter.emitFailure(deployError, { command: 'Sandbox', }); diff --git a/packages/cli/src/error_handler.ts b/packages/cli/src/error_handler.ts index 522f2cb9ec..806020e986 100644 --- a/packages/cli/src/error_handler.ts +++ b/packages/cli/src/error_handler.ts @@ -111,7 +111,7 @@ const handleError = async ({ printMessagePreamble?.(); - if (error instanceof AmplifyError) { + if (AmplifyError.isAmplifyError(error)) { printer.print(format.error(`${error.name}: ${error.message}`)); if (error.resolution) { @@ -141,7 +141,7 @@ const handleError = async ({ } await usageDataEmitter?.emitFailure( - error instanceof AmplifyError + AmplifyError.isAmplifyError(error) ? error : AmplifyError.fromError( error && error instanceof Error ? error : new Error(message) diff --git a/packages/cli/src/main_parser_factory.test.ts b/packages/cli/src/main_parser_factory.test.ts index 5a54ffa4a5..e957baa951 100644 --- a/packages/cli/src/main_parser_factory.test.ts +++ b/packages/cli/src/main_parser_factory.test.ts @@ -17,11 +17,22 @@ void describe('main parser', { concurrency: false }, () => { assert.match(output, /generate\s+Generates post deployment artifacts/); }); - void it('shows version', async () => { + void it('includes generate command in shorthand help output', async () => { + const output = await commandRunner.runCommand('-h'); + assert.match(output, /Commands:/); + assert.match(output, /generate\s+Generates post deployment artifacts/); + }); + + void it('shows version for long version option', async () => { const output = await commandRunner.runCommand('--version'); assert.equal(output, `${version}\n`); }); + void it('shows version for shorthand version option', async () => { + const output = await commandRunner.runCommand('-v'); + assert.equal(output, `${version}\n`); + }); + void it('prints help if command is not provided', async () => { await assert.rejects( () => commandRunner.runCommand(''), diff --git a/packages/cli/src/main_parser_factory.ts b/packages/cli/src/main_parser_factory.ts index 5189c7de3f..78fd56a550 100644 --- a/packages/cli/src/main_parser_factory.ts +++ b/packages/cli/src/main_parser_factory.ts @@ -29,6 +29,8 @@ export const createMainParser = (libraryVersion: string): Argv => { .command(createConfigureCommand()) .command(createInfoCommand()) .help() + .alias('h', 'help') + .alias('v', 'version') .demandCommand() .strictCommands() .recommendCommands() diff --git a/packages/client-config/API.md b/packages/client-config/API.md index 40b7882c3e..cccafacd46 100644 --- a/packages/client-config/API.md +++ b/packages/client-config/API.md @@ -16,6 +16,12 @@ type AmazonCognitoStandardAttributes = 'address' | 'birthdate' | 'email' | 'fami // @public type AmazonCognitoStandardAttributes_2 = 'address' | 'birthdate' | 'email' | 'family_name' | 'gender' | 'given_name' | 'locale' | 'middle_name' | 'name' | 'nickname' | 'phone_number' | 'picture' | 'preferred_username' | 'profile' | 'sub' | 'updated_at' | 'website' | 'zoneinfo'; +// @public +type AmazonCognitoStandardAttributes_3 = 'address' | 'birthdate' | 'email' | 'family_name' | 'gender' | 'given_name' | 'locale' | 'middle_name' | 'name' | 'nickname' | 'phone_number' | 'picture' | 'preferred_username' | 'profile' | 'sub' | 'updated_at' | 'website' | 'zoneinfo'; + +// @public +type AmazonCognitoStandardAttributes_4 = 'address' | 'birthdate' | 'email' | 'family_name' | 'gender' | 'given_name' | 'locale' | 'middle_name' | 'name' | 'nickname' | 'phone_number' | 'picture' | 'preferred_username' | 'profile' | 'sub' | 'updated_at' | 'website' | 'zoneinfo'; + // @public interface AmazonLocationServiceConfig { name?: string; @@ -28,12 +34,64 @@ interface AmazonLocationServiceConfig_2 { style?: string; } +// @public +interface AmazonLocationServiceConfig_3 { + name?: string; + style?: string; +} + +// @public +interface AmazonLocationServiceConfig_4 { + name?: string; + style?: string; +} + // @public type AmazonPinpointChannels = 'IN_APP_MESSAGING' | 'FCM' | 'APNS' | 'EMAIL' | 'SMS'; // @public type AmazonPinpointChannels_2 = 'IN_APP_MESSAGING' | 'FCM' | 'APNS' | 'EMAIL' | 'SMS'; +// @public +type AmazonPinpointChannels_3 = 'IN_APP_MESSAGING' | 'FCM' | 'APNS' | 'EMAIL' | 'SMS'; + +// @public +type AmazonPinpointChannels_4 = 'IN_APP_MESSAGING' | 'FCM' | 'APNS' | 'EMAIL' | 'SMS'; + +// @public (undocumented) +type AmplifyStorageAccessActions = 'read' | 'get' | 'list' | 'write' | 'delete'; + +// @public (undocumented) +type AmplifyStorageAccessActions_2 = 'read' | 'get' | 'list' | 'write' | 'delete'; + +// @public +interface AmplifyStorageAccessRule { + // (undocumented) + authenticated?: AmplifyStorageAccessActions[]; + // (undocumented) + entity?: AmplifyStorageAccessActions[]; + // (undocumented) + groups?: AmplifyStorageAccessActions[]; + // (undocumented) + guest?: AmplifyStorageAccessActions[]; + // (undocumented) + resource?: AmplifyStorageAccessActions[]; +} + +// @public +interface AmplifyStorageAccessRule_2 { + // (undocumented) + authenticated?: AmplifyStorageAccessActions_2[]; + // (undocumented) + entity?: AmplifyStorageAccessActions_2[]; + // (undocumented) + groups?: AmplifyStorageAccessActions_2[]; + // (undocumented) + guest?: AmplifyStorageAccessActions_2[]; + // (undocumented) + resource?: AmplifyStorageAccessActions_2[]; +} + // @public (undocumented) interface AmplifyStorageBucket { // (undocumented) @@ -42,6 +100,40 @@ interface AmplifyStorageBucket { bucket_name: string; // (undocumented) name: string; + // (undocumented) + paths?: { + [k: string]: AmplifyStorageAccessRule; + }; +} + +// @public (undocumented) +interface AmplifyStorageBucket_2 { + // (undocumented) + aws_region: string; + // (undocumented) + bucket_name: string; + // (undocumented) + name: string; + // (undocumented) + paths?: { + [k: string]: AmplifyStorageAccessRule_2; + }; +} + +// @public (undocumented) +interface AmplifyStorageBucket_3 { + // (undocumented) + aws_region: string; + // (undocumented) + bucket_name: string; + // (undocumented) + name: string; +} + +// @public +interface AmplifyUserGroupConfig { + // (undocumented) + precedence?: number; } // @public (undocumented) @@ -88,12 +180,12 @@ export type AuthClientConfig = { interface AWSAmplifyBackendOutputs { analytics?: { amazon_pinpoint?: { - aws_region: AwsRegion; + aws_region: string; app_id: string; }; }; auth?: { - aws_region: AwsRegion; + aws_region: string; user_pool_id: string; user_pool_client_id: string; identity_pool_id?: string; @@ -118,6 +210,9 @@ interface AWSAmplifyBackendOutputs { unauthenticated_identities_enabled?: boolean; mfa_configuration?: 'NONE' | 'OPTIONAL' | 'REQUIRED'; mfa_methods?: ('SMS' | 'TOTP')[]; + groups?: { + [k: string]: AmplifyUserGroupConfig; + }[]; }; custom?: { [k: string]: unknown; @@ -133,7 +228,7 @@ interface AWSAmplifyBackendOutputs { authorization_types: AwsAppsyncAuthorizationType[]; }; geo?: { - aws_region: AwsRegion; + aws_region: string; maps?: { items: { [k: string]: AmazonLocationServiceConfig; @@ -159,19 +254,19 @@ interface AWSAmplifyBackendOutputs { bucket_name: string; buckets?: AmplifyStorageBucket[]; }; - version: '1.1'; + version: '1.3'; } // @public interface AWSAmplifyBackendOutputs_2 { analytics?: { amazon_pinpoint?: { - aws_region: AwsRegion_2; + aws_region: string; app_id: string; }; }; auth?: { - aws_region: AwsRegion_2; + aws_region: string; user_pool_id: string; user_pool_client_id: string; identity_pool_id?: string; @@ -211,7 +306,7 @@ interface AWSAmplifyBackendOutputs_2 { authorization_types: AwsAppsyncAuthorizationType_2[]; }; geo?: { - aws_region: AwsRegion_2; + aws_region: string; maps?: { items: { [k: string]: AmazonLocationServiceConfig_2; @@ -235,6 +330,162 @@ interface AWSAmplifyBackendOutputs_2 { storage?: { aws_region: AwsRegion_2; bucket_name: string; + buckets?: AmplifyStorageBucket_2[]; + }; + version: '1.2'; +} + +// @public +interface AWSAmplifyBackendOutputs_3 { + analytics?: { + amazon_pinpoint?: { + aws_region: AwsRegion_3; + app_id: string; + }; + }; + auth?: { + aws_region: AwsRegion_3; + user_pool_id: string; + user_pool_client_id: string; + identity_pool_id?: string; + password_policy?: { + min_length: number; + require_numbers: boolean; + require_lowercase: boolean; + require_uppercase: boolean; + require_symbols: boolean; + }; + oauth?: { + identity_providers: ('GOOGLE' | 'FACEBOOK' | 'LOGIN_WITH_AMAZON' | 'SIGN_IN_WITH_APPLE')[]; + domain: string; + scopes: string[]; + redirect_sign_in_uri: string[]; + redirect_sign_out_uri: string[]; + response_type: 'code' | 'token'; + }; + standard_required_attributes?: AmazonCognitoStandardAttributes_3[]; + username_attributes?: ('email' | 'phone_number' | 'username')[]; + user_verification_types?: ('email' | 'phone_number')[]; + unauthenticated_identities_enabled?: boolean; + mfa_configuration?: 'NONE' | 'OPTIONAL' | 'REQUIRED'; + mfa_methods?: ('SMS' | 'TOTP')[]; + }; + custom?: { + [k: string]: unknown; + }; + data?: { + aws_region: AwsRegion_3; + url: string; + model_introspection?: { + [k: string]: unknown; + }; + api_key?: string; + default_authorization_type: AwsAppsyncAuthorizationType_3; + authorization_types: AwsAppsyncAuthorizationType_3[]; + }; + geo?: { + aws_region: AwsRegion_3; + maps?: { + items: { + [k: string]: AmazonLocationServiceConfig_3; + }; + default: string; + }; + search_indices?: { + items: string[]; + default: string; + }; + geofence_collections?: { + items: string[]; + default: string; + }; + }; + notifications?: { + aws_region: AwsRegion_3; + amazon_pinpoint_app_id: string; + channels: AmazonPinpointChannels_3[]; + }; + storage?: { + aws_region: AwsRegion_3; + bucket_name: string; + buckets?: AmplifyStorageBucket_3[]; + }; + version: '1.1'; +} + +// @public +interface AWSAmplifyBackendOutputs_4 { + analytics?: { + amazon_pinpoint?: { + aws_region: AwsRegion_4; + app_id: string; + }; + }; + auth?: { + aws_region: AwsRegion_4; + user_pool_id: string; + user_pool_client_id: string; + identity_pool_id?: string; + password_policy?: { + min_length: number; + require_numbers: boolean; + require_lowercase: boolean; + require_uppercase: boolean; + require_symbols: boolean; + }; + oauth?: { + identity_providers: ('GOOGLE' | 'FACEBOOK' | 'LOGIN_WITH_AMAZON' | 'SIGN_IN_WITH_APPLE')[]; + domain: string; + scopes: string[]; + redirect_sign_in_uri: string[]; + redirect_sign_out_uri: string[]; + response_type: 'code' | 'token'; + }; + standard_required_attributes?: AmazonCognitoStandardAttributes_4[]; + username_attributes?: ('email' | 'phone_number' | 'username')[]; + user_verification_types?: ('email' | 'phone_number')[]; + unauthenticated_identities_enabled?: boolean; + mfa_configuration?: 'NONE' | 'OPTIONAL' | 'REQUIRED'; + mfa_methods?: ('SMS' | 'TOTP')[]; + }; + custom?: { + [k: string]: unknown; + }; + data?: { + aws_region: AwsRegion_4; + url: string; + model_introspection?: { + [k: string]: unknown; + }; + api_key?: string; + default_authorization_type: AwsAppsyncAuthorizationType_4; + authorization_types: AwsAppsyncAuthorizationType_4[]; + }; + geo?: { + aws_region: AwsRegion_4; + maps?: { + items: { + [k: string]: AmazonLocationServiceConfig_4; + }; + default: string; + }; + search_indices?: { + items: string[]; + default: string; + }; + geofence_collections?: { + items: string[]; + default: string; + }; + }; + notifications?: { + aws_region: AwsRegion_4; + amazon_pinpoint_app_id: string; + channels: AmazonPinpointChannels_4[]; + }; + storage?: { + aws_region: AwsRegion_4; + bucket_name: string; }; version: '1'; } @@ -245,14 +496,26 @@ type AwsAppsyncAuthorizationType = 'AMAZON_COGNITO_USER_POOLS' | 'API_KEY' | 'AW // @public type AwsAppsyncAuthorizationType_2 = 'AMAZON_COGNITO_USER_POOLS' | 'API_KEY' | 'AWS_IAM' | 'AWS_LAMBDA' | 'OPENID_CONNECT'; +// @public +type AwsAppsyncAuthorizationType_3 = 'AMAZON_COGNITO_USER_POOLS' | 'API_KEY' | 'AWS_IAM' | 'AWS_LAMBDA' | 'OPENID_CONNECT'; + +// @public +type AwsAppsyncAuthorizationType_4 = 'AMAZON_COGNITO_USER_POOLS' | 'API_KEY' | 'AWS_IAM' | 'AWS_LAMBDA' | 'OPENID_CONNECT'; + // @public (undocumented) type AwsRegion = string; // @public (undocumented) type AwsRegion_2 = string; +// @public (undocumented) +type AwsRegion_3 = string; + +// @public (undocumented) +type AwsRegion_4 = string; + // @public -export type ClientConfig = clientConfigTypesV1_1.AWSAmplifyBackendOutputs | clientConfigTypesV1.AWSAmplifyBackendOutputs; +export type ClientConfig = clientConfigTypesV1_3.AWSAmplifyBackendOutputs | clientConfigTypesV1_2.AWSAmplifyBackendOutputs | clientConfigTypesV1_1.AWSAmplifyBackendOutputs | clientConfigTypesV1.AWSAmplifyBackendOutputs; // @public (undocumented) export enum ClientConfigFileBaseName { @@ -280,29 +543,60 @@ export enum ClientConfigFormat { export type ClientConfigLegacy = Partial; declare namespace clientConfigTypesV1 { + export { + AmazonCognitoStandardAttributes_4 as AmazonCognitoStandardAttributes, + AwsRegion_4 as AwsRegion, + AwsAppsyncAuthorizationType_4 as AwsAppsyncAuthorizationType, + AmazonPinpointChannels_4 as AmazonPinpointChannels, + AWSAmplifyBackendOutputs_4 as AWSAmplifyBackendOutputs, + AmazonLocationServiceConfig_4 as AmazonLocationServiceConfig + } +} +export { clientConfigTypesV1 } + +declare namespace clientConfigTypesV1_1 { + export { + AmazonCognitoStandardAttributes_3 as AmazonCognitoStandardAttributes, + AwsRegion_3 as AwsRegion, + AwsAppsyncAuthorizationType_3 as AwsAppsyncAuthorizationType, + AmazonPinpointChannels_3 as AmazonPinpointChannels, + AWSAmplifyBackendOutputs_3 as AWSAmplifyBackendOutputs, + AmazonLocationServiceConfig_3 as AmazonLocationServiceConfig, + AmplifyStorageBucket_3 as AmplifyStorageBucket + } +} +export { clientConfigTypesV1_1 } + +declare namespace clientConfigTypesV1_2 { export { AmazonCognitoStandardAttributes_2 as AmazonCognitoStandardAttributes, AwsRegion_2 as AwsRegion, AwsAppsyncAuthorizationType_2 as AwsAppsyncAuthorizationType, AmazonPinpointChannels_2 as AmazonPinpointChannels, + AmplifyStorageAccessActions_2 as AmplifyStorageAccessActions, AWSAmplifyBackendOutputs_2 as AWSAmplifyBackendOutputs, - AmazonLocationServiceConfig_2 as AmazonLocationServiceConfig + AmazonLocationServiceConfig_2 as AmazonLocationServiceConfig, + AmplifyStorageBucket_2 as AmplifyStorageBucket, + AmplifyStorageAccessRule_2 as AmplifyStorageAccessRule } } -export { clientConfigTypesV1 } +export { clientConfigTypesV1_2 } -declare namespace clientConfigTypesV1_1 { +declare namespace clientConfigTypesV1_3 { export { AmazonCognitoStandardAttributes, AwsRegion, AwsAppsyncAuthorizationType, AmazonPinpointChannels, + AmplifyStorageAccessActions, AWSAmplifyBackendOutputs, + AmplifyUserGroupConfig, AmazonLocationServiceConfig, - AmplifyStorageBucket + AmplifyStorageBucket, + AmplifyStorageAccessRule } } -export { clientConfigTypesV1_1 } +export { clientConfigTypesV1_3 } // @public (undocumented) export type ClientConfigVersion = `${ClientConfigVersionOption}`; @@ -314,11 +608,15 @@ export enum ClientConfigVersionOption { // (undocumented) V1 = "1", // (undocumented) - V1_1 = "1.1" + V1_1 = "1.1", + // (undocumented) + V1_2 = "1.2", + // (undocumented) + V1_3 = "1.3" } // @public -export type ClientConfigVersionTemplateType = T extends '1.1' ? clientConfigTypesV1_1.AWSAmplifyBackendOutputs : T extends '1' ? clientConfigTypesV1.AWSAmplifyBackendOutputs : never; +export type ClientConfigVersionTemplateType = T extends '1.3' ? clientConfigTypesV1_3.AWSAmplifyBackendOutputs : T extends '1.2' ? clientConfigTypesV1_2.AWSAmplifyBackendOutputs : T extends '1.1' ? clientConfigTypesV1_1.AWSAmplifyBackendOutputs : T extends '1' ? clientConfigTypesV1.AWSAmplifyBackendOutputs : never; // @public (undocumented) export type CustomClientConfig = { @@ -329,7 +627,7 @@ export type CustomClientConfig = { export const DEFAULT_CLIENT_CONFIG_VERSION: ClientConfigVersion; // @public -export const generateClientConfig: (backendIdentifier: DeployedBackendIdentifier, version: T, awsClientProvider?: AWSClientProvider<{ +export const generateClientConfig: (backendIdentifier: DeployedBackendIdentifier, version: T, awsClientProvider?: AWSClientProvider<{ getS3Client: S3Client; getAmplifyClient: AmplifyClient; getCloudFormationClient: CloudFormationClient; diff --git a/packages/client-config/CHANGELOG.md b/packages/client-config/CHANGELOG.md index 9e35ebdc83..558d349fdb 100644 --- a/packages/client-config/CHANGELOG.md +++ b/packages/client-config/CHANGELOG.md @@ -1,5 +1,61 @@ # @aws-amplify/client-config +## 1.5.2 + +### Patch Changes + +- d0d8d4e: Fix a bug where $ sign in dart outputs would fail compilation + +## 1.5.1 + +### Patch Changes + +- b56d344: update aws-cdk lib to ^2.158.0 +- Updated dependencies [b56d344] + - @aws-amplify/plugin-types@1.3.1 + +## 1.5.0 + +### Minor Changes + +- 5f46d8d: add user groups to outputs + +### Patch Changes + +- Updated dependencies [5f46d8d] + - @aws-amplify/backend-output-schemas@1.4.0 + +## 1.4.0 + +### Minor Changes + +- d538ecc: add storage access rules to outputs + +### Patch Changes + +- Updated dependencies [d538ecc] + - @aws-amplify/backend-output-schemas@1.2.1 + +## 1.3.2 + +### Patch Changes + +- 603b75d: Classify pointing client config generator at metadata-less stack as user error + +## 1.3.1 + +### Patch Changes + +- e648e8e: added main field to package.json so these packages are resolvable +- 8dd7286: fixed errors in plugin-types and cli-core along with any extraneous dependencies in other packages +- e648e8e: added main field to packages known to lack one +- Updated dependencies [e648e8e] +- Updated dependencies [8dd7286] +- Updated dependencies [e648e8e] + - @aws-amplify/deployed-backend-client@1.4.1 + - @aws-amplify/model-generator@1.0.7 + - @aws-amplify/plugin-types@1.2.2 + ## 1.3.0 ### Minor Changes diff --git a/packages/client-config/package.json b/packages/client-config/package.json index 74affd69de..9310fa1617 100644 --- a/packages/client-config/package.json +++ b/packages/client-config/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/client-config", - "version": "1.3.0", + "version": "1.5.2", "type": "module", "publishConfig": { "access": "public" @@ -24,11 +24,11 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/backend-output-schemas": "^1.2.0", - "@aws-amplify/deployed-backend-client": "^1.4.0", - "@aws-amplify/model-generator": "^1.0.5", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/deployed-backend-client": "^1.4.1", + "@aws-amplify/model-generator": "^1.0.7", "@aws-amplify/platform-core": "^1.0.7", - "@aws-amplify/plugin-types": "^1.2.1", + "@aws-amplify/plugin-types": "^1.3.1", "zod": "^3.22.2" }, "devDependencies": { diff --git a/packages/client-config/src/client-config-contributor/client_config_contributor_factory.ts b/packages/client-config/src/client-config-contributor/client_config_contributor_factory.ts index 0c269c8285..50d07f4d0a 100644 --- a/packages/client-config/src/client-config-contributor/client_config_contributor_factory.ts +++ b/packages/client-config/src/client-config-contributor/client_config_contributor_factory.ts @@ -1,12 +1,16 @@ // Versions of config schemas supported by this package version import { - AuthClientConfigContributor as Auth1_1, + AuthClientConfigContributorV1_1 as Auth1_1, + AuthClientConfigContributor as Auth1_3, CustomClientConfigContributor as Custom1_1, DataClientConfigContributor as Data1_1, StorageClientConfigContributorV1 as Storage1, - StorageClientConfigContributor as Storage1_1, - VersionContributor as VersionContributor1_1, + StorageClientConfigContributorV1_1 as Storage1_1, + StorageClientConfigContributor as Storage1_2, + VersionContributor as VersionContributor1_3, VersionContributorV1, + VersionContributorV1_1, + VersionContributorV1_2, } from './client_config_contributor_v1.js'; import { ClientConfigContributor } from '../client-config-types/client_config_contributor.js'; @@ -31,11 +35,27 @@ export class ClientConfigContributorFactory { private readonly modelIntrospectionSchemaAdapter: ModelIntrospectionSchemaAdapter ) { this.versionedClientConfigContributors = { + [ClientConfigVersionOption.V1_3]: [ + new Auth1_3(), + new Data1_1(this.modelIntrospectionSchemaAdapter), + new Storage1_2(), + new VersionContributor1_3(), + new Custom1_1(), + ], + + [ClientConfigVersionOption.V1_2]: [ + new Auth1_1(), + new Data1_1(this.modelIntrospectionSchemaAdapter), + new Storage1_2(), + new VersionContributorV1_2(), + new Custom1_1(), + ], + [ClientConfigVersionOption.V1_1]: [ new Auth1_1(), new Data1_1(this.modelIntrospectionSchemaAdapter), new Storage1_1(), - new VersionContributor1_1(), + new VersionContributorV1_1(), new Custom1_1(), ], @@ -48,12 +68,12 @@ export class ClientConfigContributorFactory { new Custom1_1(), ], - // Legacy config is derived from V1.1 (latest) of unified default config + // Legacy config is derived from V1.3 (latest) of unified default config [ClientConfigVersionOption.V0]: [ new Auth1_1(), new Data1_1(this.modelIntrospectionSchemaAdapter), - new Storage1_1(), - new VersionContributor1_1(), + new Storage1_2(), + new VersionContributor1_3(), new Custom1_1(), ], }; diff --git a/packages/client-config/src/client-config-contributor/client_config_contributor_v1.test.ts b/packages/client-config/src/client-config-contributor/client_config_contributor_v1.test.ts index dfab479b0e..8a00d4ebb4 100644 --- a/packages/client-config/src/client-config-contributor/client_config_contributor_v1.test.ts +++ b/packages/client-config/src/client-config-contributor/client_config_contributor_v1.test.ts @@ -8,7 +8,7 @@ import { } from './client_config_contributor_v1.js'; import { ClientConfig, - clientConfigTypesV1_1, + clientConfigTypesV1_3, } from '../client-config-types/client_config.js'; import assert from 'node:assert'; import { @@ -74,7 +74,7 @@ void describe('auth client config contributor v1', () => { identity_pool_id: 'testIdentityPoolId', unauthenticated_identities_enabled: true, }, - } as Partial + } as Partial ); }); @@ -99,7 +99,7 @@ void describe('auth client config contributor v1', () => { aws_region: 'testRegion', identity_pool_id: 'testIdentityPoolId', }, - } as Partial + } as Partial ); }); @@ -133,7 +133,7 @@ void describe('auth client config contributor v1', () => { require_uppercase: true, }, }, - } as Partial + } as Partial ); }); @@ -166,11 +166,23 @@ void describe('auth client config contributor v1', () => { require_uppercase: false, }, }, - } as Partial + } as Partial ); }); void it('returns translated config when output has auth with zero-config attributes', () => { + const groups = [ + { + ADMINS: { + precedence: 0, + }, + }, + { + EDITORS: { + precedence: 1, + }, + }, + ]; const contributor = new AuthClientConfigContributor(); assert.deepStrictEqual( contributor.contribute({ @@ -197,6 +209,7 @@ void describe('auth client config contributor v1', () => { oauthRedirectSignOut: 'http://logout.com,http://logout2.com', oauthResponseType: 'code', socialProviders: `["GOOGLE","FACEBOOK","SIGN_IN_WITH_APPLE","LOGIN_WITH_AMAZON","GITHUB","DISCORD"]`, + groups: JSON.stringify(groups), }, }, }), @@ -235,12 +248,36 @@ void describe('auth client config contributor v1', () => { redirect_sign_out_uri: ['http://logout.com', 'http://logout2.com'], response_type: 'code', }, + groups: [ + { + ADMINS: { + precedence: 0, + }, + }, + { + EDITORS: { + precedence: 1, + }, + }, + ], }, - } as Partial + } as Partial ); }); void it('returns translated config when output has oauth settings but no social providers', () => { + const groups = [ + { + ADMINS: { + precedence: 0, + }, + }, + { + EDITORS: { + precedence: 1, + }, + }, + ]; const contributor = new AuthClientConfigContributor(); assert.deepStrictEqual( contributor.contribute({ @@ -266,6 +303,7 @@ void describe('auth client config contributor v1', () => { oauthRedirectSignIn: 'http://callback.com,http://callback2.com', oauthRedirectSignOut: 'http://logout.com,http://logout2.com', oauthResponseType: 'code', + groups: JSON.stringify(groups), }, }, }), @@ -299,12 +337,36 @@ void describe('auth client config contributor v1', () => { redirect_sign_out_uri: ['http://logout.com', 'http://logout2.com'], response_type: 'code', }, + groups: [ + { + ADMINS: { + precedence: 0, + }, + }, + { + EDITORS: { + precedence: 1, + }, + }, + ], }, - } as Partial + } as Partial ); }); void describe('auth outputs with mfa', () => { + const groups = [ + { + ADMINS: { + precedence: 0, + }, + }, + { + EDITORS: { + precedence: 1, + }, + }, + ]; const contribution = { version: '1' as const, payload: { @@ -327,6 +389,7 @@ void describe('auth client config contributor v1', () => { oauthRedirectSignIn: 'http://callback.com,http://callback2.com', oauthRedirectSignOut: 'http://logout.com,http://logout2.com', oauthResponseType: 'code', + groups: JSON.stringify(groups), }, }; @@ -357,8 +420,20 @@ void describe('auth client config contributor v1', () => { redirect_sign_out_uri: ['http://logout.com', 'http://logout2.com'], response_type: 'code', }, + groups: [ + { + ADMINS: { + precedence: 0, + }, + }, + { + EDITORS: { + precedence: 1, + }, + }, + ], }, - } as Pick; + } as Pick; void it('returns translated config when mfa is disabled', () => { const contributor = new AuthClientConfigContributor(); @@ -459,7 +534,7 @@ void describe('data client config contributor v1', () => { url: 'testApiEndpoint', aws_region: 'us-east-1', }, - } as Partial); + } as Partial); }); void it('returns translated config with model introspection when resolvable', async () => { @@ -507,7 +582,7 @@ void describe('data client config contributor v1', () => { enums: {}, }, }, - } as Partial); + } as Partial); }); }); @@ -540,6 +615,12 @@ void describe('storage client config contributor v1', () => { name: 'testName', bucketName: 'testBucketName', storageRegion: 'testRegion', + paths: { + 'path/*': { + guest: ['get', 'list'], + authenticated: ['read', 'write', 'delete'], + }, + }, }), ]); assert.deepStrictEqual( @@ -562,6 +643,12 @@ void describe('storage client config contributor v1', () => { name: 'testName', bucket_name: 'testBucketName', aws_region: 'testRegion', + paths: { + 'path/*': { + guest: ['get', 'list'], + authenticated: ['read', 'write', 'delete'], + }, + }, }, ], }, @@ -613,6 +700,6 @@ void describe('Custom client config contributor v1', () => { void describe('Custom client config contributor v1', () => { void it('contributes the version correctly', () => { - assert.deepEqual(new VersionContributor().contribute(), { version: '1.1' }); + assert.deepEqual(new VersionContributor().contribute(), { version: '1.3' }); }); }); diff --git a/packages/client-config/src/client-config-contributor/client_config_contributor_v1.ts b/packages/client-config/src/client-config-contributor/client_config_contributor_v1.ts index 30ce2a0649..4bd0879e0b 100644 --- a/packages/client-config/src/client-config-contributor/client_config_contributor_v1.ts +++ b/packages/client-config/src/client-config-contributor/client_config_contributor_v1.ts @@ -9,18 +9,48 @@ import { import { ClientConfig, ClientConfigVersionOption, + clientConfigTypesV1, clientConfigTypesV1_1, + clientConfigTypesV1_2, + clientConfigTypesV1_3, } from '../client-config-types/client_config.js'; import { ModelIntrospectionSchemaAdapter } from '../model_introspection_schema_adapter.js'; import { AwsAppsyncAuthorizationType } from '../client-config-schema/client_config_v1.1.js'; +import { AmplifyStorageAccessRule } from '../client-config-schema/client_config_v1.2.js'; // All categories client config contributors are included here to mildly enforce them using // the same schema (version and other types) /** - * Translator for the version number of ClientConfig of V1.1 + * Translator for the version number of ClientConfig of V1.3 */ export class VersionContributor implements ClientConfigContributor { + /** + * Return the version of the schema types that this contributor uses + */ + contribute = (): ClientConfig => { + return { version: ClientConfigVersionOption.V1_3 }; + }; +} + +/** + * Translator for the version number of ClientConfig of V1.2 + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export class VersionContributorV1_2 implements ClientConfigContributor { + /** + * Return the version of the schema types that this contributor uses + */ + contribute = (): ClientConfig => { + return { version: ClientConfigVersionOption.V1_2 }; + }; +} + +/** + * Translator for the version number of ClientConfig of V1.1 + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export class VersionContributorV1_1 implements ClientConfigContributor { /** * Return the version of the schema types that this contributor uses */ @@ -42,7 +72,7 @@ export class VersionContributorV1 implements ClientConfigContributor { } /** - * Translator for the Auth portion of ClientConfig + * Translator for the Auth portion of ClientConfig in V1.3 */ export class AuthClientConfigContributor implements ClientConfigContributor { /** @@ -66,7 +96,179 @@ export class AuthClientConfigContributor implements ClientConfigContributor { obj[key] = JSON.parse(value); }; - const authClientConfig: Partial = + const authClientConfig: Partial = + {}; + + authClientConfig.auth = { + user_pool_id: authOutput.payload.userPoolId, + aws_region: authOutput.payload.authRegion, + user_pool_client_id: authOutput.payload.webClientId, + }; + + if (authOutput.payload.identityPoolId) { + authClientConfig.auth.identity_pool_id = + authOutput.payload.identityPoolId; + } + + parseAndAssignObject( + authClientConfig.auth, + 'mfa_methods', + authOutput.payload.mfaTypes + ); + + parseAndAssignObject( + authClientConfig.auth, + 'standard_required_attributes', + authOutput.payload.signupAttributes + ); + + parseAndAssignObject( + authClientConfig.auth, + 'username_attributes', + authOutput.payload.usernameAttributes + ); + + parseAndAssignObject( + authClientConfig.auth, + 'user_verification_types', + authOutput.payload.verificationMechanisms + ); + + parseAndAssignObject( + authClientConfig.auth, + 'groups', + authOutput.payload.groups + ); + + if (authOutput.payload.mfaConfiguration) { + switch (authOutput.payload.mfaConfiguration) { + case 'OFF': { + authClientConfig.auth.mfa_configuration = 'NONE'; + break; + } + case 'OPTIONAL': { + authClientConfig.auth.mfa_configuration = 'OPTIONAL'; + break; + } + case 'ON': { + authClientConfig.auth.mfa_configuration = 'REQUIRED'; + } + } + } + + if ( + authOutput.payload.passwordPolicyMinLength || + authOutput.payload.passwordPolicyRequirements + ) { + authClientConfig.auth.password_policy = { + min_length: 8, // This is the default that is matching what construct defines. + // Values below are set to false instead of being undefined as libraries expect defined values. + // They are overridden below with construct outputs (default or not) if applicable. + require_lowercase: false, + require_numbers: false, + require_symbols: false, + require_uppercase: false, + }; + if (authOutput.payload.passwordPolicyMinLength) { + authClientConfig.auth.password_policy.min_length = Number.parseInt( + authOutput.payload.passwordPolicyMinLength + ); + } + if (authOutput.payload.passwordPolicyRequirements) { + const requirements = JSON.parse( + authOutput.payload.passwordPolicyRequirements + ) as string[]; + for (const requirement of requirements) { + switch (requirement) { + case 'REQUIRES_NUMBERS': + authClientConfig.auth.password_policy.require_numbers = true; + break; + case 'REQUIRES_LOWERCASE': + authClientConfig.auth.password_policy.require_lowercase = true; + break; + case 'REQUIRES_UPPERCASE': + authClientConfig.auth.password_policy.require_uppercase = true; + break; + case 'REQUIRES_SYMBOLS': + authClientConfig.auth.password_policy.require_symbols = true; + break; + } + } + } + } + + // OAuth settings are present if both oauthRedirectSignIn and oauthRedirectSignOut are. + if ( + authOutput.payload.oauthRedirectSignIn && + authOutput.payload.oauthRedirectSignOut + ) { + let socialProviders = authOutput.payload.socialProviders + ? JSON.parse(authOutput.payload.socialProviders) + : []; + if (Array.isArray(socialProviders)) { + socialProviders = socialProviders.filter(this.isValidIdentityProvider); + } + authClientConfig.auth.oauth = { + identity_providers: socialProviders, + redirect_sign_in_uri: authOutput.payload.oauthRedirectSignIn.split(','), + redirect_sign_out_uri: + authOutput.payload.oauthRedirectSignOut.split(','), + response_type: authOutput.payload.oauthResponseType as 'code' | 'token', + scopes: authOutput.payload.oauthScope + ? JSON.parse(authOutput.payload.oauthScope) + : [], + domain: authOutput.payload.oauthCognitoDomain ?? '', + }; + } + + if (authOutput.payload.allowUnauthenticatedIdentities) { + authClientConfig.auth.unauthenticated_identities_enabled = + authOutput.payload.allowUnauthenticatedIdentities === 'true'; + } + + return authClientConfig; + }; + + // Define a type guard function to check if a value is a valid IdentityProvider + isValidIdentityProvider = (identityProvider: string): boolean => { + return [ + 'GOOGLE', + 'FACEBOOK', + 'LOGIN_WITH_AMAZON', + 'SIGN_IN_WITH_APPLE', + ].includes(identityProvider); + }; +} + +/** + * Translator for the Auth portion of ClientConfig in V1.2 + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export class AuthClientConfigContributorV1_1 + implements ClientConfigContributor +{ + /** + * Given some BackendOutput, contribute the Auth portion of the ClientConfig + */ + contribute = ({ + [authOutputKey]: authOutput, + }: UnifiedBackendOutput): Partial | Record => { + if (authOutput === undefined) { + return {}; + } + + const parseAndAssignObject = ( + obj: T, + key: keyof T, + value: string | undefined + ) => { + if (value == null) { + return; + } + obj[key] = JSON.parse(value); + }; + + const authClientConfig: Partial = {}; authClientConfig.auth = { @@ -257,9 +459,59 @@ export class DataClientConfigContributor implements ClientConfigContributor { } /** - * Translator for the Storage portion of ClientConfig in V1.1 + * Translator for the Storage portion of ClientConfig in V1.2 */ +// eslint-disable-next-line @typescript-eslint/naming-convention export class StorageClientConfigContributor implements ClientConfigContributor { + /** + * Given some BackendOutput, contribute the Storage portion of the client config + */ + contribute = ({ + [storageOutputKey]: storageOutput, + }: UnifiedBackendOutput): Partial | Record => { + if (storageOutput === undefined) { + return {}; + } + const config: Partial = {}; + const bucketsStringArray = JSON.parse( + storageOutput.payload.buckets ?? '[]' + ); + config.storage = { + aws_region: storageOutput.payload.storageRegion, + bucket_name: storageOutput.payload.bucketName, + buckets: bucketsStringArray + .map((b: string) => JSON.parse(b)) + .map( + ({ + name, + bucketName, + storageRegion, + paths, + }: { + name: string; + bucketName: string; + storageRegion: string; + paths: Record; + }) => ({ + name, + bucket_name: bucketName, + aws_region: storageRegion, + paths, + }) + ), + }; + + return config; + }; +} + +/** + * Translator for the Storage portion of ClientConfig in V1.1 + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export class StorageClientConfigContributorV1_1 + implements ClientConfigContributor +{ /** * Given some BackendOutput, contribute the Storage portion of the client config */ @@ -314,7 +566,7 @@ export class StorageClientConfigContributorV1 if (storageOutput === undefined) { return {}; } - const config: Partial = {}; + const config: Partial = {}; config.storage = { aws_region: storageOutput.payload.storageRegion, diff --git a/packages/client-config/src/client-config-schema/client_config_v1.2.ts b/packages/client-config/src/client-config-schema/client_config_v1.2.ts new file mode 100644 index 0000000000..f9aa397d97 --- /dev/null +++ b/packages/client-config/src/client-config-schema/client_config_v1.2.ts @@ -0,0 +1,272 @@ +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * Amazon Cognito standard attributes for users -- https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html + */ +export type AmazonCognitoStandardAttributes = + | 'address' + | 'birthdate' + | 'email' + | 'family_name' + | 'gender' + | 'given_name' + | 'locale' + | 'middle_name' + | 'name' + | 'nickname' + | 'phone_number' + | 'picture' + | 'preferred_username' + | 'profile' + | 'sub' + | 'updated_at' + | 'website' + | 'zoneinfo'; +export type AwsRegion = string; +/** + * List of supported auth types for AWS AppSync + */ +export type AwsAppsyncAuthorizationType = + | 'AMAZON_COGNITO_USER_POOLS' + | 'API_KEY' + | 'AWS_IAM' + | 'AWS_LAMBDA' + | 'OPENID_CONNECT'; +/** + * supported channels for Amazon Pinpoint + */ +export type AmazonPinpointChannels = + | 'IN_APP_MESSAGING' + | 'FCM' + | 'APNS' + | 'EMAIL' + | 'SMS'; +export type AmplifyStorageAccessActions = + | 'read' + | 'get' + | 'list' + | 'write' + | 'delete'; + +/** + * Config format for Amplify Gen 2 client libraries to communicate with backend services. + */ +export interface AWSAmplifyBackendOutputs { + /** + * Version of this schema + */ + version: '1.2'; + /** + * Outputs manually specified by developers for use with frontend library + */ + analytics?: { + amazon_pinpoint?: { + /** + * AWS Region of Amazon Pinpoint resources + */ + aws_region: string; + app_id: string; + }; + }; + /** + * Outputs generated from defineAuth + */ + auth?: { + /** + * AWS Region of Amazon Cognito resources + */ + aws_region: string; + /** + * Cognito User Pool ID + */ + user_pool_id: string; + /** + * Cognito User Pool Client ID + */ + user_pool_client_id: string; + /** + * Cognito Identity Pool ID + */ + identity_pool_id?: string; + /** + * Cognito User Pool password policy + */ + password_policy?: { + min_length: number; + require_numbers: boolean; + require_lowercase: boolean; + require_uppercase: boolean; + require_symbols: boolean; + }; + oauth?: { + /** + * Identity providers set on Cognito User Pool + * + * @minItems 0 + */ + identity_providers: ( + | 'GOOGLE' + | 'FACEBOOK' + | 'LOGIN_WITH_AMAZON' + | 'SIGN_IN_WITH_APPLE' + )[]; + /** + * Domain used for identity providers + */ + domain: string; + /** + * @minItems 0 + */ + scopes: string[]; + /** + * URIs used to redirect after signing in using an identity provider + * + * @minItems 1 + */ + redirect_sign_in_uri: string[]; + /** + * URIs used to redirect after signing out + * + * @minItems 1 + */ + redirect_sign_out_uri: string[]; + response_type: 'code' | 'token'; + }; + /** + * Cognito User Pool standard attributes required for signup + * + * @minItems 0 + */ + standard_required_attributes?: AmazonCognitoStandardAttributes[]; + /** + * Cognito User Pool username attributes + * + * @minItems 1 + */ + username_attributes?: ('email' | 'phone_number' | 'username')[]; + user_verification_types?: ('email' | 'phone_number')[]; + unauthenticated_identities_enabled?: boolean; + mfa_configuration?: 'NONE' | 'OPTIONAL' | 'REQUIRED'; + mfa_methods?: ('SMS' | 'TOTP')[]; + }; + /** + * Outputs generated from defineData + */ + data?: { + aws_region: AwsRegion; + /** + * AppSync endpoint URL + */ + url: string; + /** + * generated model introspection schema for use with generateClient + */ + model_introspection?: { + [k: string]: unknown; + }; + api_key?: string; + default_authorization_type: AwsAppsyncAuthorizationType; + authorization_types: AwsAppsyncAuthorizationType[]; + }; + /** + * Outputs manually specified by developers for use with frontend library + */ + geo?: { + /** + * AWS Region of Amazon Location Service resources + */ + aws_region: string; + /** + * Maps from Amazon Location Service + */ + maps?: { + items: { + [k: string]: AmazonLocationServiceConfig; + }; + default: string; + }; + /** + * Location search (search by places, addresses, coordinates) + */ + search_indices?: { + /** + * @minItems 1 + */ + items: string[]; + default: string; + }; + /** + * Geofencing (visualize virtual perimeters) + */ + geofence_collections?: { + /** + * @minItems 1 + */ + items: string[]; + default: string; + }; + }; + /** + * Outputs manually specified by developers for use with frontend library + */ + notifications?: { + aws_region: AwsRegion; + amazon_pinpoint_app_id: string; + /** + * @minItems 1 + */ + channels: AmazonPinpointChannels[]; + }; + /** + * Outputs generated from defineStorage + */ + storage?: { + aws_region: AwsRegion; + bucket_name: string; + buckets?: AmplifyStorageBucket[]; + }; + /** + * Outputs generated from backend.addOutput({ custom: }) + */ + custom?: { + [k: string]: unknown; + }; +} +/** + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` ".*". + */ +export interface AmazonLocationServiceConfig { + /** + * Map resource name + */ + name?: string; + /** + * Map style + */ + style?: string; +} +export interface AmplifyStorageBucket { + name: string; + bucket_name: string; + aws_region: string; + paths?: { + [k: string]: AmplifyStorageAccessRule; + }; +} +/** + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` ".*". + */ +export interface AmplifyStorageAccessRule { + guest?: AmplifyStorageAccessActions[]; + authenticated?: AmplifyStorageAccessActions[]; + groups?: AmplifyStorageAccessActions[]; + entity?: AmplifyStorageAccessActions[]; + resource?: AmplifyStorageAccessActions[]; +} diff --git a/packages/client-config/src/client-config-schema/client_config_v1.3.ts b/packages/client-config/src/client-config-schema/client_config_v1.3.ts new file mode 100644 index 0000000000..560b0773ec --- /dev/null +++ b/packages/client-config/src/client-config-schema/client_config_v1.3.ts @@ -0,0 +1,282 @@ +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * Amazon Cognito standard attributes for users -- https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html + */ +export type AmazonCognitoStandardAttributes = + | 'address' + | 'birthdate' + | 'email' + | 'family_name' + | 'gender' + | 'given_name' + | 'locale' + | 'middle_name' + | 'name' + | 'nickname' + | 'phone_number' + | 'picture' + | 'preferred_username' + | 'profile' + | 'sub' + | 'updated_at' + | 'website' + | 'zoneinfo'; +export type AwsRegion = string; +/** + * List of supported auth types for AWS AppSync + */ +export type AwsAppsyncAuthorizationType = + | 'AMAZON_COGNITO_USER_POOLS' + | 'API_KEY' + | 'AWS_IAM' + | 'AWS_LAMBDA' + | 'OPENID_CONNECT'; +/** + * supported channels for Amazon Pinpoint + */ +export type AmazonPinpointChannels = + | 'IN_APP_MESSAGING' + | 'FCM' + | 'APNS' + | 'EMAIL' + | 'SMS'; +export type AmplifyStorageAccessActions = + | 'read' + | 'get' + | 'list' + | 'write' + | 'delete'; + +/** + * Config format for Amplify Gen 2 client libraries to communicate with backend services. + */ +export interface AWSAmplifyBackendOutputs { + /** + * Version of this schema + */ + version: '1.3'; + /** + * Outputs manually specified by developers for use with frontend library + */ + analytics?: { + amazon_pinpoint?: { + /** + * AWS Region of Amazon Pinpoint resources + */ + aws_region: string; + app_id: string; + }; + }; + /** + * Outputs generated from defineAuth + */ + auth?: { + /** + * AWS Region of Amazon Cognito resources + */ + aws_region: string; + /** + * Cognito User Pool ID + */ + user_pool_id: string; + /** + * Cognito User Pool Client ID + */ + user_pool_client_id: string; + /** + * Cognito Identity Pool ID + */ + identity_pool_id?: string; + /** + * Cognito User Pool password policy + */ + password_policy?: { + min_length: number; + require_numbers: boolean; + require_lowercase: boolean; + require_uppercase: boolean; + require_symbols: boolean; + }; + oauth?: { + /** + * Identity providers set on Cognito User Pool + * + * @minItems 0 + */ + identity_providers: ( + | 'GOOGLE' + | 'FACEBOOK' + | 'LOGIN_WITH_AMAZON' + | 'SIGN_IN_WITH_APPLE' + )[]; + /** + * Domain used for identity providers + */ + domain: string; + /** + * @minItems 0 + */ + scopes: string[]; + /** + * URIs used to redirect after signing in using an identity provider + * + * @minItems 1 + */ + redirect_sign_in_uri: string[]; + /** + * URIs used to redirect after signing out + * + * @minItems 1 + */ + redirect_sign_out_uri: string[]; + response_type: 'code' | 'token'; + }; + /** + * Cognito User Pool standard attributes required for signup + * + * @minItems 0 + */ + standard_required_attributes?: AmazonCognitoStandardAttributes[]; + /** + * Cognito User Pool username attributes + * + * @minItems 1 + */ + username_attributes?: ('email' | 'phone_number' | 'username')[]; + user_verification_types?: ('email' | 'phone_number')[]; + unauthenticated_identities_enabled?: boolean; + mfa_configuration?: 'NONE' | 'OPTIONAL' | 'REQUIRED'; + mfa_methods?: ('SMS' | 'TOTP')[]; + groups?: { + [k: string]: AmplifyUserGroupConfig; + }[]; + }; + /** + * Outputs generated from defineData + */ + data?: { + aws_region: AwsRegion; + /** + * AppSync endpoint URL + */ + url: string; + /** + * generated model introspection schema for use with generateClient + */ + model_introspection?: { + [k: string]: unknown; + }; + api_key?: string; + default_authorization_type: AwsAppsyncAuthorizationType; + authorization_types: AwsAppsyncAuthorizationType[]; + }; + /** + * Outputs manually specified by developers for use with frontend library + */ + geo?: { + /** + * AWS Region of Amazon Location Service resources + */ + aws_region: string; + /** + * Maps from Amazon Location Service + */ + maps?: { + items: { + [k: string]: AmazonLocationServiceConfig; + }; + default: string; + }; + /** + * Location search (search by places, addresses, coordinates) + */ + search_indices?: { + /** + * @minItems 1 + */ + items: string[]; + default: string; + }; + /** + * Geofencing (visualize virtual perimeters) + */ + geofence_collections?: { + /** + * @minItems 1 + */ + items: string[]; + default: string; + }; + }; + /** + * Outputs manually specified by developers for use with frontend library + */ + notifications?: { + aws_region: AwsRegion; + amazon_pinpoint_app_id: string; + /** + * @minItems 1 + */ + channels: AmazonPinpointChannels[]; + }; + /** + * Outputs generated from defineStorage + */ + storage?: { + aws_region: AwsRegion; + bucket_name: string; + buckets?: AmplifyStorageBucket[]; + }; + /** + * Outputs generated from backend.addOutput({ custom: }) + */ + custom?: { + [k: string]: unknown; + }; +} +/** + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` ".*". + */ +export interface AmplifyUserGroupConfig { + precedence?: number; +} +/** + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` ".*". + */ +export interface AmazonLocationServiceConfig { + /** + * Map resource name + */ + name?: string; + /** + * Map style + */ + style?: string; +} +export interface AmplifyStorageBucket { + name: string; + bucket_name: string; + aws_region: string; + paths?: { + [k: string]: AmplifyStorageAccessRule; + }; +} +/** + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` ".*". + */ +export interface AmplifyStorageAccessRule { + guest?: AmplifyStorageAccessActions[]; + authenticated?: AmplifyStorageAccessActions[]; + groups?: AmplifyStorageAccessActions[]; + entity?: AmplifyStorageAccessActions[]; + resource?: AmplifyStorageAccessActions[]; +} diff --git a/packages/client-config/src/client-config-schema/schema_v1.2.json b/packages/client-config/src/client-config-schema/schema_v1.2.json new file mode 100644 index 0000000000..b85a10ff30 --- /dev/null +++ b/packages/client-config/src/client-config-schema/schema_v1.2.json @@ -0,0 +1,476 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://amplify.aws/2024-02/outputs-schema.json", + "title": "AWS Amplify Backend Outputs", + "description": "Config format for Amplify Gen 2 client libraries to communicate with backend services.", + "type": "object", + "additionalProperties": false, + "properties": { + "$schema": { + "description": "JSON schema", + "type": "string" + }, + "version": { + "description": "Version of this schema", + "const": "1.2" + }, + "analytics": { + "description": "Outputs manually specified by developers for use with frontend library", + "type": "object", + "additionalProperties": false, + "properties": { + "amazon_pinpoint": { + "type": "object", + "additionalProperties": false, + "properties": { + "aws_region": { + "description": "AWS Region of Amazon Pinpoint resources", + "$ref": "#/$defs/aws_region" + }, + "app_id": { + "type": "string" + } + }, + "required": ["aws_region", "app_id"] + } + } + }, + "auth": { + "description": "Outputs generated from defineAuth", + "type": "object", + "additionalProperties": false, + "properties": { + "aws_region": { + "description": "AWS Region of Amazon Cognito resources", + "$ref": "#/$defs/aws_region" + }, + "user_pool_id": { + "description": "Cognito User Pool ID", + "type": "string" + }, + "user_pool_client_id": { + "description": "Cognito User Pool Client ID", + "type": "string" + }, + "identity_pool_id": { + "description": "Cognito Identity Pool ID", + "type": "string" + }, + "password_policy": { + "description": "Cognito User Pool password policy", + "type": "object", + "additionalProperties": false, + "properties": { + "min_length": { + "type": "integer", + "minimum": 6, + "maximum": 99 + }, + "require_numbers": { + "type": "boolean" + }, + "require_lowercase": { + "type": "boolean" + }, + "require_uppercase": { + "type": "boolean" + }, + "require_symbols": { + "type": "boolean" + } + }, + "required": [ + "min_length", + "require_numbers", + "require_lowercase", + "require_uppercase", + "require_symbols" + ] + }, + "oauth": { + "type": "object", + "additionalProperties": false, + "properties": { + "identity_providers": { + "description": "Identity providers set on Cognito User Pool", + "type": "array", + "items": { + "type": "string", + "enum": [ + "GOOGLE", + "FACEBOOK", + "LOGIN_WITH_AMAZON", + "SIGN_IN_WITH_APPLE" + ] + }, + "minItems": 0, + "uniqueItems": true + }, + "domain": { + "description": "Domain used for identity providers", + "type": "string" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 0, + "uniqueItems": true + }, + "redirect_sign_in_uri": { + "description": "URIs used to redirect after signing in using an identity provider", + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, + "redirect_sign_out_uri": { + "description": "URIs used to redirect after signing out", + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, + "response_type": { + "type": "string", + "enum": ["code", "token"] + } + }, + "required": [ + "identity_providers", + "domain", + "scopes", + "redirect_sign_in_uri", + "redirect_sign_out_uri", + "response_type" + ] + }, + "standard_required_attributes": { + "description": "Cognito User Pool standard attributes required for signup", + "type": "array", + "items": { + "$ref": "#/$defs/amazon_cognito_standard_attributes" + }, + "minItems": 0, + "uniqueItems": true + }, + "username_attributes": { + "description": "Cognito User Pool username attributes", + "type": "array", + "items": { + "type": "string", + "enum": ["email", "phone_number", "username"] + }, + "minItems": 1, + "uniqueItems": true + }, + "user_verification_types": { + "type": "array", + "items": { + "type": "string", + "enum": ["email", "phone_number"] + } + }, + "unauthenticated_identities_enabled": { + "type": "boolean", + "default": true + }, + "mfa_configuration": { + "type": "string", + "enum": ["NONE", "OPTIONAL", "REQUIRED"] + }, + "mfa_methods": { + "type": "array", + "items": { + "enum": ["SMS", "TOTP"] + } + } + }, + "required": ["aws_region", "user_pool_id", "user_pool_client_id"] + }, + "data": { + "description": "Outputs generated from defineData", + "type": "object", + "additionalProperties": false, + "properties": { + "aws_region": { + "$ref": "#/$defs/aws_region" + }, + "url": { + "description": "AppSync endpoint URL", + "type": "string" + }, + "model_introspection": { + "description": "generated model introspection schema for use with generateClient", + "type": "object" + }, + "api_key": { + "type": "string" + }, + "default_authorization_type": { + "$ref": "#/$defs/aws_appsync_authorization_type" + }, + "authorization_types": { + "type": "array", + "items": { + "$ref": "#/$defs/aws_appsync_authorization_type" + } + } + }, + "required": [ + "aws_region", + "url", + "default_authorization_type", + "authorization_types" + ] + }, + "geo": { + "description": "Outputs manually specified by developers for use with frontend library", + "type": "object", + "additionalProperties": false, + "properties": { + "aws_region": { + "description": "AWS Region of Amazon Location Service resources", + "$ref": "#/$defs/aws_region" + }, + "maps": { + "description": "Maps from Amazon Location Service", + "type": "object", + "additionalProperties": false, + "properties": { + "items": { + "type": "object", + "additionalProperties": false, + "propertyNames": { + "description": "Amazon Location Service Map name", + "type": "string" + }, + "patternProperties": { + ".*": { + "$ref": "#/$defs/amazon_location_service_config" + } + } + }, + "default": { + "type": "string" + } + }, + "required": ["items", "default"] + }, + "search_indices": { + "description": "Location search (search by places, addresses, coordinates)", + "type": "object", + "additionalProperties": false, + "properties": { + "items": { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "description": "Actual search name", + "type": "string" + } + }, + "default": { + "type": "string" + } + }, + "required": ["items", "default"] + }, + "geofence_collections": { + "description": "Geofencing (visualize virtual perimeters)", + "type": "object", + "additionalProperties": false, + "properties": { + "items": { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "description": "Geofence name", + "type": "string" + } + }, + "default": { + "type": "string" + } + }, + "required": ["items", "default"] + } + }, + "required": ["aws_region"] + }, + "notifications": { + "type": "object", + "description": "Outputs manually specified by developers for use with frontend library", + "additionalProperties": false, + "properties": { + "aws_region": { + "$ref": "#/$defs/aws_region" + }, + "amazon_pinpoint_app_id": { + "type": "string" + }, + "channels": { + "type": "array", + "items": { + "$ref": "#/$defs/amazon_pinpoint_channels" + }, + "minItems": 1, + "uniqueItems": true + } + }, + "required": ["aws_region", "amazon_pinpoint_app_id", "channels"] + }, + "storage": { + "type": "object", + "description": "Outputs generated from defineStorage", + "additionalProperties": false, + "properties": { + "aws_region": { + "$ref": "#/$defs/aws_region" + }, + "bucket_name": { + "type": "string" + }, + "buckets": { + "type": "array", + "items": { + "$ref": "#/$defs/amplify_storage_bucket" + } + } + }, + "required": ["aws_region", "bucket_name"] + }, + "custom": { + "description": "Outputs generated from backend.addOutput({ custom: })", + "type": "object" + } + }, + "required": ["version"], + "$defs": { + "amplify_storage_access_actions": { + "type": "string", + "enum": ["read", "get", "list", "write", "delete"] + }, + "amplify_storage_access_rule": { + "type": "object", + "additionalProperties": false, + "properties": { + "guest": { + "type": "array", + "items": { + "$ref": "#/$defs/amplify_storage_access_actions" + } + }, + "authenticated": { + "type": "array", + "items": { + "$ref": "#/$defs/amplify_storage_access_actions" + } + }, + "groups": { + "type": "array", + "items": { + "$ref": "#/$defs/amplify_storage_access_actions" + } + }, + "entity": { + "type": "array", + "items": { + "$ref": "#/$defs/amplify_storage_access_actions" + } + }, + "resource": { + "type": "array", + "items": { + "$ref": "#/$defs/amplify_storage_access_actions" + } + } + } + }, + "amplify_storage_bucket": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "bucket_name": { + "type": "string" + }, + "aws_region": { + "type": "string" + }, + "paths": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + ".*": { + "$ref": "#/$defs/amplify_storage_access_rule" + } + } + } + }, + "required": ["bucket_name", "aws_region", "name"] + }, + "aws_region": { + "type": "string" + }, + "amazon_cognito_standard_attributes": { + "description": "Amazon Cognito standard attributes for users -- https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html", + "type": "string", + "enum": [ + "address", + "birthdate", + "email", + "family_name", + "gender", + "given_name", + "locale", + "middle_name", + "name", + "nickname", + "phone_number", + "picture", + "preferred_username", + "profile", + "sub", + "updated_at", + "website", + "zoneinfo" + ] + }, + "aws_appsync_authorization_type": { + "description": "List of supported auth types for AWS AppSync", + "type": "string", + "enum": [ + "AMAZON_COGNITO_USER_POOLS", + "API_KEY", + "AWS_IAM", + "AWS_LAMBDA", + "OPENID_CONNECT" + ] + }, + "amazon_location_service_config": { + "type": "object", + "additionalProperties": false, + "properties": { + "style": { + "description": "Map style", + "type": "string" + } + } + }, + "amazon_pinpoint_channels": { + "description": "supported channels for Amazon Pinpoint", + "type": "string", + "enum": ["IN_APP_MESSAGING", "FCM", "APNS", "EMAIL", "SMS"] + } + } +} diff --git a/packages/client-config/src/client-config-schema/schema_v1.3.json b/packages/client-config/src/client-config-schema/schema_v1.3.json new file mode 100644 index 0000000000..bf89de5504 --- /dev/null +++ b/packages/client-config/src/client-config-schema/schema_v1.3.json @@ -0,0 +1,500 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://amplify.aws/2024-02/outputs-schema.json", + "title": "AWS Amplify Backend Outputs", + "description": "Config format for Amplify Gen 2 client libraries to communicate with backend services.", + "type": "object", + "additionalProperties": false, + "properties": { + "$schema": { + "description": "JSON schema", + "type": "string" + }, + "version": { + "description": "Version of this schema", + "const": "1.3" + }, + "analytics": { + "description": "Outputs manually specified by developers for use with frontend library", + "type": "object", + "additionalProperties": false, + "properties": { + "amazon_pinpoint": { + "type": "object", + "additionalProperties": false, + "properties": { + "aws_region": { + "description": "AWS Region of Amazon Pinpoint resources", + "$ref": "#/$defs/aws_region" + }, + "app_id": { + "type": "string" + } + }, + "required": ["aws_region", "app_id"] + } + } + }, + "auth": { + "description": "Outputs generated from defineAuth", + "type": "object", + "additionalProperties": false, + "properties": { + "aws_region": { + "description": "AWS Region of Amazon Cognito resources", + "$ref": "#/$defs/aws_region" + }, + "user_pool_id": { + "description": "Cognito User Pool ID", + "type": "string" + }, + "user_pool_client_id": { + "description": "Cognito User Pool Client ID", + "type": "string" + }, + "identity_pool_id": { + "description": "Cognito Identity Pool ID", + "type": "string" + }, + "password_policy": { + "description": "Cognito User Pool password policy", + "type": "object", + "additionalProperties": false, + "properties": { + "min_length": { + "type": "integer", + "minimum": 6, + "maximum": 99 + }, + "require_numbers": { + "type": "boolean" + }, + "require_lowercase": { + "type": "boolean" + }, + "require_uppercase": { + "type": "boolean" + }, + "require_symbols": { + "type": "boolean" + } + }, + "required": [ + "min_length", + "require_numbers", + "require_lowercase", + "require_uppercase", + "require_symbols" + ] + }, + "oauth": { + "type": "object", + "additionalProperties": false, + "properties": { + "identity_providers": { + "description": "Identity providers set on Cognito User Pool", + "type": "array", + "items": { + "type": "string", + "enum": [ + "GOOGLE", + "FACEBOOK", + "LOGIN_WITH_AMAZON", + "SIGN_IN_WITH_APPLE" + ] + }, + "minItems": 0, + "uniqueItems": true + }, + "domain": { + "description": "Domain used for identity providers", + "type": "string" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 0, + "uniqueItems": true + }, + "redirect_sign_in_uri": { + "description": "URIs used to redirect after signing in using an identity provider", + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, + "redirect_sign_out_uri": { + "description": "URIs used to redirect after signing out", + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, + "response_type": { + "type": "string", + "enum": ["code", "token"] + } + }, + "required": [ + "identity_providers", + "domain", + "scopes", + "redirect_sign_in_uri", + "redirect_sign_out_uri", + "response_type" + ] + }, + "standard_required_attributes": { + "description": "Cognito User Pool standard attributes required for signup", + "type": "array", + "items": { + "$ref": "#/$defs/amazon_cognito_standard_attributes" + }, + "minItems": 0, + "uniqueItems": true + }, + "username_attributes": { + "description": "Cognito User Pool username attributes", + "type": "array", + "items": { + "type": "string", + "enum": ["email", "phone_number", "username"] + }, + "minItems": 1, + "uniqueItems": true + }, + "user_verification_types": { + "type": "array", + "items": { + "type": "string", + "enum": ["email", "phone_number"] + } + }, + "unauthenticated_identities_enabled": { + "type": "boolean", + "default": true + }, + "mfa_configuration": { + "type": "string", + "enum": ["NONE", "OPTIONAL", "REQUIRED"] + }, + "mfa_methods": { + "type": "array", + "items": { + "enum": ["SMS", "TOTP"] + } + }, + "groups": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "propertyNames": { + "type": "string" + }, + "patternProperties": { + ".*": { + "$ref": "#/$defs/amplify_user_group_config" + } + } + } + } + }, + "required": ["aws_region", "user_pool_id", "user_pool_client_id"] + }, + "data": { + "description": "Outputs generated from defineData", + "type": "object", + "additionalProperties": false, + "properties": { + "aws_region": { + "$ref": "#/$defs/aws_region" + }, + "url": { + "description": "AppSync endpoint URL", + "type": "string" + }, + "model_introspection": { + "description": "generated model introspection schema for use with generateClient", + "type": "object" + }, + "api_key": { + "type": "string" + }, + "default_authorization_type": { + "$ref": "#/$defs/aws_appsync_authorization_type" + }, + "authorization_types": { + "type": "array", + "items": { + "$ref": "#/$defs/aws_appsync_authorization_type" + } + } + }, + "required": [ + "aws_region", + "url", + "default_authorization_type", + "authorization_types" + ] + }, + "geo": { + "description": "Outputs manually specified by developers for use with frontend library", + "type": "object", + "additionalProperties": false, + "properties": { + "aws_region": { + "description": "AWS Region of Amazon Location Service resources", + "$ref": "#/$defs/aws_region" + }, + "maps": { + "description": "Maps from Amazon Location Service", + "type": "object", + "additionalProperties": false, + "properties": { + "items": { + "type": "object", + "additionalProperties": false, + "propertyNames": { + "description": "Amazon Location Service Map name", + "type": "string" + }, + "patternProperties": { + ".*": { + "$ref": "#/$defs/amazon_location_service_config" + } + } + }, + "default": { + "type": "string" + } + }, + "required": ["items", "default"] + }, + "search_indices": { + "description": "Location search (search by places, addresses, coordinates)", + "type": "object", + "additionalProperties": false, + "properties": { + "items": { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "description": "Actual search name", + "type": "string" + } + }, + "default": { + "type": "string" + } + }, + "required": ["items", "default"] + }, + "geofence_collections": { + "description": "Geofencing (visualize virtual perimeters)", + "type": "object", + "additionalProperties": false, + "properties": { + "items": { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "description": "Geofence name", + "type": "string" + } + }, + "default": { + "type": "string" + } + }, + "required": ["items", "default"] + } + }, + "required": ["aws_region"] + }, + "notifications": { + "type": "object", + "description": "Outputs manually specified by developers for use with frontend library", + "additionalProperties": false, + "properties": { + "aws_region": { + "$ref": "#/$defs/aws_region" + }, + "amazon_pinpoint_app_id": { + "type": "string" + }, + "channels": { + "type": "array", + "items": { + "$ref": "#/$defs/amazon_pinpoint_channels" + }, + "minItems": 1, + "uniqueItems": true + } + }, + "required": ["aws_region", "amazon_pinpoint_app_id", "channels"] + }, + "storage": { + "type": "object", + "description": "Outputs generated from defineStorage", + "additionalProperties": false, + "properties": { + "aws_region": { + "$ref": "#/$defs/aws_region" + }, + "bucket_name": { + "type": "string" + }, + "buckets": { + "type": "array", + "items": { + "$ref": "#/$defs/amplify_storage_bucket" + } + } + }, + "required": ["aws_region", "bucket_name"] + }, + "custom": { + "description": "Outputs generated from backend.addOutput({ custom: })", + "type": "object" + } + }, + "required": ["version"], + "$defs": { + "amplify_storage_access_actions": { + "type": "string", + "enum": ["read", "get", "list", "write", "delete"] + }, + "amplify_storage_access_rule": { + "type": "object", + "additionalProperties": false, + "properties": { + "guest": { + "type": "array", + "items": { + "$ref": "#/$defs/amplify_storage_access_actions" + } + }, + "authenticated": { + "type": "array", + "items": { + "$ref": "#/$defs/amplify_storage_access_actions" + } + }, + "groups": { + "type": "array", + "items": { + "$ref": "#/$defs/amplify_storage_access_actions" + } + }, + "entity": { + "type": "array", + "items": { + "$ref": "#/$defs/amplify_storage_access_actions" + } + }, + "resource": { + "type": "array", + "items": { + "$ref": "#/$defs/amplify_storage_access_actions" + } + } + } + }, + "amplify_storage_bucket": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "bucket_name": { + "type": "string" + }, + "aws_region": { + "type": "string" + }, + "paths": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + ".*": { + "$ref": "#/$defs/amplify_storage_access_rule" + } + } + } + }, + "required": ["bucket_name", "aws_region", "name"] + }, + "aws_region": { + "type": "string" + }, + "amazon_cognito_standard_attributes": { + "description": "Amazon Cognito standard attributes for users -- https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html", + "type": "string", + "enum": [ + "address", + "birthdate", + "email", + "family_name", + "gender", + "given_name", + "locale", + "middle_name", + "name", + "nickname", + "phone_number", + "picture", + "preferred_username", + "profile", + "sub", + "updated_at", + "website", + "zoneinfo" + ] + }, + "aws_appsync_authorization_type": { + "description": "List of supported auth types for AWS AppSync", + "type": "string", + "enum": [ + "AMAZON_COGNITO_USER_POOLS", + "API_KEY", + "AWS_IAM", + "AWS_LAMBDA", + "OPENID_CONNECT" + ] + }, + "amazon_location_service_config": { + "type": "object", + "additionalProperties": false, + "properties": { + "style": { + "description": "Map style", + "type": "string" + } + } + }, + "amazon_pinpoint_channels": { + "description": "supported channels for Amazon Pinpoint", + "type": "string", + "enum": ["IN_APP_MESSAGING", "FCM", "APNS", "EMAIL", "SMS"] + }, + "amplify_user_group_config": { + "type": "object", + "additionalProperties": false, + "properties": { + "precedence": { + "type": "integer" + } + } + } + } +} diff --git a/packages/client-config/src/client-config-types/client_config.ts b/packages/client-config/src/client-config-types/client_config.ts index d9cc71e1aa..501d52792b 100644 --- a/packages/client-config/src/client-config-types/client_config.ts +++ b/packages/client-config/src/client-config-types/client_config.ts @@ -9,8 +9,11 @@ import { NotificationsClientConfig } from './notifications_client_config.js'; // Versions of new unified config schemas import * as clientConfigTypesV1 from '../client-config-schema/client_config_v1.js'; -// eslint-disable-next-line @typescript-eslint/naming-convention +/* eslint-disable @typescript-eslint/naming-convention */ import * as clientConfigTypesV1_1 from '../client-config-schema/client_config_v1.1.js'; +import * as clientConfigTypesV1_2 from '../client-config-schema/client_config_v1.2.js'; +import * as clientConfigTypesV1_3 from '../client-config-schema/client_config_v1.3.js'; +/* eslint-enable @typescript-eslint/naming-convention */ /** * Merged type of all category client config legacy types @@ -32,22 +35,31 @@ export type ClientConfigLegacy = Partial< * ClientConfig = clientConfigTypesV1.AWSAmplifyBackendOutputs | clientConfigTypesV2.AWSAmplifyBackendOutputs; */ export type ClientConfig = + | clientConfigTypesV1_3.AWSAmplifyBackendOutputs + | clientConfigTypesV1_2.AWSAmplifyBackendOutputs | clientConfigTypesV1_1.AWSAmplifyBackendOutputs | clientConfigTypesV1.AWSAmplifyBackendOutputs; -export { clientConfigTypesV1, clientConfigTypesV1_1 }; +export { + clientConfigTypesV1, + clientConfigTypesV1_1, + clientConfigTypesV1_2, + clientConfigTypesV1_3, +}; export enum ClientConfigVersionOption { V0 = '0', // Legacy client config V1 = '1', V1_1 = '1.1', + V1_2 = '1.2', + V1_3 = '1.3', } export type ClientConfigVersion = `${ClientConfigVersionOption}`; // Client config version that is generated by default if customers didn't specify one export const DEFAULT_CLIENT_CONFIG_VERSION: ClientConfigVersion = - ClientConfigVersionOption.V1_1; + ClientConfigVersionOption.V1_3; /** * Return type of `getClientConfig`. This types narrow the returned client config version @@ -60,7 +72,11 @@ export const DEFAULT_CLIENT_CONFIG_VERSION: ClientConfigVersion = * ? clientConfigTypesV2.AWSAmplifyBackendOutputs * : never; */ -export type ClientConfigVersionTemplateType = T extends '1.1' +export type ClientConfigVersionTemplateType = T extends '1.3' + ? clientConfigTypesV1_3.AWSAmplifyBackendOutputs + : T extends '1.2' + ? clientConfigTypesV1_2.AWSAmplifyBackendOutputs + : T extends '1.1' ? clientConfigTypesV1_1.AWSAmplifyBackendOutputs : T extends '1' ? clientConfigTypesV1.AWSAmplifyBackendOutputs diff --git a/packages/client-config/src/client-config-writer/client_config_formatter_default.test.ts b/packages/client-config/src/client-config-writer/client_config_formatter_default.test.ts index 72023851a4..33a6e57ff0 100644 --- a/packages/client-config/src/client-config-writer/client_config_formatter_default.test.ts +++ b/packages/client-config/src/client-config-writer/client_config_formatter_default.test.ts @@ -13,7 +13,7 @@ void describe('client config formatter', () => { const sampleIdentityPoolId = 'test_identity_pool_id'; const sampleUserPoolClientId = 'test_user_pool_client_id'; const clientConfig: ClientConfig = { - version: '1.1', + version: '1.3', auth: { aws_region: sampleRegion, identity_pool_id: sampleIdentityPoolId, @@ -23,7 +23,7 @@ void describe('client config formatter', () => { }; const expectedConfigReturned: ClientConfig = { - version: '1.1', + version: '1.3', auth: { aws_region: sampleRegion, identity_pool_id: sampleIdentityPoolId, @@ -50,7 +50,7 @@ void describe('client config formatter', () => { ClientConfigFormat.DART ); - assert.ok(formattedConfig.startsWith("const amplifyConfig = '''")); + assert.ok(formattedConfig.startsWith("const amplifyConfig = r'''")); assert.ok( formattedConfig.includes(JSON.stringify(expectedConfigReturned, null, 2)) ); diff --git a/packages/client-config/src/client-config-writer/client_config_formatter_default.ts b/packages/client-config/src/client-config-writer/client_config_formatter_default.ts index e05e68704a..54994f0096 100644 --- a/packages/client-config/src/client-config-writer/client_config_formatter_default.ts +++ b/packages/client-config/src/client-config-writer/client_config_formatter_default.ts @@ -16,7 +16,9 @@ export class ClientConfigFormatterDefault implements ClientConfigFormatter { format = (clientConfig: ClientConfig, format: ClientConfigFormat): string => { switch (format) { case ClientConfigFormat.DART: { - return `const amplifyConfig = '''${JSON.stringify( + // Using raw string, i.e. r''' to disable Dart's interpolations + // because we're using special characters like $ in some outputs. + return `const amplifyConfig = r'''${JSON.stringify( clientConfig, null, 2 diff --git a/packages/client-config/src/client-config-writer/client_config_formatter_legacy.test.ts b/packages/client-config/src/client-config-writer/client_config_formatter_legacy.test.ts index 479c542efa..bfe343250d 100644 --- a/packages/client-config/src/client-config-writer/client_config_formatter_legacy.test.ts +++ b/packages/client-config/src/client-config-writer/client_config_formatter_legacy.test.ts @@ -20,7 +20,7 @@ void describe('client config formatter', () => { const sampleUserPoolId = randomUUID(); const clientConfig: ClientConfig = { - version: '1.1', + version: '1.3', auth: { aws_region: sampleRegion, identity_pool_id: sampleIdentityPoolId, @@ -109,7 +109,7 @@ void describe('client config formatter', () => { expectedLegacyConfig.aws_user_pools_id ); - assert.ok(formattedConfig.startsWith("const amplifyConfig = '''")); + assert.ok(formattedConfig.startsWith("const amplifyConfig = r'''")); assert.ok( formattedConfig.includes(JSON.stringify(clientConfigMobile, null, 2)) ); diff --git a/packages/client-config/src/client-config-writer/client_config_formatter_legacy.ts b/packages/client-config/src/client-config-writer/client_config_formatter_legacy.ts index 153b6add97..fdb1355259 100644 --- a/packages/client-config/src/client-config-writer/client_config_formatter_legacy.ts +++ b/packages/client-config/src/client-config-writer/client_config_formatter_legacy.ts @@ -29,7 +29,9 @@ export class ClientConfigFormatterLegacy implements ClientConfigFormatter { }export default amplifyConfig;${os.EOL}`; } case ClientConfigFormat.DART: { - return `const amplifyConfig = '''${JSON.stringify( + // Using raw string, i.e. r''' to disable Dart's interpolations + // because we're using special characters like $ in some outputs. + return `const amplifyConfig = r'''${JSON.stringify( this.configConverter.convertToMobileConfig(legacyConfig), null, 2 diff --git a/packages/client-config/src/client-config-writer/client_config_to_legacy_converter.test.ts b/packages/client-config/src/client-config-writer/client_config_to_legacy_converter.test.ts index 1d564740a0..ce5ce1b110 100644 --- a/packages/client-config/src/client-config-writer/client_config_to_legacy_converter.test.ts +++ b/packages/client-config/src/client-config-writer/client_config_to_legacy_converter.test.ts @@ -26,7 +26,7 @@ void describe('ClientConfigLegacyConverter', () => { version: '3' as any, }), new AmplifyFault('UnsupportedClientConfigVersionFault', { - message: 'Only version 1.1 of ClientConfig is supported.', + message: 'Only version 1.3 of ClientConfig is supported.', }) ); }); @@ -35,7 +35,7 @@ void describe('ClientConfigLegacyConverter', () => { const converter = new ClientConfigLegacyConverter(); const v1Config: ClientConfig = { - version: ClientConfigVersionOption.V1_1, + version: ClientConfigVersionOption.V1_3, auth: { identity_pool_id: 'testIdentityPoolId', user_pool_id: 'testUserPoolId', @@ -133,7 +133,7 @@ void describe('ClientConfigLegacyConverter', () => { const converter = new ClientConfigLegacyConverter(); const v1Config: ClientConfig = { - version: ClientConfigVersionOption.V1_1, + version: ClientConfigVersionOption.V1_3, data: { aws_region: 'testRegion', url: 'testUrl', @@ -274,7 +274,7 @@ void describe('ClientConfigLegacyConverter', () => { const converter = new ClientConfigLegacyConverter(); const v1Config: ClientConfig = { - version: ClientConfigVersionOption.V1_1, + version: ClientConfigVersionOption.V1_3, storage: { aws_region: 'testRegion', bucket_name: 'testBucket', @@ -296,7 +296,7 @@ void describe('ClientConfigLegacyConverter', () => { const converter = new ClientConfigLegacyConverter(); const v1Config: ClientConfig = { - version: ClientConfigVersionOption.V1_1, + version: ClientConfigVersionOption.V1_3, custom: { customKey: { customNestedKey: { @@ -327,7 +327,7 @@ void describe('ClientConfigLegacyConverter', () => { const converter = new ClientConfigLegacyConverter(); const v1Config: ClientConfig = { - version: ClientConfigVersionOption.V1_1, + version: ClientConfigVersionOption.V1_3, analytics: { amazon_pinpoint: { aws_region: 'testRegion', @@ -356,7 +356,7 @@ void describe('ClientConfigLegacyConverter', () => { const converter = new ClientConfigLegacyConverter(); const v1Config: ClientConfig = { - version: ClientConfigVersionOption.V1_1, + version: ClientConfigVersionOption.V1_3, geo: { aws_region: 'testRegion', maps: { @@ -409,7 +409,7 @@ void describe('ClientConfigLegacyConverter', () => { const converter = new ClientConfigLegacyConverter(); let v1Config: ClientConfig = { - version: ClientConfigVersionOption.V1_1, + version: ClientConfigVersionOption.V1_3, notifications: { amazon_pinpoint_app_id: 'testAppId', aws_region: 'testRegion', @@ -452,7 +452,7 @@ void describe('ClientConfigLegacyConverter', () => { // both APNS and FCM cannot be specified together as they both map to Push. v1Config = { - version: ClientConfigVersionOption.V1_1, + version: ClientConfigVersionOption.V1_3, notifications: { amazon_pinpoint_app_id: 'testAppId', aws_region: 'testRegion', diff --git a/packages/client-config/src/client-config-writer/client_config_to_legacy_converter.ts b/packages/client-config/src/client-config-writer/client_config_to_legacy_converter.ts index db61d1b1de..c3b89dcf6e 100644 --- a/packages/client-config/src/client-config-writer/client_config_to_legacy_converter.ts +++ b/packages/client-config/src/client-config-writer/client_config_to_legacy_converter.ts @@ -2,7 +2,7 @@ import { AmplifyFault } from '@aws-amplify/platform-core'; import { ClientConfig, ClientConfigLegacy, - clientConfigTypesV1_1, + clientConfigTypesV1_3, } from '../client-config-types/client_config.js'; import { @@ -22,10 +22,10 @@ export class ClientConfigLegacyConverter { * Converts client config to a shape consumable by legacy libraries. */ convertToLegacyConfig = (clientConfig: ClientConfig): ClientConfigLegacy => { - // We can only convert from V1.1 of ClientConfig. For everything else, throw - if (!this.isClientConfigV1_1(clientConfig)) { + // We can only convert from V1.3 of ClientConfig. For everything else, throw + if (!this.isClientConfigV1_3(clientConfig)) { throw new AmplifyFault('UnsupportedClientConfigVersionFault', { - message: 'Only version 1.1 of ClientConfig is supported.', + message: 'Only version 1.3 of ClientConfig is supported.', }); } @@ -274,9 +274,9 @@ export class ClientConfigLegacyConverter { }; // eslint-disable-next-line @typescript-eslint/naming-convention - isClientConfigV1_1 = ( + isClientConfigV1_3 = ( clientConfig: ClientConfig - ): clientConfig is clientConfigTypesV1_1.AWSAmplifyBackendOutputs => { - return clientConfig.version === '1.1'; + ): clientConfig is clientConfigTypesV1_3.AWSAmplifyBackendOutputs => { + return clientConfig.version === '1.3'; }; } diff --git a/packages/client-config/src/client-config-writer/client_config_writer.test.ts b/packages/client-config/src/client-config-writer/client_config_writer.test.ts index 69697ebe3e..e181d3deb2 100644 --- a/packages/client-config/src/client-config-writer/client_config_writer.test.ts +++ b/packages/client-config/src/client-config-writer/client_config_writer.test.ts @@ -42,7 +42,7 @@ void describe('client config writer', () => { }); const clientConfig: ClientConfig = { - version: '1.1', + version: '1.3', auth: { aws_region: sampleRegion, identity_pool_id: sampleIdentityPoolId, diff --git a/packages/client-config/src/generate_empty_client_config_to_file.test.ts b/packages/client-config/src/generate_empty_client_config_to_file.test.ts index 2552a1309a..21abe85a03 100644 --- a/packages/client-config/src/generate_empty_client_config_to_file.test.ts +++ b/packages/client-config/src/generate_empty_client_config_to_file.test.ts @@ -30,15 +30,15 @@ void describe('generate empty client config to file', () => { path.join(process.cwd(), 'userOutDir', 'amplifyconfiguration.ts') ); }); - void it('correctly generates an empty file for client config version 1.1', async () => { + void it('correctly generates an empty file for client config version 1.3', async () => { await generateEmptyClientConfigToFile( - ClientConfigVersionOption.V1_1, + ClientConfigVersionOption.V1_3, 'userOutDir' ); assert.equal(writeFileMock.mock.callCount(), 1); assert.deepStrictEqual( writeFileMock.mock.calls[0].arguments[1], - `{\n "version": "1.1"\n}` + `{\n "version": "1.3"\n}` ); assert.deepStrictEqual( writeFileMock.mock.calls[0].arguments[0], diff --git a/packages/client-config/src/generate_empty_client_config_to_file.ts b/packages/client-config/src/generate_empty_client_config_to_file.ts index 039cf781c4..b2563330b1 100644 --- a/packages/client-config/src/generate_empty_client_config_to_file.ts +++ b/packages/client-config/src/generate_empty_client_config_to_file.ts @@ -15,7 +15,7 @@ export const generateEmptyClientConfigToFile = async ( format?: ClientConfigFormat ): Promise => { const clientConfig: ClientConfig = { - version: '1.1', + version: '1.3', }; return writeClientConfigToFile(clientConfig, version, outDir, format); }; diff --git a/packages/client-config/src/unified_client_config_generator.test.ts b/packages/client-config/src/unified_client_config_generator.test.ts index 9f1734ca2a..496d59df9d 100644 --- a/packages/client-config/src/unified_client_config_generator.test.ts +++ b/packages/client-config/src/unified_client_config_generator.test.ts @@ -26,6 +26,249 @@ const stubClientProvider = { }; void describe('UnifiedClientConfigGenerator', () => { void describe('generateClientConfig', () => { + void it('transforms backend output into client config for V1.3', async () => { + const groups = [ + { + ADMINS: { + precedence: 0, + }, + }, + { + EDITORS: { + precedence: 1, + }, + }, + ]; + const stubOutput: UnifiedBackendOutput = { + [platformOutputKey]: { + version: '1', + payload: { + deploymentType: 'branch', + region: 'us-east-1', + }, + }, + [authOutputKey]: { + version: '1', + payload: { + identityPoolId: 'testIdentityPoolId', + userPoolId: 'testUserPoolId', + webClientId: 'testWebClientId', + authRegion: 'us-east-1', + passwordPolicyMinLength: '8', + passwordPolicyRequirements: + '["REQUIRES_NUMBERS","REQUIRES_LOWERCASE","REQUIRES_UPPERCASE"]', + mfaTypes: '["SMS","TOTP"]', + mfaConfiguration: 'OPTIONAL', + verificationMechanisms: '["email","phone_number"]', + usernameAttributes: '["email"]', + signupAttributes: '["email"]', + allowUnauthenticatedIdentities: 'true', + groups: JSON.stringify(groups), + }, + }, + [graphqlOutputKey]: { + version: '1', + payload: { + awsAppsyncApiEndpoint: 'testApiEndpoint', + awsAppsyncRegion: 'us-east-1', + awsAppsyncAuthenticationType: 'API_KEY', + awsAppsyncAdditionalAuthenticationTypes: 'API_KEY', + awsAppsyncConflictResolutionMode: 'AUTO_MERGE', + awsAppsyncApiKey: 'testApiKey', + awsAppsyncApiId: 'testApiId', + amplifyApiModelSchemaS3Uri: 'testApiSchemaUri', + }, + }, + [customOutputKey]: { + version: '1', + payload: { + customOutputs: JSON.stringify({ + custom: { + output1: 'val1', + output2: 'val2', + }, + }), + }, + }, + }; + const outputRetrieval = mock.fn(async () => stubOutput); + const modelSchemaAdapter = new ModelIntrospectionSchemaAdapter( + stubClientProvider + ); + + mock.method( + modelSchemaAdapter, + 'getModelIntrospectionSchemaFromS3Uri', + () => undefined + ); + const configContributors = new ClientConfigContributorFactory( + modelSchemaAdapter + ).getContributors('1.3'); + const clientConfigGenerator = new UnifiedClientConfigGenerator( + outputRetrieval, + configContributors + ); + const result = await clientConfigGenerator.generateClientConfig(); + const expectedClientConfig: ClientConfig = { + auth: { + user_pool_id: 'testUserPoolId', + aws_region: 'us-east-1', + user_pool_client_id: 'testWebClientId', + identity_pool_id: 'testIdentityPoolId', + mfa_methods: ['SMS', 'TOTP'], + standard_required_attributes: ['email'], + username_attributes: ['email'], + user_verification_types: ['email', 'phone_number'], + mfa_configuration: 'OPTIONAL', + + password_policy: { + min_length: 8, + require_lowercase: true, + require_numbers: true, + require_symbols: false, + require_uppercase: true, + }, + + unauthenticated_identities_enabled: true, + groups: [ + { + ADMINS: { + precedence: 0, + }, + }, + { + EDITORS: { + precedence: 1, + }, + }, + ], + }, + data: { + url: 'testApiEndpoint', + aws_region: 'us-east-1', + api_key: 'testApiKey', + default_authorization_type: 'API_KEY', + authorization_types: ['API_KEY'], + }, + custom: { + output1: 'val1', + output2: 'val2', + }, + version: '1.3', + }; + + assert.deepStrictEqual(result, expectedClientConfig); + }); + + void it('transforms backend output into client config for V1.2', async () => { + const stubOutput: UnifiedBackendOutput = { + [platformOutputKey]: { + version: '1', + payload: { + deploymentType: 'branch', + region: 'us-east-1', + }, + }, + [authOutputKey]: { + version: '1', + payload: { + identityPoolId: 'testIdentityPoolId', + userPoolId: 'testUserPoolId', + webClientId: 'testWebClientId', + authRegion: 'us-east-1', + passwordPolicyMinLength: '8', + passwordPolicyRequirements: + '["REQUIRES_NUMBERS","REQUIRES_LOWERCASE","REQUIRES_UPPERCASE"]', + mfaTypes: '["SMS","TOTP"]', + mfaConfiguration: 'OPTIONAL', + verificationMechanisms: '["email","phone_number"]', + usernameAttributes: '["email"]', + signupAttributes: '["email"]', + allowUnauthenticatedIdentities: 'true', + }, + }, + [graphqlOutputKey]: { + version: '1', + payload: { + awsAppsyncApiEndpoint: 'testApiEndpoint', + awsAppsyncRegion: 'us-east-1', + awsAppsyncAuthenticationType: 'API_KEY', + awsAppsyncAdditionalAuthenticationTypes: 'API_KEY', + awsAppsyncConflictResolutionMode: 'AUTO_MERGE', + awsAppsyncApiKey: 'testApiKey', + awsAppsyncApiId: 'testApiId', + amplifyApiModelSchemaS3Uri: 'testApiSchemaUri', + }, + }, + [customOutputKey]: { + version: '1', + payload: { + customOutputs: JSON.stringify({ + custom: { + output1: 'val1', + output2: 'val2', + }, + }), + }, + }, + }; + const outputRetrieval = mock.fn(async () => stubOutput); + const modelSchemaAdapter = new ModelIntrospectionSchemaAdapter( + stubClientProvider + ); + + mock.method( + modelSchemaAdapter, + 'getModelIntrospectionSchemaFromS3Uri', + () => undefined + ); + const configContributors = new ClientConfigContributorFactory( + modelSchemaAdapter + ).getContributors('1.2'); + const clientConfigGenerator = new UnifiedClientConfigGenerator( + outputRetrieval, + configContributors + ); + const result = await clientConfigGenerator.generateClientConfig(); + const expectedClientConfig: ClientConfig = { + auth: { + user_pool_id: 'testUserPoolId', + aws_region: 'us-east-1', + user_pool_client_id: 'testWebClientId', + identity_pool_id: 'testIdentityPoolId', + mfa_methods: ['SMS', 'TOTP'], + standard_required_attributes: ['email'], + username_attributes: ['email'], + user_verification_types: ['email', 'phone_number'], + mfa_configuration: 'OPTIONAL', + + password_policy: { + min_length: 8, + require_lowercase: true, + require_numbers: true, + require_symbols: false, + require_uppercase: true, + }, + + unauthenticated_identities_enabled: true, + }, + data: { + url: 'testApiEndpoint', + aws_region: 'us-east-1', + api_key: 'testApiKey', + default_authorization_type: 'API_KEY', + authorization_types: ['API_KEY'], + }, + custom: { + output1: 'val1', + output2: 'val2', + }, + version: '1.2', + }; + + assert.deepStrictEqual(result, expectedClientConfig); + }); + void it('transforms backend output into client config for V1.1', async () => { const stubOutput: UnifiedBackendOutput = { [platformOutputKey]: { @@ -297,7 +540,7 @@ void describe('UnifiedClientConfigGenerator', () => { ); const configContributors = new ClientConfigContributorFactory( modelSchemaAdapter - ).getContributors('1.1'); //Generate with new configuration format + ).getContributors('1.3'); //Generate with new configuration format const clientConfigGenerator = new UnifiedClientConfigGenerator( outputRetrieval, configContributors @@ -329,7 +572,7 @@ void describe('UnifiedClientConfigGenerator', () => { output1: 'val1', output2: 'val2', }, - version: '1.1', // The max version prevails + version: '1.3', // The max version prevails }; assert.deepStrictEqual(result, expectedClientConfig); @@ -368,7 +611,7 @@ void describe('UnifiedClientConfigGenerator', () => { ); const configContributors = new ClientConfigContributorFactory( modelSchemaAdapter - ).getContributors('1.1'); + ).getContributors('1.3'); const clientConfigGenerator = new UnifiedClientConfigGenerator( outputRetrieval, @@ -400,7 +643,7 @@ void describe('UnifiedClientConfigGenerator', () => { const configContributors = new ClientConfigContributorFactory( modelSchemaAdapter - ).getContributors('1.1'); + ).getContributors('1.3'); const clientConfigGenerator = new UnifiedClientConfigGenerator( outputRetrieval, @@ -432,7 +675,7 @@ void describe('UnifiedClientConfigGenerator', () => { const configContributors = new ClientConfigContributorFactory( modelSchemaAdapter - ).getContributors('1.1'); + ).getContributors('1.3'); const clientConfigGenerator = new UnifiedClientConfigGenerator( outputRetrieval, @@ -449,6 +692,39 @@ void describe('UnifiedClientConfigGenerator', () => { ); }); + void it('throws user error if the stack is missing metadata', async () => { + const outputRetrieval = mock.fn(() => { + throw new BackendOutputClientError( + BackendOutputClientErrorType.METADATA_RETRIEVAL_ERROR, + 'Stack template metadata is not a string' + ); + }); + const modelSchemaAdapter = new ModelIntrospectionSchemaAdapter( + stubClientProvider + ); + + const configContributors = new ClientConfigContributorFactory( + modelSchemaAdapter + ).getContributors('1.1'); + + const clientConfigGenerator = new UnifiedClientConfigGenerator( + outputRetrieval, + configContributors + ); + + await assert.rejects( + () => clientConfigGenerator.generateClientConfig(), + (error: AmplifyUserError) => { + assert.strictEqual( + error.message, + 'Stack was not created with Amplify.' + ); + assert.ok(error.resolution); + return true; + } + ); + }); + void it('throws user error if credentials are expired when getting backend outputs', async () => { const outputRetrieval = mock.fn(() => { throw new BackendOutputClientError( @@ -462,7 +738,7 @@ void describe('UnifiedClientConfigGenerator', () => { const configContributors = new ClientConfigContributorFactory( modelSchemaAdapter - ).getContributors('1.1'); + ).getContributors('1.3'); const clientConfigGenerator = new UnifiedClientConfigGenerator( outputRetrieval, @@ -495,7 +771,7 @@ void describe('UnifiedClientConfigGenerator', () => { const configContributors = new ClientConfigContributorFactory( modelSchemaAdapter - ).getContributors('1.1'); + ).getContributors('1.3'); const clientConfigGenerator = new UnifiedClientConfigGenerator( outputRetrieval, diff --git a/packages/client-config/src/unified_client_config_generator.ts b/packages/client-config/src/unified_client_config_generator.ts index eb8397b01b..284bde0097 100644 --- a/packages/client-config/src/unified_client_config_generator.ts +++ b/packages/client-config/src/unified_client_config_generator.ts @@ -66,6 +66,20 @@ export class UnifiedClientConfigGenerator implements ClientConfigGenerator { error ); } + if ( + error instanceof BackendOutputClientError && + error.code === BackendOutputClientErrorType.METADATA_RETRIEVAL_ERROR + ) { + throw new AmplifyUserError( + 'NonAmplifyStackError', + { + message: 'Stack was not created with Amplify.', + resolution: + 'Ensure the CloudFormation stack ID references a main stack created with Amplify, then re-run this command.', + }, + error + ); + } if ( error instanceof BackendOutputClientError && error.code === BackendOutputClientErrorType.CREDENTIALS_ERROR diff --git a/packages/create-amplify/CHANGELOG.md b/packages/create-amplify/CHANGELOG.md index aaf0868ebc..7881bbcdbf 100644 --- a/packages/create-amplify/CHANGELOG.md +++ b/packages/create-amplify/CHANGELOG.md @@ -1,5 +1,14 @@ # create-amplify +## 1.0.6 + +### Patch Changes + +- e648e8e: added main field to package.json so these packages are resolvable +- Updated dependencies [8dd7286] + - @aws-amplify/plugin-types@1.2.2 + - @aws-amplify/cli-core@1.1.3 + ## 1.0.5 ### Patch Changes diff --git a/packages/create-amplify/package.json b/packages/create-amplify/package.json index 53c9a4f5c8..7cbff22777 100644 --- a/packages/create-amplify/package.json +++ b/packages/create-amplify/package.json @@ -1,6 +1,6 @@ { "name": "create-amplify", - "version": "1.0.5", + "version": "1.0.6", "type": "module", "main": "lib/index.js", "publishConfig": { @@ -17,9 +17,9 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/cli-core": "^1.1.1", + "@aws-amplify/cli-core": "^1.1.3", "@aws-amplify/platform-core": "^1.0.3", - "@aws-amplify/plugin-types": "^1.1.0", + "@aws-amplify/plugin-types": "^1.2.2", "execa": "^8.0.1", "kleur": "^4.1.5", "yargs": "^17.7.2" diff --git a/packages/deployed-backend-client/CHANGELOG.md b/packages/deployed-backend-client/CHANGELOG.md index 3df442dc35..49a70a8190 100644 --- a/packages/deployed-backend-client/CHANGELOG.md +++ b/packages/deployed-backend-client/CHANGELOG.md @@ -1,5 +1,21 @@ # @aws-amplify/deployed-backend-client +## 1.4.2 + +### Patch Changes + +- fdf28bd: fix: detect deploymentType from Stack Tags + +## 1.4.1 + +### Patch Changes + +- e648e8e: added main field to package.json so these packages are resolvable +- 8dd7286: fixed errors in plugin-types and cli-core along with any extraneous dependencies in other packages +- e648e8e: added main field to packages known to lack one +- Updated dependencies [8dd7286] + - @aws-amplify/plugin-types@1.2.2 + ## 1.4.0 ### Minor Changes diff --git a/packages/deployed-backend-client/package.json b/packages/deployed-backend-client/package.json index 6f631b038b..e1df5238d2 100644 --- a/packages/deployed-backend-client/package.json +++ b/packages/deployed-backend-client/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/deployed-backend-client", - "version": "1.4.0", + "version": "1.4.2", "type": "module", "publishConfig": { "access": "public" @@ -21,7 +21,7 @@ "dependencies": { "@aws-amplify/backend-output-schemas": "^1.2.0", "@aws-amplify/platform-core": "^1.0.5", - "@aws-amplify/plugin-types": "^1.2.1", + "@aws-amplify/plugin-types": "^1.2.2", "zod": "^3.22.2" }, "peerDependencies": { diff --git a/packages/deployed-backend-client/src/deployed_backend_client.ts b/packages/deployed-backend-client/src/deployed_backend_client.ts index 94b18def4c..2ff69efe31 100644 --- a/packages/deployed-backend-client/src/deployed_backend_client.ts +++ b/packages/deployed-backend-client/src/deployed_backend_client.ts @@ -15,11 +15,7 @@ import { ListBackendsResponse, } from './deployed_backend_client_factory.js'; import { BackendIdentifierConversions } from '@aws-amplify/platform-core'; -import { - BackendOutputClient, - BackendOutputClientError, - BackendOutputClientErrorType, -} from './backend_output_client_factory.js'; +import { BackendOutputClient } from './backend_output_client_factory.js'; import { CloudFormationClient, DeleteStackCommand, @@ -158,26 +154,13 @@ export class DefaultDeployedBackendClient implements DeployedBackendClient { private tryGetDeploymentType = async ( stackSummary: StackSummary ): Promise => { - const backendIdentifier = { - stackName: stackSummary.StackName as string, - }; + const stackDescription = await this.cfnClient.send( + new DescribeStacksCommand({ StackName: stackSummary.StackName }) + ); - try { - const backendOutput: BackendOutput = - await this.backendOutputClient.getOutput(backendIdentifier); - - return backendOutput[platformOutputKey].payload - .deploymentType as DeploymentType; - } catch (error) { - if ( - (error as BackendOutputClientError).code === - BackendOutputClientErrorType.METADATA_RETRIEVAL_ERROR - ) { - // Ignore stacks where metadata cannot be retrieved. These are not Amplify stacks, or not compatible with this library. - return; - } - throw error; - } + return stackDescription.Stacks?.[0].Tags?.find( + (tag) => tag.Key === 'amplify:deployment-type' + )?.Value as DeploymentType; }; private listStacks = async ( diff --git a/packages/deployed-backend-client/src/deployed_backend_client_list_delete_failed_stacks.test.ts b/packages/deployed-backend-client/src/deployed_backend_client_list_delete_failed_stacks.test.ts index 96afbc73c2..116042ad1c 100644 --- a/packages/deployed-backend-client/src/deployed_backend_client_list_delete_failed_stacks.test.ts +++ b/packages/deployed-backend-client/src/deployed_backend_client_list_delete_failed_stacks.test.ts @@ -6,15 +6,9 @@ import { ListStacksCommand, StackStatus, } from '@aws-sdk/client-cloudformation'; -import { platformOutputKey } from '@aws-amplify/backend-output-schemas'; import { DefaultBackendOutputClient } from './backend_output_client.js'; import { DefaultDeployedBackendClient } from './deployed_backend_client.js'; import { BackendStatus } from './deployed_backend_client_factory.js'; -import { - BackendOutputClientError, - BackendOutputClientErrorType, - StackIdentifier, -} from './index.js'; import { AmplifyClient } from '@aws-sdk/client-amplify'; import { S3 } from '@aws-sdk/client-s3'; import { DeployedResourcesEnumerator } from './deployed-backend-client/deployed_resources_enumerator.js'; @@ -34,14 +28,6 @@ const listStacksMock = { ], }; -const getOutputMockResponse = { - [platformOutputKey]: { - payload: { - deploymentType: 'branch', - }, - }, -}; - void describe('Deployed Backend Client list delete failed stacks', () => { const mockCfnClient = new CloudFormation(); const mockS3Client = new S3(); @@ -56,9 +42,19 @@ void describe('Deployed Backend Client list delete failed stacks', () => { const matchingStack = listStacksMock.StackSummaries.find((stack) => { return stack.StackName === request.input.StackName; }); - const stack = matchingStack; + // Add tags that are used to detect deployment type return { - Stacks: [stack], + Stacks: [ + { + ...matchingStack, + Tags: [ + { + Key: 'amplify:deployment-type', + Value: 'branch', + }, + ], + }, + ], }; } throw request; @@ -83,23 +79,6 @@ void describe('Deployed Backend Client list delete failed stacks', () => { mockCfnClient, new AmplifyClient() ); - const getOutputMock = mock.method( - mockBackendOutputClient, - 'getOutput', - (backendIdentifier: StackIdentifier) => { - if (backendIdentifier.stackName === 'amplify-test-not-a-sandbox') { - return { - ...getOutputMockResponse, - [platformOutputKey]: { - payload: { - deploymentType: 'branch', - }, - }, - }; - } - return getOutputMockResponse; - } - ); const returnedDeleteFailedStacks = [ { deploymentType: 'branch', @@ -116,7 +95,6 @@ void describe('Deployed Backend Client list delete failed stacks', () => { ]; beforeEach(() => { - getOutputMock.mock.resetCalls(); listStacksMockFn.mock.resetCalls(); cfnClientSendMock.mock.resetCalls(); const deployedResourcesEnumerator = new DeployedResourcesEnumerator( @@ -171,98 +149,4 @@ void describe('Deployed Backend Client list delete failed stacks', () => { assert.equal(listStacksMockFn.mock.callCount(), 2); }); - - void it('paginates listBackends when one page contains stacks, but it gets filtered due to not deleted failed status', async () => { - listStacksMockFn.mock.mockImplementationOnce(() => { - return { - StackSummaries: [], - NextToken: 'abc', - }; - }); - const failedStacks = deployedBackendClient.listBackends({ - deploymentType: 'branch', - backendStatusFilters: [BackendStatus.DELETE_FAILED], - }); - assert.deepEqual( - (await failedStacks.getBackendSummaryByPage().next()).value, - returnedDeleteFailedStacks - ); - - assert.equal(listStacksMockFn.mock.callCount(), 2); - }); - - void it('paginates listBackends when one page contains stacks, but it gets filtered due to sandbox deploymentType', async () => { - listStacksMockFn.mock.mockImplementationOnce(() => { - return { - StackSummaries: [], - NextToken: 'abc', - }; - }); - const failedStacks = deployedBackendClient.listBackends({ - deploymentType: 'branch', - backendStatusFilters: [BackendStatus.DELETE_FAILED], - }); - assert.deepEqual( - (await failedStacks.getBackendSummaryByPage().next()).value, - returnedDeleteFailedStacks - ); - - assert.equal(listStacksMockFn.mock.callCount(), 2); - }); - - void it('paginates listBackends when one page contains a stack, but it gets filtered due to not having gen2 outputs', async () => { - getOutputMock.mock.mockImplementationOnce(() => { - throw new BackendOutputClientError( - BackendOutputClientErrorType.METADATA_RETRIEVAL_ERROR, - 'Test metadata retrieval error' - ); - }); - listStacksMockFn.mock.mockImplementationOnce(() => { - return { - StackSummaries: [ - { - StackName: 'amplify-123-name-branch-testHash', - StackStatus: StackStatus.DELETE_FAILED, - CreationTime: new Date(0), - LastUpdatedTime: new Date(1), - }, - ], - NextToken: 'abc', - }; - }); - const failedStacks = deployedBackendClient.listBackends({ - deploymentType: 'branch', - backendStatusFilters: [BackendStatus.DELETE_FAILED], - }); - assert.deepEqual( - (await failedStacks.getBackendSummaryByPage().next()).value, - returnedDeleteFailedStacks - ); - - assert.equal(listStacksMockFn.mock.callCount(), 2); - }); - - void it('does not paginate listBackends when one page throws an unexpected error fetching gen2 outputs', async () => { - getOutputMock.mock.mockImplementationOnce(() => { - throw new Error('Unexpected Error!'); - }); - listStacksMockFn.mock.mockImplementationOnce(() => { - return { - StackSummaries: [ - { - StackName: 'amplify-123-name-branch-testHash', - StackStatus: StackStatus.DELETE_FAILED, - CreationTime: new Date(0), - LastUpdatedTime: new Date(1), - }, - ], - NextToken: 'abc', - }; - }); - const listBackendsPromise = deployedBackendClient.listBackends({ - deploymentType: 'branch', - backendStatusFilters: [BackendStatus.DELETE_FAILED], - }); - await assert.rejects(listBackendsPromise.getBackendSummaryByPage().next()); - }); }); diff --git a/packages/deployed-backend-client/src/deployed_backend_client_list_sandboxes.test.ts b/packages/deployed-backend-client/src/deployed_backend_client_list_sandboxes.test.ts index 12a9e024f9..fb8d690037 100644 --- a/packages/deployed-backend-client/src/deployed_backend_client_list_sandboxes.test.ts +++ b/packages/deployed-backend-client/src/deployed_backend_client_list_sandboxes.test.ts @@ -7,14 +7,8 @@ import { StackStatus, } from '@aws-sdk/client-cloudformation'; import { BackendDeploymentStatus } from './deployed_backend_client_factory.js'; -import { platformOutputKey } from '@aws-amplify/backend-output-schemas'; import { DefaultBackendOutputClient } from './backend_output_client.js'; import { DefaultDeployedBackendClient } from './deployed_backend_client.js'; -import { - BackendOutputClientError, - BackendOutputClientErrorType, - StackIdentifier, -} from './index.js'; import { AmplifyClient } from '@aws-sdk/client-amplify'; import { S3 } from '@aws-sdk/client-s3'; import { DeployedResourcesEnumerator } from './deployed-backend-client/deployed_resources_enumerator.js'; @@ -34,14 +28,6 @@ const listStacksMock = { ], }; -const getOutputMockResponse = { - [platformOutputKey]: { - payload: { - deploymentType: 'sandbox', - }, - }, -}; - void describe('Deployed Backend Client list sandboxes', () => { const mockCfnClient = new CloudFormation(); const mockS3Client = new S3(); @@ -56,9 +42,18 @@ void describe('Deployed Backend Client list sandboxes', () => { const matchingStack = listStacksMock.StackSummaries.find((stack) => { return stack.StackName === request.input.StackName; }); - const stack = matchingStack; return { - Stacks: [stack], + Stacks: [ + { + ...matchingStack, + Tags: [ + { + Key: 'amplify:deployment-type', + Value: 'sandbox', + }, + ], + }, + ], }; } throw request; @@ -84,23 +79,7 @@ void describe('Deployed Backend Client list sandboxes', () => { mockCfnClient, new AmplifyClient() ); - const getOutputMock = mock.method( - mockBackendOutputClient, - 'getOutput', - (backendIdentifier: StackIdentifier) => { - if (backendIdentifier.stackName === 'amplify-test-not-a-sandbox') { - return { - ...getOutputMockResponse, - [platformOutputKey]: { - payload: { - deploymentType: 'branch', - }, - }, - }; - } - return getOutputMockResponse; - } - ); + const returnedSandboxes = [ { deploymentType: 'sandbox', @@ -117,7 +96,6 @@ void describe('Deployed Backend Client list sandboxes', () => { ]; beforeEach(() => { - getOutputMock.mock.resetCalls(); listStacksMockFn.mock.resetCalls(); cfnClientSendMock.mock.resetCalls(); const deployedResourcesEnumerator = new DeployedResourcesEnumerator( @@ -209,57 +187,36 @@ void describe('Deployed Backend Client list sandboxes', () => { assert.equal(listStacksMockFn.mock.callCount(), 2); }); - void it('paginates listBackends when one page contains a stack, but it gets filtered due to not having gen2 outputs', async () => { - getOutputMock.mock.mockImplementationOnce(() => { - throw new BackendOutputClientError( - BackendOutputClientErrorType.METADATA_RETRIEVAL_ERROR, - 'Test metadata retrieval error' - ); - }); - listStacksMockFn.mock.mockImplementationOnce(() => { - return { - StackSummaries: [ - { - StackName: 'amplify-test-name-sandbox-testHash', - StackStatus: StackStatus.CREATE_COMPLETE, - CreationTime: new Date(0), - LastUpdatedTime: new Date(1), - }, - ], - NextToken: 'abc', - }; - }); + void it('filter stacks that do not have deploymentType tag in it', async () => { + cfnClientSendMock.mock.mockImplementation( + (request: ListStacksCommand | DescribeStacksCommand) => { + if (request instanceof ListStacksCommand) { + return listStacksMockFn(request.input); + } + if (request instanceof DescribeStacksCommand) { + const matchingStack = listStacksMock.StackSummaries.find((stack) => { + return stack.StackName === request.input.StackName; + }); + return { + Stacks: [ + { + ...matchingStack, + Tags: [], + }, + ], + }; + } + throw request; + } + ); const sandboxes = deployedBackendClient.listBackends({ deploymentType: 'sandbox', }); assert.deepEqual( - (await sandboxes.getBackendSummaryByPage().next()).value, - returnedSandboxes + (await sandboxes.getBackendSummaryByPage().next()).done, + true ); - assert.equal(listStacksMockFn.mock.callCount(), 2); - }); - - void it('does not paginate listBackends when one page throws an unexpected error fetching gen2 outputs', async () => { - getOutputMock.mock.mockImplementationOnce(() => { - throw new Error('Unexpected Error!'); - }); - listStacksMockFn.mock.mockImplementationOnce(() => { - return { - StackSummaries: [ - { - StackName: 'amplify-test-name-sandbox-testHash', - StackStatus: StackStatus.CREATE_COMPLETE, - CreationTime: new Date(0), - LastUpdatedTime: new Date(1), - }, - ], - NextToken: 'abc', - }; - }); - const listBackendsPromise = deployedBackendClient.listBackends({ - deploymentType: 'sandbox', - }); - await assert.rejects(listBackendsPromise.getBackendSummaryByPage().next()); + assert.equal(listStacksMockFn.mock.callCount(), 1); }); }); diff --git a/packages/eslint-rules/src/index.ts b/packages/eslint-rules/src/index.ts index 11cc1b7ab1..2daf628e91 100644 --- a/packages/eslint-rules/src/index.ts +++ b/packages/eslint-rules/src/index.ts @@ -2,9 +2,11 @@ import { noEmptyCatchRule } from './rules/no_empty_catch.js'; import { amplifyErrorNameRule } from './rules/amplify_error_name.js'; import { preferAmplifyErrorsRule } from './rules/prefer_amplify_errors.js'; import { noAmplifyErrors } from './rules/no_amplify_errors.js'; +import { amplifyErrorNoInstanceOf } from './rules/amplify_error_no_instance_of'; export const rules: Record = { 'amplify-error-name': amplifyErrorNameRule, + 'amplify-error-no-instanceof': amplifyErrorNoInstanceOf, 'no-empty-catch': noEmptyCatchRule, 'prefer-amplify-errors': preferAmplifyErrorsRule, 'no-amplify-errors': noAmplifyErrors, @@ -15,6 +17,7 @@ export const configs = { plugins: ['amplify-backend-rules'], rules: { 'amplify-backend-rules/amplify-error-name': 'error', + 'amplify-backend-rules/amplify-error-no-instanceof': 'error', 'amplify-backend-rules/no-empty-catch': 'error', 'amplify-backend-rules/prefer-amplify-errors': 'off', 'amplify-backend-rules/no-amplify-errors': 'off', @@ -27,6 +30,9 @@ export const configs = { 'packages/backend-auth/src/**', 'packages/backend-deployer/src/**', 'packages/create-amplify/src/**', + 'packages/form-generator/src/**', + 'packages/model-generator/src/**', + 'packages/schema-generator/src/**', ], excludedFiles: ['**/*.test.ts'], rules: { @@ -39,9 +45,6 @@ export const configs = { 'packages/ai-constructs/src/**', 'packages/backend-output-storage/src/**', 'packages/deployed-backend-client/src/**', - 'packages/form-generator/src/**', - 'packages/model-generator/src/**', - 'packages/schema-generator/src/**', ], rules: { 'amplify-backend-rules/no-amplify-errors': 'error', diff --git a/packages/eslint-rules/src/rules/amplify_error_no_instance_of.test.ts b/packages/eslint-rules/src/rules/amplify_error_no_instance_of.test.ts new file mode 100644 index 0000000000..c805e88fbd --- /dev/null +++ b/packages/eslint-rules/src/rules/amplify_error_no_instance_of.test.ts @@ -0,0 +1,28 @@ +import * as nodeTest from 'node:test'; +import { RuleTester } from '@typescript-eslint/rule-tester'; +import { amplifyErrorNoInstanceOf } from './amplify_error_no_instance_of.js'; + +RuleTester.afterAll = nodeTest.after; +// See https://typescript-eslint.io/packages/rule-tester/#with-specific-frameworks +// Node test runner methods return promises which are not relevant in the context of testing. +// We do ignore them in other places with void keyword. +// eslint-disable-next-line @typescript-eslint/no-misused-promises +RuleTester.it = nodeTest.it; +// eslint-disable-next-line @typescript-eslint/no-misused-promises +RuleTester.describe = nodeTest.describe; + +const ruleTester = new RuleTester(); + +ruleTester.run('amplify-error-no-instanceof', amplifyErrorNoInstanceOf, { + valid: ['e instanceof Error'], + invalid: [ + { + code: 'e instanceof AmplifyError', + errors: [ + { + messageId: 'noInstanceOfWithAmplifyError', + }, + ], + }, + ], +}); diff --git a/packages/eslint-rules/src/rules/amplify_error_no_instance_of.ts b/packages/eslint-rules/src/rules/amplify_error_no_instance_of.ts new file mode 100644 index 0000000000..bd040d2134 --- /dev/null +++ b/packages/eslint-rules/src/rules/amplify_error_no_instance_of.ts @@ -0,0 +1,41 @@ +import { ESLintUtils } from '@typescript-eslint/utils'; + +/** + * This rule flags empty catch blocks. Even if they contain comments. + * + * This rule differs from built in https://github.com/eslint/eslint/blob/main/lib/rules/no-empty.js + * in such a way that it uses typescript-eslint and typescript AST + * which does not include comments as statements in catch clause body block. + */ +export const amplifyErrorNoInstanceOf = ESLintUtils.RuleCreator.withoutDocs({ + create(context) { + return { + // This naming comes from @typescript-eslint/utils types. + // eslint-disable-next-line @typescript-eslint/naming-convention + BinaryExpression(node) { + if ( + node.operator === 'instanceof' && + node.right.type === 'Identifier' && + node.right.name === 'AmplifyError' + ) { + context.report({ + messageId: 'noInstanceOfWithAmplifyError', + node, + }); + } + }, + }; + }, + meta: { + docs: { + description: 'Instanceof operator must not be used with AmplifyError.', + }, + messages: { + noInstanceOfWithAmplifyError: + 'Do not use instanceof with AmplifyError. Use AmplifyError.isAmplifyError instead.', + }, + type: 'problem', + schema: [], + }, + defaultOptions: [], +}); diff --git a/packages/form-generator/CHANGELOG.md b/packages/form-generator/CHANGELOG.md index a2b4d28246..b09cd2d4ed 100644 --- a/packages/form-generator/CHANGELOG.md +++ b/packages/form-generator/CHANGELOG.md @@ -1,5 +1,18 @@ # @aws-amplify/form-generator +## 1.0.3 + +### Patch Changes + +- e325044: Prefer amplify errors in generators + +## 1.0.2 + +### Patch Changes + +- e648e8e: added main field to package.json so these packages are resolvable +- e648e8e: added main field to packages known to lack one + ## 1.0.1 ### Patch Changes diff --git a/packages/form-generator/package.json b/packages/form-generator/package.json index 37703ba852..31a5652720 100644 --- a/packages/form-generator/package.json +++ b/packages/form-generator/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/form-generator", - "version": "1.0.1", + "version": "1.0.3", "type": "module", "publishConfig": { "access": "public" diff --git a/packages/form-generator/src/local_codegen_graphql_form_generator.ts b/packages/form-generator/src/local_codegen_graphql_form_generator.ts index 9f98f91943..1d268581bf 100644 --- a/packages/form-generator/src/local_codegen_graphql_form_generator.ts +++ b/packages/form-generator/src/local_codegen_graphql_form_generator.ts @@ -222,11 +222,13 @@ export class LocalGraphqlFormGenerator implements GraphqlFormGenerator { ([key]) => key.toLowerCase() === model.toLowerCase() ); if (!entry) { + // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors throw new Error(`Could not find specified model ${model}`); } prev.push(entry); return prev; } + // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors throw new Error(`Could not find specified model ${model}`); }, [] diff --git a/packages/form-generator/src/s3_string_object_fetcher.ts b/packages/form-generator/src/s3_string_object_fetcher.ts index 0b29d0e6c2..3ae3759592 100644 --- a/packages/form-generator/src/s3_string_object_fetcher.ts +++ b/packages/form-generator/src/s3_string_object_fetcher.ts @@ -19,6 +19,7 @@ export class S3StringObjectFetcher { ); const schema = await getSchemaCommandResult.Body?.transformToString(); if (!schema) { + // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors throw new Error('Error on parsing output schema'); } return schema; diff --git a/packages/integration-tests/CHANGELOG.md b/packages/integration-tests/CHANGELOG.md index 528477bdbc..e06cf49a9d 100644 --- a/packages/integration-tests/CHANGELOG.md +++ b/packages/integration-tests/CHANGELOG.md @@ -1,5 +1,27 @@ # @aws-amplify/integration-tests +## 0.6.0 + +### Minor Changes + +- 11d62fe: Add support for custom Lambda function email senders in Auth construct + +### Patch Changes + +- b56d344: update aws-cdk lib to ^2.158.0 + +## 0.5.10 + +### Patch Changes + +- d538ecc: add storage access rules to outputs + +## 0.5.9 + +### Patch Changes + +- 8dd7286: fixed errors in plugin-types and cli-core along with any extraneous dependencies in other packages + ## 0.5.8 ### Patch Changes diff --git a/packages/integration-tests/README.md b/packages/integration-tests/README.md index 780c9a1929..f5e3f4b709 100644 --- a/packages/integration-tests/README.md +++ b/packages/integration-tests/README.md @@ -17,14 +17,25 @@ or `npm run test:dir packages/integration-tests/lib/test-in-memory` (to run them The create-amplify e2e suite tests the first-time installation and setup of a new amplify backend project. To run this suite, run `npm run test:dir packages/integration-tests/lib/test-e2e/create_amplify.test.js` -## deployment tests +## deployment and sandbox tests -To run end-to-end deployment tests, credentials to an AWS account must be available on the machine. Any credentials that will be picked up by the +To run end-to-end deployment or sandbox tests, credentials to an AWS account must be available on the machine. Any credentials that will be picked up by the [default node credential provider](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html) should work. -This include setting environment variables for a default profile. +This includes setting environment variables for a default profile. -To run this suite, run -`npm run test:dir packages/integration-tests/lib/test-e2e/deployment.test.js` +To run deployment suite, run +`npm run test:dir packages/integration-tests/lib/test-e2e/deployment/*.deployment.test.js` + +To run sandbox suite, run +`npm run test:dir packages/integration-tests/lib/test-e2e/sandbox/*.sandbox.test.js` + +To run deployment or sandbox test for specific project, specify exact test file, for example +`npm run test:dir packages/integration-tests/lib/test-e2e/sandbox/data_storage_auth_with_triggers.sandbox.test.js` + +When working locally with sandbox tests, it is sometimes useful to retain deployment of test project to avoid full re-deployments while working +on single test project incrementally. To retain deployment set `AMPLIFY_BACKEND_TESTS_RETAIN_TEST_PROJECT_DEPLOYMENT` environment +variable to `true`. This flag disables project name randomization and deployment cleanup, so that subsequent runs of same test +target the same CFN stacks. This option is not available for deployment tests (hotswap is not going to work there anyway). ## backend-output tests diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index 60073eac05..7f394b28b8 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -1,24 +1,25 @@ { "name": "@aws-amplify/integration-tests", "private": true, - "version": "0.5.8", + "version": "0.6.0", "type": "module", "devDependencies": { "@apollo/client": "^3.10.1", - "@aws-amplify/ai-constructs": "^0.1.0", - "@aws-amplify/auth-construct": "^1.2.2", - "@aws-amplify/backend": "^1.2.1", - "@aws-amplify/backend-ai": "^0.1.0", - "@aws-amplify/backend-secret": "^1.0.1", - "@aws-amplify/client-config": "^1.1.3", + "@aws-amplify/ai-constructs": "^1.0.0", + "@aws-amplify/auth-construct": "^1.4.0", + "@aws-amplify/backend": "^1.6.0", + "@aws-amplify/backend-ai": "^1.0.0", + "@aws-amplify/backend-secret": "^1.1.4", + "@aws-amplify/client-config": "^1.5.1", "@aws-amplify/data-schema": "^1.0.0", - "@aws-amplify/deployed-backend-client": "^1.3.0", + "@aws-amplify/deployed-backend-client": "^1.4.1", "@aws-amplify/platform-core": "^1.1.0", - "@aws-amplify/plugin-types": "^1.2.1", + "@aws-amplify/plugin-types": "^1.3.1", "@aws-sdk/client-accessanalyzer": "^3.624.0", "@aws-sdk/client-amplify": "^3.624.0", "@aws-sdk/client-bedrock-runtime": "^3.622.0", "@aws-sdk/client-cloudformation": "^3.624.0", + "@aws-sdk/client-cloudtrail": "^3.624.0", "@aws-sdk/client-cognito-identity": "^3.624.0", "@aws-sdk/client-cognito-identity-provider": "^3.624.0", "@aws-sdk/client-iam": "^3.624.0", @@ -29,9 +30,10 @@ "@aws-sdk/credential-providers": "^3.624.0", "@smithy/shared-ini-file-loader": "^2.2.5", "@types/lodash.ismatch": "^4.4.9", + "@zip.js/zip.js": "^2.7.52", "aws-amplify": "^6.0.16", "aws-appsync-auth-link": "^3.0.7", - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0", "execa": "^8.0.1", "fs-extra": "^11.1.1", diff --git a/packages/integration-tests/src/amplify_app_pool.ts b/packages/integration-tests/src/amplify_app_pool.ts index f3cf955e50..026a4625d0 100644 --- a/packages/integration-tests/src/amplify_app_pool.ts +++ b/packages/integration-tests/src/amplify_app_pool.ts @@ -13,6 +13,7 @@ import { } from '@aws-sdk/client-amplify'; import { shortUuid } from './short_uuid.js'; import { e2eToolingClientConfig } from './e2e_tooling_client_config.js'; +import { runWithRetry } from './retry.js'; export type TestBranch = { readonly appId: string; @@ -46,42 +47,46 @@ class DefaultAmplifyAppPool implements AmplifyAppPool { } fetchTestBranchDetails = async (testBranch: TestBranch): Promise => { - const branch = ( - await this.amplifyClient.send( - new GetBranchCommand({ - appId: testBranch.appId, - branchName: testBranch.branchName, - }) - ) - ).branch; - if (!branch) { - throw new Error( - `Failed to retrieve ${testBranch.branchName} branch of app ${testBranch.appId}` - ); - } - return branch; + return this.retryableOperation(async () => { + const branch = ( + await this.amplifyClient.send( + new GetBranchCommand({ + appId: testBranch.appId, + branchName: testBranch.branchName, + }) + ) + ).branch; + if (!branch) { + throw new Error( + `Failed to retrieve ${testBranch.branchName} branch of app ${testBranch.appId}` + ); + } + return branch; + }); }; createTestBranch = async (): Promise => { - const app = await this.getAppWithCapacity(); - const branch = ( - await this.amplifyClient.send( - new CreateBranchCommand({ - branchName: `${this.testBranchPrefix}${shortUuid()}`, + return this.retryableOperation(async () => { + const app = await this.getAppWithCapacity(); + const branch = ( + await this.amplifyClient.send( + new CreateBranchCommand({ + branchName: `${this.testBranchPrefix}${shortUuid()}`, + appId: app.appId, + }) + ) + ).branch; + if (app.appId && branch?.branchName) { + const testBranch: TestBranch = { appId: app.appId, - }) - ) - ).branch; - if (app.appId && branch?.branchName) { - const testBranch: TestBranch = { - appId: app.appId, - branchName: branch.branchName, - }; - this.branchesCreated.push(testBranch); - return testBranch; - } + branchName: branch.branchName, + }; + this.branchesCreated.push(testBranch); + return testBranch; + } - throw new Error('Unable to create branch'); + throw new Error('Unable to create branch'); + }); }; private listAllTestAmplifyApps = async (): Promise> => { @@ -173,6 +178,16 @@ class DefaultAmplifyAppPool implements AmplifyAppPool { } } }; + + private retryableOperation = (operation: () => Promise) => { + return runWithRetry(operation, (error) => { + // Add specific error conditions here that warrant a retry + return ( + error.message.includes('Unexpected token') || + error.message.includes('Bad control character') + ); + }); + }; } export const amplifyAppPool: AmplifyAppPool = new DefaultAmplifyAppPool( diff --git a/packages/integration-tests/src/amplify_auth_credentials_factory.ts b/packages/integration-tests/src/amplify_auth_credentials_factory.ts index a08b1455bd..d21f9f8d86 100644 --- a/packages/integration-tests/src/amplify_auth_credentials_factory.ts +++ b/packages/integration-tests/src/amplify_auth_credentials_factory.ts @@ -16,23 +16,24 @@ import { AsyncLock } from './async_lock.js'; * This class is safe to use in concurrent settings, i.e. tests running in parallel. */ export class AmplifyAuthCredentialsFactory { - private readonly userPoolId: string; - private readonly userPoolClientId: string; - private readonly identityPoolId: string; - private readonly allowGuestAccess: boolean | undefined; /** * Asynchronous lock is used to assure that all calls to Amplify JS library are * made in single transaction. This is because that library maintains global state, * for example auth session. */ - private readonly lock: AsyncLock = new AsyncLock(60 * 1000); + private static readonly lock: AsyncLock = new AsyncLock(60 * 1000); + + private readonly userPoolId: string; + private readonly userPoolClientId: string; + private readonly identityPoolId: string; + private readonly allowGuestAccess: boolean | undefined; /** * Creates Amplify Auth credentials factory. */ constructor( private readonly cognitoIdentityProviderClient: CognitoIdentityProviderClient, - authConfig: NonNullable['auth']> + authConfig: NonNullable['auth']> ) { if (!authConfig.identity_pool_id) { throw new Error('Client config must have identity pool id.'); @@ -47,7 +48,7 @@ export class AmplifyAuthCredentialsFactory { iamCredentials: IamCredentials; accessToken: string; }> => { - await this.lock.acquire(); + await AmplifyAuthCredentialsFactory.lock.acquire(); try { const username = `amplify-backend-${shortUuid()}@amazon.com`; const temporaryPassword = `Test1@Temp${shortUuid()}`; @@ -103,12 +104,12 @@ export class AmplifyAuthCredentialsFactory { accessToken: authSession.tokens.accessToken.toString(), }; } finally { - this.lock.release(); + AmplifyAuthCredentialsFactory.lock.release(); } }; getGuestAccessCredentials = async (): Promise => { - await this.lock.acquire(); + await AmplifyAuthCredentialsFactory.lock.acquire(); try { Amplify.configure({ Auth: { @@ -131,7 +132,7 @@ export class AmplifyAuthCredentialsFactory { return authSession.credentials; } finally { - this.lock.release(); + AmplifyAuthCredentialsFactory.lock.release(); } }; } diff --git a/packages/integration-tests/src/define_backend_template_harness.ts b/packages/integration-tests/src/define_backend_template_harness.ts index 455c8b0cf6..ae484e4b9e 100644 --- a/packages/integration-tests/src/define_backend_template_harness.ts +++ b/packages/integration-tests/src/define_backend_template_harness.ts @@ -66,10 +66,14 @@ const backendTemplatesCollector: SynthesizeBackendTemplates = < } as Partial<{ [K in keyof T]: Template }> & { root: Template }; for (const [key, resourceRecord] of Object.entries(backend)) { - // skip over the methods that we add on to the backend object + // skip over the properties and methods that we add on to the backend object if (typeof resourceRecord === 'function') { continue; } + // skip non-resource properties + if (!('resources' in resourceRecord)) { + continue; + } // find some construct in the resources exposed by the resourceRecord const firstConstruct = Object.values(resourceRecord.resources).find( (value) => value instanceof Construct diff --git a/packages/integration-tests/src/find_deployed_resource.ts b/packages/integration-tests/src/find_deployed_resource.ts index ef8830e680..933415ddd3 100644 --- a/packages/integration-tests/src/find_deployed_resource.ts +++ b/packages/integration-tests/src/find_deployed_resource.ts @@ -4,6 +4,7 @@ import { DescribeStackResourcesCommand, } from '@aws-sdk/client-cloudformation'; import { BackendIdentifierConversions } from '@aws-amplify/platform-core'; +import { e2eToolingClientConfig } from './e2e_tooling_client_config.js'; export type StringPredicate = (str: string) => boolean; @@ -14,7 +15,11 @@ export class DeployedResourcesFinder { /** * Construct with a cfnClient */ - constructor(private readonly cfnClient: CloudFormationClient) {} + constructor( + private readonly cfnClient: CloudFormationClient = new CloudFormationClient( + e2eToolingClientConfig + ) + ) {} /** * Find resources of type "resourceType" within the stack defined by "backendId" diff --git a/packages/integration-tests/src/package_manager_sanity_checks.test.ts b/packages/integration-tests/src/package_manager_sanity_checks.test.ts index cc90bcd05d..3375aee6e1 100644 --- a/packages/integration-tests/src/package_manager_sanity_checks.test.ts +++ b/packages/integration-tests/src/package_manager_sanity_checks.test.ts @@ -26,6 +26,7 @@ import { runWithPackageManager, } from './process-controller/process_controller.js'; import { amplifyAtTag } from './constants.js'; +import { RetryPredicates, runWithRetry } from './retry.js'; void describe('getting started happy path', async () => { let branchBackendIdentifier: BackendIdentifier; @@ -81,19 +82,22 @@ void describe('getting started happy path', async () => { if (packageManager === 'pnpm' && process.platform === 'win32') { return; } - if (packageManager === 'yarn-classic') { - await execa('yarn', ['add', 'create-amplify'], { cwd: tempDir }); - await execaCommand('./node_modules/.bin/create-amplify --yes --debug', { - cwd: tempDir, - env: { npm_config_user_agent: 'yarn/1.22.21' }, - }); - } else { - await runPackageManager( - packageManager, - ['create', amplifyAtTag, '--yes'], - tempDir - ).run(); - } + + await runWithRetry(async () => { + if (packageManager === 'yarn-classic') { + await execa('yarn', ['add', 'create-amplify'], { cwd: tempDir }); + await execaCommand('./node_modules/.bin/create-amplify --yes --debug', { + cwd: tempDir, + env: { npm_config_user_agent: 'yarn/1.22.21' }, + }); + } else { + await runPackageManager( + packageManager, + ['create', amplifyAtTag, '--yes'], + tempDir + ).run(); + } + }, RetryPredicates.createAmplifyRetryPredicate); const pathPrefix = path.join(tempDir, 'amplify'); diff --git a/packages/integration-tests/src/process-controller/execa_process_killer.ts b/packages/integration-tests/src/process-controller/execa_process_killer.ts index 0f55e04c6f..945b3ba38d 100644 --- a/packages/integration-tests/src/process-controller/execa_process_killer.ts +++ b/packages/integration-tests/src/process-controller/execa_process_killer.ts @@ -14,8 +14,20 @@ export const killExecaProcess = async (processInstance: ExecaChildProcess) => { // turns out killing child process on Windows is a huge PITA // https://stackoverflow.com/questions/23706055/why-can-i-not-kill-my-child-process-in-nodejs-on-windows // https://github.com/sindresorhus/execa#killsignal-options - // eslint-disable-next-line spellcheck/spell-checker - await execa('taskkill', ['/pid', `${processInstance.pid}`, '/f', '/t']); + try { + // eslint-disable-next-line spellcheck/spell-checker + await execa('taskkill', ['/pid', `${processInstance.pid}`, '/f', '/t']); + } catch (e) { + // if process doesn't exist it means that it managed to exit gracefully by now. + // so don't fail in that case. + const isProcessNotFoundError = + e instanceof Error && + (e.message.includes('not found') || + e.message.includes('There is no running instance of the task')); + if (!isProcessNotFoundError) { + throw e; + } + } } else { processInstance.kill('SIGINT'); } diff --git a/packages/integration-tests/src/process-controller/predicated_action_macros.ts b/packages/integration-tests/src/process-controller/predicated_action_macros.ts index 7f38d4789b..4fff93a726 100644 --- a/packages/integration-tests/src/process-controller/predicated_action_macros.ts +++ b/packages/integration-tests/src/process-controller/predicated_action_macros.ts @@ -40,16 +40,6 @@ export const confirmDeleteSandbox = () => ) .sendYes(); -/** - * Reusable predicated action: Wait for sandbox to prompt on quitting to delete all the resource and respond with no - */ -export const rejectCleanupSandbox = () => - new PredicatedActionBuilder() - .waitForLineIncludes( - 'Would you like to delete all the resources in your sandbox environment' - ) - .sendNo(); - /** * Reusable predicated action: Wait for sandbox to become idle, * then perform the specified file replacements in the backend code which will trigger sandbox again @@ -59,9 +49,10 @@ export const replaceFiles = (replacements: CopyDefinition[]) => { }; /** - * Reusable predicated action: Wait for sandbox to become idle and then quit it (CTRL-C) + * Reusable predicated action: Wait for sandbox to become idle and config to be generated and then quit it (CTRL-C) */ -export const interruptSandbox = () => waitForSandboxToBecomeIdle().sendCtrlC(); +export const interruptSandbox = () => + waitForConfigUpdateAfterDeployment().sendCtrlC(); /** * Reusable predicated action: Wait for sandbox to finish deployment and assert that the deployment time is less diff --git a/packages/integration-tests/src/resource-creation/auth_resource_creator.ts b/packages/integration-tests/src/resource-creation/auth_resource_creator.ts new file mode 100644 index 0000000000..da405b6bbe --- /dev/null +++ b/packages/integration-tests/src/resource-creation/auth_resource_creator.ts @@ -0,0 +1,372 @@ +import { + CognitoIdentityProviderClient, + CreateGroupCommand, + CreateGroupCommandInput, + CreateIdentityProviderCommand, + CreateIdentityProviderCommandInput, + CreateUserPoolClientCommand, + CreateUserPoolClientCommandInput, + CreateUserPoolCommand, + CreateUserPoolCommandInput, + CreateUserPoolDomainCommand, + CreateUserPoolDomainCommandInput, + DeleteGroupCommand, + DeleteIdentityProviderCommand, + DeleteUserPoolClientCommand, + DeleteUserPoolCommand, + DeleteUserPoolDomainCommand, +} from '@aws-sdk/client-cognito-identity-provider'; +import { + CreateRoleCommand, + CreateRoleCommandInput, + DeleteRoleCommand, + IAMClient, +} from '@aws-sdk/client-iam'; +import { + CognitoIdentityClient, + CreateIdentityPoolCommand, + CreateIdentityPoolCommandInput, + DeleteIdentityPoolCommand, + SetIdentityPoolRolesCommand, +} from '@aws-sdk/client-cognito-identity'; +import { shortUuid } from '../short_uuid.js'; +import { e2eToolingClientConfig } from '../e2e_tooling_client_config.js'; +const TEST_AMPLIFY_RESOURCE_PREFIX = 'amplify-'; + +type CleanupTask = { + run: () => Promise; + arn?: string | undefined; + id?: string | undefined; +}; +/** + * Provides a way to create auth resources using aws sdk + */ +export class AuthResourceCreator { + private cleanup: CleanupTask[] = []; + + /** + * Setup a new auth resource creator + * @param cognitoIdentityProviderClient client + * @param cognitoIdentityClient client + * @param iamClient client + */ + constructor( + private cognitoIdentityProviderClient: CognitoIdentityProviderClient = new CognitoIdentityProviderClient( + e2eToolingClientConfig + ), + private cognitoIdentityClient: CognitoIdentityClient = new CognitoIdentityClient( + e2eToolingClientConfig + ), + private iamClient: IAMClient = new IAMClient(e2eToolingClientConfig), + private createResourceNameSuffix: () => string = shortUuid + ) {} + + cleanupResources = async () => { + // delete in reverse order + const list = this.cleanup.map((t) => t.arn ?? t.id); + console.log( + `Attempting to delete a total of ${this.cleanup.length} resources` + ); + console.log('Resource descriptions/ARNs/IDs:', list); + const failedTasks: CleanupTask[] = []; + for (let i = this.cleanup.length - 1; i >= 0; i--) { + const task = this.cleanup[i]; + try { + await task.run(); + console.log(`Deleted: ${task.arn ?? task.id}`); + } catch (e) { + failedTasks.push(task); + console.error(`Failed to delete resource: ${task.arn ?? task.id}`, e); + } + } + console.error( + 'Failed tasks:', + failedTasks.map((t) => t.arn ?? t.id) + ); + }; + + createUserPoolBase = async (props: CreateUserPoolCommandInput) => { + const result = await this.cognitoIdentityProviderClient.send( + new CreateUserPoolCommand({ + ...props, + PoolName: `${TEST_AMPLIFY_RESOURCE_PREFIX}${ + props.PoolName + }-${this.createResourceNameSuffix()}`, + }) + ); + const userPool = result.UserPool; + if (!userPool) { + throw new Error('Failed to create user pool.'); + } + this.cleanup.push({ + run: async () => { + await this.cognitoIdentityProviderClient.send( + new DeleteUserPoolCommand({ UserPoolId: userPool.Id }) + ); + }, + arn: userPool.Arn, + }); + return userPool; + }; + + createUserPoolClientBase = async ( + props: CreateUserPoolClientCommandInput + ) => { + const result = await this.cognitoIdentityProviderClient.send( + new CreateUserPoolClientCommand({ + ...props, + ClientName: `${TEST_AMPLIFY_RESOURCE_PREFIX}${ + props.ClientName + }-${this.createResourceNameSuffix()}`, + }) + ); + const client = result.UserPoolClient; + if (!client) { + throw new Error('Failed to create user pool client.'); + } + this.cleanup.push({ + run: async () => { + await this.cognitoIdentityProviderClient.send( + new DeleteUserPoolClientCommand({ + ClientId: client.ClientId, + UserPoolId: client.UserPoolId, + }) + ); + }, + id: `UserPoolClientId: ${client.ClientId}`, + }); + return client; + }; + + createUserPoolDomainBase = async ( + props: CreateUserPoolDomainCommandInput + ) => { + const domain = `${TEST_AMPLIFY_RESOURCE_PREFIX}${ + props.Domain + }-${this.createResourceNameSuffix()}`; + await this.cognitoIdentityProviderClient.send( + new CreateUserPoolDomainCommand({ + ...props, + Domain: domain, + }) + ); + // if it didn't throw, domain was created. + this.cleanup.push({ + run: async () => { + await this.cognitoIdentityProviderClient.send( + new DeleteUserPoolDomainCommand({ + Domain: domain, + UserPoolId: props.UserPoolId, + }) + ); + }, + id: `Domain: ${domain}`, + }); + return domain; + }; + + createIdentityProviderBase = async ( + props: CreateIdentityProviderCommandInput + ) => { + const result = await this.cognitoIdentityProviderClient.send( + new CreateIdentityProviderCommand({ + ...props, + }) + ); + const provider = result.IdentityProvider; + if (!provider) { + throw new Error( + `An error occurred while creating the identity provider ${props.ProviderName}` + ); + } + this.cleanup.push({ + run: async () => { + await this.cognitoIdentityProviderClient.send( + new DeleteIdentityProviderCommand({ + UserPoolId: props.UserPoolId, + ProviderName: provider.ProviderName, + }) + ); + }, + id: `Provider: ${provider.ProviderName}`, + }); + return provider; + }; + + createIdentityPoolBase = async (props: CreateIdentityPoolCommandInput) => { + const identityPoolResponse = await this.cognitoIdentityClient.send( + new CreateIdentityPoolCommand({ + ...props, + IdentityPoolName: `${TEST_AMPLIFY_RESOURCE_PREFIX}${ + props.IdentityPoolName + }-${this.createResourceNameSuffix()}`, + }) + ); + const identityPoolId = identityPoolResponse.IdentityPoolId; + if (!identityPoolId) { + throw new Error('An error occurred while creating the identity pool'); + } + this.cleanup.push({ + run: async () => { + await this.cognitoIdentityClient.send( + new DeleteIdentityPoolCommand({ IdentityPoolId: identityPoolId }) + ); + }, + id: `IdentityPool: ${identityPoolResponse.IdentityPoolId}`, + }); + return { + ...identityPoolResponse, + // the line below ensures that the type engine sees IdentityPoolId as string, not string | undefined. + IdentityPoolId: identityPoolId, + }; + }; + + createRoleBase = async (props: CreateRoleCommandInput) => { + const result = await this.iamClient.send( + new CreateRoleCommand({ + ...props, + RoleName: `${TEST_AMPLIFY_RESOURCE_PREFIX}${ + props.RoleName + }-${this.createResourceNameSuffix()}`, + }) + ); + const role = result.Role; + if (!role) { + throw new Error( + `An error occurred while creating the role: ${props.RoleName}` + ); + } + this.cleanup.push({ + run: async () => { + await this.iamClient.send( + new DeleteRoleCommand({ RoleName: role.RoleName }) + ); + }, + arn: role.Arn, + }); + return role; + }; + + createUserPoolGroupBase = async (props: CreateGroupCommandInput) => { + const result = await this.cognitoIdentityProviderClient.send( + new CreateGroupCommand({ + ...props, + GroupName: `${TEST_AMPLIFY_RESOURCE_PREFIX}${ + props.GroupName + }-${this.createResourceNameSuffix()}`, + }) + ); + const group = result.Group; + if (!group || !group.GroupName) { + throw new Error(`Error creating group with name: ${props.GroupName}`); + } + this.cleanup.push({ + run: async () => { + await this.cognitoIdentityProviderClient.send( + new DeleteGroupCommand({ + UserPoolId: props.UserPoolId, + GroupName: group.GroupName, + }) + ); + }, + id: `Group: ${group.GroupName}`, + }); + return group; + }; + + setupUserPoolGroup = async ( + groupName: string, + userPoolId: string, + identityPoolId: string + ) => { + const groupRole = await this.createRoleBase({ + RoleName: 'ref-auth-group-role', + AssumeRolePolicyDocument: this.getIdentityPoolAssumeRolePolicyDocument( + identityPoolId, + 'authenticated' + ), + }); + const group = await this.createUserPoolGroupBase({ + GroupName: groupName, + UserPoolId: userPoolId, + RoleArn: groupRole.Arn, + }); + return group; + }; + + /** + * Setup standard auth and unauth roles for an identity pool + * @param userPoolId user pool id + * @param userPoolClientId user pool client id + * @param identityPoolId identity pool id + * @returns auth and unauth roles + */ + setupIdentityPoolRoles = async ( + userPoolId: string, + userPoolClientId: string, + identityPoolId: string + ) => { + const authRole = await this.createRoleBase({ + RoleName: `ref-auth-role`, + AssumeRolePolicyDocument: this.getIdentityPoolAssumeRolePolicyDocument( + identityPoolId, + 'authenticated' + ), + }); + const unauthRole = await this.createRoleBase({ + RoleName: `ref-unauth-role`, + AssumeRolePolicyDocument: this.getIdentityPoolAssumeRolePolicyDocument( + identityPoolId, + 'unauthenticated' + ), + }); + const region = await this.cognitoIdentityClient.config.region(); + await this.cognitoIdentityClient.send( + new SetIdentityPoolRolesCommand({ + IdentityPoolId: identityPoolId, + Roles: { + unauthenticated: unauthRole.Arn!, + authenticated: authRole.Arn!, + }, + RoleMappings: { + [`cognito-idp.${region}.amazonaws.com/${userPoolId}:${userPoolClientId}`]: + { + Type: 'Token', + AmbiguousRoleResolution: 'AuthenticatedRole', + }, + }, + }) + ); + + return { + authRole, + unauthRole, + }; + }; + + private getIdentityPoolAssumeRolePolicyDocument = ( + identityPoolId: string, + roleType: 'authenticated' | 'unauthenticated' + ) => { + return `{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "cognito-identity.amazonaws.com" + }, + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "cognito-identity.amazonaws.com:aud": "${identityPoolId}" + }, + "ForAnyValue:StringLike": { + "cognito-identity.amazonaws.com:amr": "${roleType}" + } + } + } + ] + }`; + }; +} diff --git a/packages/integration-tests/src/retry.ts b/packages/integration-tests/src/retry.ts new file mode 100644 index 0000000000..713d3e37a3 --- /dev/null +++ b/packages/integration-tests/src/retry.ts @@ -0,0 +1,58 @@ +export type RetryPredicate = (error: Error) => boolean; + +/** + * Executes an asynchronous operation with retry logic. + * This function attempts to execute the provided callable function multiple times + * based on the specified retry conditions. It's useful for handling transient + * errors or temporary service unavailability. + */ +export const runWithRetry = async ( + callable: (attempt: number) => Promise, + retryPredicate: RetryPredicate, + maxAttempts = 3 +): Promise => { + const collectedErrors: Error[] = []; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const result = await callable(attempt); + return result; + } catch (error) { + if (error instanceof Error) { + collectedErrors.push(error); + if (!retryPredicate(error)) { + throw error; + } + } else { + // re-throw non-Error. + // This should never happen, but we should be aware if it does. + throw error; + } + } + } + + throw new AggregateError( + collectedErrors, + `All ${maxAttempts} attempts failed` + ); +}; + +/** + * Known retry predicates that repeat in multiple places. + */ +export class RetryPredicates { + static createAmplifyRetryPredicate: RetryPredicate = ( + error: Error + ): boolean => { + const message = error.message.toLowerCase(); + // Note: we can't assert on whole stdout or stderr because + // they're not always captured in the error due to settings we need for + // ProcessController to work. + const didProcessExitWithError = message.includes('exit code 1'); + const isKnownProcess = + (message.includes('yarn add') && message.includes('aws-amplify')) || + message.includes('npm create amplify') || + message.includes('pnpm create amplify'); + return didProcessExitWithError && isKnownProcess; + }; +} diff --git a/packages/integration-tests/src/setup_package_manager.ts b/packages/integration-tests/src/setup_package_manager.ts index e1b51622f0..ad72f3270f 100644 --- a/packages/integration-tests/src/setup_package_manager.ts +++ b/packages/integration-tests/src/setup_package_manager.ts @@ -39,6 +39,11 @@ const initializeYarnClassic = async (execaOptions: { ['config', 'set', 'registry', customRegistry], execaOptions ); + await execa( + packageManager, + ['config', 'set', 'network-timeout', '60000'], + execaOptions + ); await execa(packageManager, ['config', 'get', 'registry'], execaOptions); await execa(packageManager, ['cache', 'clean'], execaOptions); }; diff --git a/packages/integration-tests/src/setup_test_directory.ts b/packages/integration-tests/src/setup_test_directory.ts index 0939e5709c..8cddf8b2cf 100644 --- a/packages/integration-tests/src/setup_test_directory.ts +++ b/packages/integration-tests/src/setup_test_directory.ts @@ -22,6 +22,13 @@ export const createTestDirectory = async (pathName: string | URL) => { * Delete a test directory. */ export const deleteTestDirectory = async (pathName: string | URL) => { + if (process.env.CI) { + // We don't have to delete test directories in CI. + // The VMs are ephemeral. + // On the other hand we want to keep shared parent directories for test projects + // for tests executing in parallel on the same VM. + return; + } if (existsSync(pathName)) { await fs.rm(pathName, { recursive: true, force: true }); } diff --git a/packages/integration-tests/src/test-e2e/amplify_outputs_backwards_compatibility.test.ts b/packages/integration-tests/src/test-e2e/amplify_outputs_backwards_compatibility.test.ts new file mode 100644 index 0000000000..bdbedc2988 --- /dev/null +++ b/packages/integration-tests/src/test-e2e/amplify_outputs_backwards_compatibility.test.ts @@ -0,0 +1,212 @@ +import { after, before, describe, it } from 'node:test'; +import { execa } from 'execa'; +import path from 'path'; +import { TestBranch, amplifyAppPool } from '../amplify_app_pool.js'; +import { BackendIdentifier } from '@aws-amplify/plugin-types'; +import { + CloudFormationClient, + DeleteStackCommand, +} from '@aws-sdk/client-cloudformation'; +import fsp from 'fs/promises'; +import { e2eToolingClientConfig } from '../e2e_tooling_client_config.js'; +import { NpmProxyController } from '../npm_proxy_controller.js'; +import assert from 'assert'; +import os from 'os'; +import { generateClientConfig } from '@aws-amplify/client-config'; +import { BackendIdentifierConversions } from '@aws-amplify/platform-core'; +import { amplifyAtTag } from '../constants.js'; + +void describe('client config backwards compatibility', () => { + let branchBackendIdentifier: BackendIdentifier; + let testBranch: TestBranch; + let cfnClient: CloudFormationClient; + let tempDir: string; + let baselineDir: string; + let baselineNpmProxyController: NpmProxyController; + let currentNpmProxyController: NpmProxyController; + + before(async () => { + assert.ok( + process.env.BASELINE_DIR, + 'BASELINE_DIR environment variable must be set and point to amplify-backend repo at baseline version' + ); + baselineDir = process.env.BASELINE_DIR; + + tempDir = await fsp.mkdtemp( + path.join(os.tmpdir(), 'test-amplify-outputs-backwards-compatibility') + ); + + console.log(`Temp dir is ${tempDir}`); + + cfnClient = new CloudFormationClient(e2eToolingClientConfig); + baselineNpmProxyController = new NpmProxyController(baselineDir); + currentNpmProxyController = new NpmProxyController(); + testBranch = await amplifyAppPool.createTestBranch(); + branchBackendIdentifier = { + namespace: testBranch.appId, + name: testBranch.branchName, + type: 'branch', + }; + }); + + after(async () => { + await cfnClient.send( + new DeleteStackCommand({ + StackName: BackendIdentifierConversions.toStackName( + branchBackendIdentifier + ), + }) + ); + await fsp.rm(tempDir, { recursive: true }); + + await baselineNpmProxyController.tearDown(); + await currentNpmProxyController.tearDown(); + }); + + const deploy = async (): Promise => { + await execa( + 'npx', + [ + 'ampx', + 'pipeline-deploy', + '--branch', + branchBackendIdentifier.name, + '--appId', + branchBackendIdentifier.namespace, + ], + { + cwd: tempDir, + stdio: 'inherit', + env: { + CI: 'true', + }, + } + ); + }; + + const reinstallDependencies = async (): Promise => { + await fsp.rm(path.join(tempDir, 'node_modules'), { + recursive: true, + force: true, + }); + await fsp.unlink(path.join(tempDir, 'package-lock.json')); + + await execa('npm', ['install'], { + cwd: tempDir, + stdio: 'inherit', + }); + }; + + const assertGenerateClientConfigAPI = async ( + type: 'baseline' | 'current' + ) => { + try { + assert.ok( + await generateClientConfig(branchBackendIdentifier, '1'), + `outputs v1 failed to be generated for an app created with ${type} library version` + ); + } catch (e) { + throw new Error( + `outputs v1 failed to be generated for an app created with ${type} library version. Error: ${JSON.stringify( + e + )}` + ); + } + try { + assert.ok( + await generateClientConfig(branchBackendIdentifier, '1.1'), + `outputs v1.1 failed to be generated for an app created with ${type} library version` + ); + } catch (e) { + throw new Error( + `outputs v1.1 failed to be generated for an app created with ${type} library version. Error: ${JSON.stringify( + e + )}` + ); + } + }; + + const assertGenerateClientConfigCommand = async ( + type: 'baseline' | 'current' + ) => { + await execa( + 'npx', + [ + 'ampx', + 'generate', + 'outputs', + '--stack', + BackendIdentifierConversions.toStackName(branchBackendIdentifier), + ], + { + cwd: tempDir, + stdio: 'inherit', + } + ); + + const fileSize = ( + await fsp.stat(path.join(tempDir, 'amplify_outputs.json')) + ).size; + assert.ok( + fileSize > 100, // Validate that it's not just a shim + `outputs file should not be empty when generating for a ${ + type === 'baseline' ? 'new' : 'old' + } new app with the ${type} version` + ); + }; + + void it('outputs generation should be backwards and forward compatible', async () => { + // build an app using previous (baseline) version + await baselineNpmProxyController.setUp(); + await execa('npm', ['create', amplifyAtTag, '--yes'], { + cwd: tempDir, + stdio: 'inherit', + }); + + // Replace backend.ts to add custom outputs without version as well. + await fsp.writeFile( + path.join(tempDir, 'amplify', 'backend.ts'), + `import { defineBackend } from '@aws-amplify/backend'; +import { auth } from './auth/resource'; +import { data } from './data/resource'; + +const backend = defineBackend({ + auth, + data, +}); + +backend.addOutput({ + custom: { + someCustomOutput: 'someCustomOutputValue', + }, +}); +` + ); + await deploy(); + await baselineNpmProxyController.tearDown(); + + // Generate the outputs using the current version for apps built with baseline version + + // 1. via CLI command + await currentNpmProxyController.setUp(); + await reinstallDependencies(); + await assertGenerateClientConfigCommand('current'); + + // 2. via API. + await assertGenerateClientConfigAPI('current'); + + // Re-deploy the app using the current version now + await deploy(); + + // Generate the outputs using the baseline version for apps built with current version + + // 1. via CLI command + await currentNpmProxyController.tearDown(); + await baselineNpmProxyController.setUp(); + await reinstallDependencies(); + await assertGenerateClientConfigCommand('baseline'); + + // 2. via API. + await assertGenerateClientConfigAPI('baseline'); + }); +}); diff --git a/packages/integration-tests/src/test-e2e/backend_output.test.ts b/packages/integration-tests/src/test-e2e/backend_output.test.ts index 88397deba4..8f8d5c6f2d 100644 --- a/packages/integration-tests/src/test-e2e/backend_output.test.ts +++ b/packages/integration-tests/src/test-e2e/backend_output.test.ts @@ -21,8 +21,13 @@ import { S3Client } from '@aws-sdk/client-s3'; import { IAMClient } from '@aws-sdk/client-iam'; import { DeployedResourcesFinder } from '../find_deployed_resource.js'; import { DataStorageAuthWithTriggerTestProjectCreator } from '../test-project-setup/data_storage_auth_with_triggers.js'; -import { SQSClient } from '@aws-sdk/client-sqs'; import { setupDeployedBackendClient } from '../test-project-setup/setup_deployed_backend_client.js'; +import { CloudTrailClient } from '@aws-sdk/client-cloudtrail'; + +/** + * This E2E test is to check whether current (aka latest) repository content introduces breaking changes + * for our deployed backend client to read outputs. + */ // Different root test dir to avoid race conditions with e2e deployment tests const rootTestDir = fileURLToPath( @@ -34,12 +39,12 @@ void describe( { concurrency: testConcurrencyLevel }, () => { const cfnClient = new CloudFormationClient(e2eToolingClientConfig); + const cloudTrailClient = new CloudTrailClient(e2eToolingClientConfig); const amplifyClient = new AmplifyClient(e2eToolingClientConfig); const secretClient = getSecretClient(e2eToolingClientConfig); const lambdaClient = new LambdaClient(e2eToolingClientConfig); const s3Client = new S3Client(e2eToolingClientConfig); const iamClient = new IAMClient(e2eToolingClientConfig); - const sqsClient = new SQSClient(e2eToolingClientConfig); const resourceFinder = new DeployedResourcesFinder(cfnClient); const dataStorageAuthWithTriggerTestProjectCreator = new DataStorageAuthWithTriggerTestProjectCreator( @@ -49,7 +54,7 @@ void describe( lambdaClient, s3Client, iamClient, - sqsClient, + cloudTrailClient, resourceFinder ); @@ -83,7 +88,6 @@ void describe( await testProject.deploy(branchBackendIdentifier, sharedSecretsEnv); await testProject.assertPostDeployment(branchBackendIdentifier); - await testProject.assertDeployedClientOutputs(branchBackendIdentifier); }); } diff --git a/packages/integration-tests/src/test-e2e/create_amplify.test.ts b/packages/integration-tests/src/test-e2e/create_amplify.test.ts index 189405ef6f..b7b3c1333a 100644 --- a/packages/integration-tests/src/test-e2e/create_amplify.test.ts +++ b/packages/integration-tests/src/test-e2e/create_amplify.test.ts @@ -9,6 +9,7 @@ import { testConcurrencyLevel } from './test_concurrency.js'; import { findBaselineCdkVersion } from '../cdk_version_finder.js'; import { amplifyAtTag } from '../constants.js'; import { NpmProxyController } from '../npm_proxy_controller.js'; +import { RetryPredicates, runWithRetry } from '../retry.js'; void describe( 'create-amplify script', @@ -78,14 +79,16 @@ void describe( ); } - await execa( - 'npm', - ['create', amplifyAtTag, '--yes', '--', '--debug'], - { - cwd: tempDir, - stdio: 'inherit', - } - ); + await runWithRetry(async () => { + await execa( + 'npm', + ['create', amplifyAtTag, '--yes', '--', '--debug'], + { + cwd: tempDir, + stdio: 'inherit', + } + ); + }, RetryPredicates.createAmplifyRetryPredicate); // Override CDK installation with baseline version await execa( diff --git a/packages/integration-tests/src/test-e2e/deployment.test.ts b/packages/integration-tests/src/test-e2e/deployment.test.ts deleted file mode 100644 index c9ea7500de..0000000000 --- a/packages/integration-tests/src/test-e2e/deployment.test.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { after, afterEach, before, beforeEach, describe, it } from 'node:test'; -import { - createTestDirectory, - deleteTestDirectory, - rootTestDir, -} from '../setup_test_directory.js'; -import fs from 'fs/promises'; -import { shortUuid } from '../short_uuid.js'; -import { getTestProjectCreators } from '../test-project-setup/test_project_creator.js'; -import { TestProjectBase } from '../test-project-setup/test_project_base.js'; -import { PredicatedActionBuilder } from '../process-controller/predicated_action_queue_builder.js'; -import { ampxCli } from '../process-controller/process_controller.js'; -import path from 'path'; -import { - interruptSandbox, - rejectCleanupSandbox, -} from '../process-controller/predicated_action_macros.js'; -import assert from 'node:assert'; -import { TestBranch, amplifyAppPool } from '../amplify_app_pool.js'; -import { BackendIdentifier } from '@aws-amplify/plugin-types'; -import { ClientConfigFormat } from '@aws-amplify/client-config'; -import { testConcurrencyLevel } from './test_concurrency.js'; -import { TestCdkProjectBase } from '../test-project-setup/cdk/test_cdk_project_base.js'; -import { getTestCdkProjectCreators } from '../test-project-setup/cdk/test_cdk_project_creator.js'; - -const testProjectCreators = getTestProjectCreators(); -const testCdkProjectCreators = getTestCdkProjectCreators(); -void describe('deployment tests', { concurrency: testConcurrencyLevel }, () => { - before(async () => { - await createTestDirectory(rootTestDir); - }); - after(async () => { - await deleteTestDirectory(rootTestDir); - }); - - void describe('amplify deploys', async () => { - testProjectCreators.forEach((testProjectCreator) => { - void describe(`branch deploys ${testProjectCreator.name}`, () => { - let branchBackendIdentifier: BackendIdentifier; - let testBranch: TestBranch; - let testProject: TestProjectBase; - - beforeEach(async () => { - testProject = await testProjectCreator.createProject(rootTestDir); - testBranch = await amplifyAppPool.createTestBranch(); - branchBackendIdentifier = { - namespace: testBranch.appId, - name: testBranch.branchName, - type: 'branch', - }; - }); - - afterEach(async () => { - await testProject.tearDown(branchBackendIdentifier); - }); - - void it(`[${testProjectCreator.name}] deploys fully`, async () => { - await testProject.deploy(branchBackendIdentifier); - await testProject.assertPostDeployment(branchBackendIdentifier); - const testBranchDetails = await amplifyAppPool.fetchTestBranchDetails( - testBranch - ); - assert.ok( - testBranchDetails.backend?.stackArn, - 'branch should have stack associated' - ); - assert.ok( - testBranchDetails.backend?.stackArn?.includes( - branchBackendIdentifier.namespace - ) - ); - assert.ok( - testBranchDetails.backend?.stackArn?.includes( - branchBackendIdentifier.name - ) - ); - - // test generating all client formats - for (const format of [ - ClientConfigFormat.DART, - ClientConfigFormat.JSON, - ]) { - await ampxCli( - [ - 'generate', - 'outputs', - '--branch', - testBranch.branchName, - '--app-id', - testBranch.appId, - '--format', - format, - ], - testProject.projectDirPath - ).run(); - - await testProject.assertClientConfigExists( - testProject.projectDirPath, - format - ); - } - }); - }); - }); - - void describe('fails on compilation error', async () => { - let testProject: TestProjectBase; - before(async () => { - // any project is fine - testProject = await testProjectCreators[0].createProject(rootTestDir); - await fs.cp( - testProject.sourceProjectAmplifyDirURL, - testProject.projectAmplifyDirPath, - { - recursive: true, - } - ); - - // inject failure - await fs.appendFile( - path.join(testProject.projectAmplifyDirPath, 'backend.ts'), - "this won't compile" - ); - }); - - void describe('in sequence', { concurrency: false }, () => { - void it('in sandbox deploy', async () => { - await ampxCli( - ['sandbox', '--dirToWatch', 'amplify'], - testProject.projectDirPath - ) - .do( - new PredicatedActionBuilder().waitForLineIncludes( - 'TypeScript validation check failed' - ) - ) - .do(interruptSandbox()) - .do(rejectCleanupSandbox()) - .run(); - }); - - void it('in pipeline deploy', async () => { - await assert.rejects(() => - ampxCli( - [ - 'pipeline-deploy', - '--branch', - 'test-branch', - '--app-id', - `test-${shortUuid()}`, - ], - testProject.projectDirPath, - { - env: { CI: 'true' }, - } - ) - .do( - new PredicatedActionBuilder().waitForLineIncludes( - 'TypeScript validation check failed' - ) - ) - .run() - ); - }); - }); - }); - }); - - void describe('cdk deploys', () => { - testCdkProjectCreators.forEach((testCdkProjectCreator) => { - void describe(`${testCdkProjectCreator.name}`, () => { - let testCdkProject: TestCdkProjectBase; - - beforeEach(async () => { - testCdkProject = await testCdkProjectCreator.createProject( - rootTestDir - ); - }); - - afterEach(async () => { - await testCdkProject.tearDown(); - }); - - void it(`deploys`, async () => { - await testCdkProject.deploy(); - await testCdkProject.assertPostDeployment(); - }); - }); - }); - }); -}); diff --git a/packages/integration-tests/src/test-e2e/deployment/access_testing_project.deployment.test.ts b/packages/integration-tests/src/test-e2e/deployment/access_testing_project.deployment.test.ts new file mode 100644 index 0000000000..6730123191 --- /dev/null +++ b/packages/integration-tests/src/test-e2e/deployment/access_testing_project.deployment.test.ts @@ -0,0 +1,4 @@ +import { AccessTestingProjectTestProjectCreator } from '../../test-project-setup/access_testing_project.js'; +import { defineDeploymentTest } from './deployment.test.template.js'; + +defineDeploymentTest(new AccessTestingProjectTestProjectCreator()); diff --git a/packages/integration-tests/src/test-e2e/deployment/advanced_auth_and_functions.deployment.test.ts b/packages/integration-tests/src/test-e2e/deployment/advanced_auth_and_functions.deployment.test.ts new file mode 100644 index 0000000000..b988eb72c2 --- /dev/null +++ b/packages/integration-tests/src/test-e2e/deployment/advanced_auth_and_functions.deployment.test.ts @@ -0,0 +1,4 @@ +import { defineDeploymentTest } from './deployment.test.template.js'; +import { AdvancedAuthAndFunctionsTestProjectCreator } from '../../test-project-setup/advanced_auth_and_functions.js'; + +defineDeploymentTest(new AdvancedAuthAndFunctionsTestProjectCreator()); diff --git a/packages/integration-tests/src/test-e2e/deployment/auth_cdk_project.deployment.test.ts b/packages/integration-tests/src/test-e2e/deployment/auth_cdk_project.deployment.test.ts new file mode 100644 index 0000000000..2b6f324624 --- /dev/null +++ b/packages/integration-tests/src/test-e2e/deployment/auth_cdk_project.deployment.test.ts @@ -0,0 +1,4 @@ +import { defineCdkDeploymentTest } from './cdk.deployment.test.template.js'; +import { AuthTestCdkProjectCreator } from '../../test-project-setup/cdk/auth_cdk_project.js'; + +defineCdkDeploymentTest(new AuthTestCdkProjectCreator()); diff --git a/packages/integration-tests/src/test-e2e/deployment/cdk.deployment.test.template.ts b/packages/integration-tests/src/test-e2e/deployment/cdk.deployment.test.template.ts new file mode 100644 index 0000000000..1bfa79ce14 --- /dev/null +++ b/packages/integration-tests/src/test-e2e/deployment/cdk.deployment.test.template.ts @@ -0,0 +1,50 @@ +import { after, afterEach, before, beforeEach, describe, it } from 'node:test'; +import { + createTestDirectory, + deleteTestDirectory, + rootTestDir, +} from '../../setup_test_directory.js'; +import { testConcurrencyLevel } from '../test_concurrency.js'; +import { TestCdkProjectBase } from '../../test-project-setup/cdk/test_cdk_project_base.js'; +import { TestCdkProjectCreator } from '../../test-project-setup/cdk/test_cdk_project_creator.js'; + +/** + * Defines cdk deployment test + */ +export const defineCdkDeploymentTest = ( + testCdkProjectCreator: TestCdkProjectCreator +) => { + void describe( + 'cdk deployment tests', + { concurrency: testConcurrencyLevel }, + () => { + before(async () => { + await createTestDirectory(rootTestDir); + }); + after(async () => { + await deleteTestDirectory(rootTestDir); + }); + + void describe('cdk deploys', () => { + void describe(`${testCdkProjectCreator.name}`, () => { + let testCdkProject: TestCdkProjectBase; + + beforeEach(async () => { + testCdkProject = await testCdkProjectCreator.createProject( + rootTestDir + ); + }); + + afterEach(async () => { + await testCdkProject.tearDown(); + }); + + void it(`deploys`, async () => { + await testCdkProject.deploy(); + await testCdkProject.assertPostDeployment(); + }); + }); + }); + } + ); +}; diff --git a/packages/integration-tests/src/test-e2e/deployment/circular_dep_auth_data_func.deployment.test.ts b/packages/integration-tests/src/test-e2e/deployment/circular_dep_auth_data_func.deployment.test.ts new file mode 100644 index 0000000000..88f36000bf --- /dev/null +++ b/packages/integration-tests/src/test-e2e/deployment/circular_dep_auth_data_func.deployment.test.ts @@ -0,0 +1,4 @@ +import { CircularDepAuthDataFuncTestProjectCreator } from '../../test-project-setup/circular_dep_auth_data_func.js'; +import { defineDeploymentTest } from './deployment.test.template.js'; + +defineDeploymentTest(new CircularDepAuthDataFuncTestProjectCreator()); diff --git a/packages/integration-tests/src/test-e2e/deployment/circular_dep_data_func.deployment.test.ts b/packages/integration-tests/src/test-e2e/deployment/circular_dep_data_func.deployment.test.ts new file mode 100644 index 0000000000..8725ac07c6 --- /dev/null +++ b/packages/integration-tests/src/test-e2e/deployment/circular_dep_data_func.deployment.test.ts @@ -0,0 +1,4 @@ +import { CircularDepDataFuncTestProjectCreator } from '../../test-project-setup/circular_dep_data_func.js'; +import { defineDeploymentTest } from './deployment.test.template.js'; + +defineDeploymentTest(new CircularDepDataFuncTestProjectCreator()); diff --git a/packages/integration-tests/src/test-e2e/deployment/conversation_handler_project.deployment.test.ts b/packages/integration-tests/src/test-e2e/deployment/conversation_handler_project.deployment.test.ts new file mode 100644 index 0000000000..b26f10daea --- /dev/null +++ b/packages/integration-tests/src/test-e2e/deployment/conversation_handler_project.deployment.test.ts @@ -0,0 +1,4 @@ +import { ConversationHandlerTestProjectCreator } from '../../test-project-setup/conversation_handler_project.js'; +import { defineDeploymentTest } from './deployment.test.template.js'; + +defineDeploymentTest(new ConversationHandlerTestProjectCreator()); diff --git a/packages/integration-tests/src/test-e2e/deployment/custom_outputs.deployment.test.ts b/packages/integration-tests/src/test-e2e/deployment/custom_outputs.deployment.test.ts new file mode 100644 index 0000000000..4654bac4f4 --- /dev/null +++ b/packages/integration-tests/src/test-e2e/deployment/custom_outputs.deployment.test.ts @@ -0,0 +1,4 @@ +import { CustomOutputsTestProjectCreator } from '../../test-project-setup/custom_outputs.js'; +import { defineDeploymentTest } from './deployment.test.template.js'; + +defineDeploymentTest(new CustomOutputsTestProjectCreator()); diff --git a/packages/integration-tests/src/test-e2e/deployment/data_storage_auth_with_triggers.deployment.test.ts b/packages/integration-tests/src/test-e2e/deployment/data_storage_auth_with_triggers.deployment.test.ts new file mode 100644 index 0000000000..0fd2ea5062 --- /dev/null +++ b/packages/integration-tests/src/test-e2e/deployment/data_storage_auth_with_triggers.deployment.test.ts @@ -0,0 +1,4 @@ +import { DataStorageAuthWithTriggerTestProjectCreator } from '../../test-project-setup/data_storage_auth_with_triggers.js'; +import { defineDeploymentTest } from './deployment.test.template.js'; + +defineDeploymentTest(new DataStorageAuthWithTriggerTestProjectCreator()); diff --git a/packages/integration-tests/src/test-e2e/deployment/deployment.test.template.ts b/packages/integration-tests/src/test-e2e/deployment/deployment.test.template.ts new file mode 100644 index 0000000000..4a17afb9b0 --- /dev/null +++ b/packages/integration-tests/src/test-e2e/deployment/deployment.test.template.ts @@ -0,0 +1,169 @@ +import { after, afterEach, before, beforeEach, describe, it } from 'node:test'; +import { + createTestDirectory, + deleteTestDirectory, + rootTestDir, +} from '../../setup_test_directory.js'; +import fs from 'fs/promises'; +import { shortUuid } from '../../short_uuid.js'; +import { TestProjectCreator } from '../../test-project-setup/test_project_creator.js'; +import { TestProjectBase } from '../../test-project-setup/test_project_base.js'; +import { PredicatedActionBuilder } from '../../process-controller/predicated_action_queue_builder.js'; +import { ampxCli } from '../../process-controller/process_controller.js'; +import path from 'path'; +import { waitForSandboxToBecomeIdle } from '../../process-controller/predicated_action_macros.js'; +import assert from 'node:assert'; +import { TestBranch, amplifyAppPool } from '../../amplify_app_pool.js'; +import { BackendIdentifier } from '@aws-amplify/plugin-types'; +import { ClientConfigFormat } from '@aws-amplify/client-config'; +import { testConcurrencyLevel } from '../test_concurrency.js'; + +/** + * Defines deployment test + */ +export const defineDeploymentTest = ( + testProjectCreator: TestProjectCreator +) => { + void describe( + 'deployment tests', + { concurrency: testConcurrencyLevel }, + () => { + before(async () => { + await createTestDirectory(rootTestDir); + }); + after(async () => { + await deleteTestDirectory(rootTestDir); + }); + + void describe(`branch deploys ${testProjectCreator.name}`, () => { + let branchBackendIdentifier: BackendIdentifier; + let testBranch: TestBranch; + let testProject: TestProjectBase; + + beforeEach(async () => { + testProject = await testProjectCreator.createProject(rootTestDir); + testBranch = await amplifyAppPool.createTestBranch(); + branchBackendIdentifier = { + namespace: testBranch.appId, + name: testBranch.branchName, + type: 'branch', + }; + }); + + afterEach(async () => { + await testProject.tearDown(branchBackendIdentifier); + }); + + void it(`[${testProjectCreator.name}] deploys fully`, async () => { + await testProject.deploy(branchBackendIdentifier); + await testProject.assertPostDeployment(branchBackendIdentifier); + const testBranchDetails = await amplifyAppPool.fetchTestBranchDetails( + testBranch + ); + assert.ok( + testBranchDetails.backend?.stackArn, + 'branch should have stack associated' + ); + assert.ok( + testBranchDetails.backend?.stackArn?.includes( + branchBackendIdentifier.namespace + ) + ); + assert.ok( + testBranchDetails.backend?.stackArn?.includes( + branchBackendIdentifier.name + ) + ); + + // test generating all client formats + for (const format of [ + ClientConfigFormat.DART, + ClientConfigFormat.JSON, + ]) { + await ampxCli( + [ + 'generate', + 'outputs', + '--branch', + testBranch.branchName, + '--app-id', + testBranch.appId, + '--format', + format, + ], + testProject.projectDirPath + ).run(); + + await testProject.assertClientConfigExists( + testProject.projectDirPath, + format + ); + } + }); + }); + + void describe('fails on compilation error', async () => { + let testProject: TestProjectBase; + before(async () => { + // any project is fine + testProject = await testProjectCreator.createProject(rootTestDir); + await fs.cp( + testProject.sourceProjectAmplifyDirURL, + testProject.projectAmplifyDirPath, + { + recursive: true, + } + ); + + // inject failure + await fs.appendFile( + path.join(testProject.projectAmplifyDirPath, 'backend.ts'), + "this won't compile" + ); + }); + + void describe('in sequence', { concurrency: false }, () => { + void it('in sandbox deploy', async () => { + const predicatedActionBuilder = new PredicatedActionBuilder(); + await ampxCli( + ['sandbox', '--dirToWatch', 'amplify'], + testProject.projectDirPath + ) + .do( + predicatedActionBuilder.waitForLineIncludes( + 'TypeScript validation check failed' + ) + ) + .do(waitForSandboxToBecomeIdle()) + .do(predicatedActionBuilder.sendCtrlC()) + .run(); + }); + + void it('in pipeline deploy', async () => { + await assert.rejects(() => + ampxCli( + [ + 'pipeline-deploy', + '--branch', + 'test-branch', + '--app-id', + `test-${shortUuid()}`, + ], + testProject.projectDirPath, + { + env: { CI: 'true' }, + } + ) + .do( + new PredicatedActionBuilder().waitForLineIncludes( + 'TypeScript validation check failed' + ) + ) + .run() + ); + }); + }); + }); + } + ); +}; diff --git a/packages/integration-tests/src/test-e2e/deployment/minimal_with_typescript_idioms.deployment.test.ts b/packages/integration-tests/src/test-e2e/deployment/minimal_with_typescript_idioms.deployment.test.ts new file mode 100644 index 0000000000..af3e619d9d --- /dev/null +++ b/packages/integration-tests/src/test-e2e/deployment/minimal_with_typescript_idioms.deployment.test.ts @@ -0,0 +1,4 @@ +import { MinimalWithTypescriptIdiomTestProjectCreator } from '../../test-project-setup/minimal_with_typescript_idioms.js'; +import { defineDeploymentTest } from './deployment.test.template.js'; + +defineDeploymentTest(new MinimalWithTypescriptIdiomTestProjectCreator()); diff --git a/packages/integration-tests/src/test-e2e/deployment/reference_auth_project.deployment.test.ts b/packages/integration-tests/src/test-e2e/deployment/reference_auth_project.deployment.test.ts new file mode 100644 index 0000000000..2a30ffa54e --- /dev/null +++ b/packages/integration-tests/src/test-e2e/deployment/reference_auth_project.deployment.test.ts @@ -0,0 +1,4 @@ +import { ReferenceAuthTestProjectCreator } from '../../test-project-setup/reference_auth_project.js'; +import { defineDeploymentTest } from './deployment.test.template.js'; + +defineDeploymentTest(new ReferenceAuthTestProjectCreator()); diff --git a/packages/integration-tests/src/test-e2e/sandbox.test.ts b/packages/integration-tests/src/test-e2e/sandbox.test.ts deleted file mode 100644 index 394b3c8b3e..0000000000 --- a/packages/integration-tests/src/test-e2e/sandbox.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { after, before, describe, it } from 'node:test'; -import { - createTestDirectory, - deleteTestDirectory, - rootTestDir, -} from '../setup_test_directory.js'; -import { getTestProjectCreators } from '../test-project-setup/test_project_creator.js'; -import { TestProjectBase } from '../test-project-setup/test_project_base.js'; -import { userInfo } from 'os'; -import { ampxCli } from '../process-controller/process_controller.js'; -import { - ensureDeploymentTimeLessThan, - interruptSandbox, - rejectCleanupSandbox, - replaceFiles, - waitForConfigUpdateAfterDeployment, -} from '../process-controller/predicated_action_macros.js'; -import { BackendIdentifier } from '@aws-amplify/plugin-types'; -import { testConcurrencyLevel } from './test_concurrency.js'; -import { - amplifySharedSecretNameKey, - createAmplifySharedSecretName, -} from '../shared_secret.js'; - -const testProjectCreators = getTestProjectCreators(); -void describe('sandbox tests', { concurrency: testConcurrencyLevel }, () => { - before(async () => { - await createTestDirectory(rootTestDir); - }); - after(async () => { - await deleteTestDirectory(rootTestDir); - }); - - void describe('amplify deploys', async () => { - testProjectCreators.forEach((testProjectCreator) => { - void describe(`sandbox deploys ${testProjectCreator.name}`, () => { - let testProject: TestProjectBase; - let sandboxBackendIdentifier: BackendIdentifier; - - before(async () => { - testProject = await testProjectCreator.createProject(rootTestDir); - sandboxBackendIdentifier = { - type: 'sandbox', - namespace: testProject.name, - name: userInfo().username, - }; - }); - - after(async () => { - await testProject.tearDown(sandboxBackendIdentifier); - }); - - void describe('in sequence', { concurrency: false }, () => { - const sharedSecretsEnv = { - [amplifySharedSecretNameKey]: createAmplifySharedSecretName(), - }; - void it(`[${testProjectCreator.name}] deploys fully`, async () => { - await testProject.deploy( - sandboxBackendIdentifier, - sharedSecretsEnv - ); - await testProject.assertPostDeployment(sandboxBackendIdentifier); - }); - - void it('generates config after sandbox --once deployment', async () => { - const processController = ampxCli( - ['sandbox', '--once'], - testProject.projectDirPath, - { - env: sharedSecretsEnv, - } - ); - await processController - .do(waitForConfigUpdateAfterDeployment()) - .run(); - - await testProject.assertPostDeployment(sandboxBackendIdentifier); - }); - - void it(`[${testProjectCreator.name}] hot-swaps a change`, async () => { - const updates = await testProject.getUpdates(); - if (updates.length > 0) { - const processController = ampxCli( - ['sandbox', '--dirToWatch', 'amplify'], - testProject.projectDirPath, - { - env: sharedSecretsEnv, - } - ); - - for (const update of updates) { - processController - .do(replaceFiles(update.replacements)) - .do(ensureDeploymentTimeLessThan(update.deployThresholdSec)); - } - - // Execute the process. - await processController - .do(interruptSandbox()) - .do(rejectCleanupSandbox()) - .run(); - - await testProject.assertPostDeployment(sandboxBackendIdentifier); - } - }); - }); - }); - }); - }); -}); diff --git a/packages/integration-tests/src/test-e2e/sandbox/access_testing_project.sandbox.test.ts b/packages/integration-tests/src/test-e2e/sandbox/access_testing_project.sandbox.test.ts new file mode 100644 index 0000000000..42fc2460d1 --- /dev/null +++ b/packages/integration-tests/src/test-e2e/sandbox/access_testing_project.sandbox.test.ts @@ -0,0 +1,4 @@ +import { defineSandboxTest } from './sandbox.test.template.js'; +import { AccessTestingProjectTestProjectCreator } from '../../test-project-setup/access_testing_project.js'; + +defineSandboxTest(new AccessTestingProjectTestProjectCreator()); diff --git a/packages/integration-tests/src/test-e2e/sandbox/advanced_auth_and_functions.sandbox.test.ts b/packages/integration-tests/src/test-e2e/sandbox/advanced_auth_and_functions.sandbox.test.ts new file mode 100644 index 0000000000..16118e48ea --- /dev/null +++ b/packages/integration-tests/src/test-e2e/sandbox/advanced_auth_and_functions.sandbox.test.ts @@ -0,0 +1,4 @@ +import { defineSandboxTest } from './sandbox.test.template.js'; +import { AdvancedAuthAndFunctionsTestProjectCreator } from '../../test-project-setup/advanced_auth_and_functions.js'; + +defineSandboxTest(new AdvancedAuthAndFunctionsTestProjectCreator()); diff --git a/packages/integration-tests/src/test-e2e/sandbox/circular_dep_auth_data_func.sandbox.test.ts b/packages/integration-tests/src/test-e2e/sandbox/circular_dep_auth_data_func.sandbox.test.ts new file mode 100644 index 0000000000..16def74599 --- /dev/null +++ b/packages/integration-tests/src/test-e2e/sandbox/circular_dep_auth_data_func.sandbox.test.ts @@ -0,0 +1,4 @@ +import { CircularDepAuthDataFuncTestProjectCreator } from '../../test-project-setup/circular_dep_auth_data_func.js'; +import { defineSandboxTest } from './sandbox.test.template.js'; + +defineSandboxTest(new CircularDepAuthDataFuncTestProjectCreator()); diff --git a/packages/integration-tests/src/test-e2e/sandbox/circular_dep_data_func.sandbox.test.ts b/packages/integration-tests/src/test-e2e/sandbox/circular_dep_data_func.sandbox.test.ts new file mode 100644 index 0000000000..d024c5f764 --- /dev/null +++ b/packages/integration-tests/src/test-e2e/sandbox/circular_dep_data_func.sandbox.test.ts @@ -0,0 +1,4 @@ +import { CircularDepDataFuncTestProjectCreator } from '../../test-project-setup/circular_dep_data_func.js'; +import { defineSandboxTest } from './sandbox.test.template.js'; + +defineSandboxTest(new CircularDepDataFuncTestProjectCreator()); diff --git a/packages/integration-tests/src/test-e2e/sandbox/conversation_handler_project.sandbox.test.ts b/packages/integration-tests/src/test-e2e/sandbox/conversation_handler_project.sandbox.test.ts new file mode 100644 index 0000000000..b4ee374c49 --- /dev/null +++ b/packages/integration-tests/src/test-e2e/sandbox/conversation_handler_project.sandbox.test.ts @@ -0,0 +1,4 @@ +import { defineSandboxTest } from './sandbox.test.template.js'; +import { ConversationHandlerTestProjectCreator } from '../../test-project-setup/conversation_handler_project.js'; + +defineSandboxTest(new ConversationHandlerTestProjectCreator()); diff --git a/packages/integration-tests/src/test-e2e/sandbox/custom_outputs.sandbox.test.ts b/packages/integration-tests/src/test-e2e/sandbox/custom_outputs.sandbox.test.ts new file mode 100644 index 0000000000..17f47a7efb --- /dev/null +++ b/packages/integration-tests/src/test-e2e/sandbox/custom_outputs.sandbox.test.ts @@ -0,0 +1,4 @@ +import { defineSandboxTest } from './sandbox.test.template.js'; +import { CustomOutputsTestProjectCreator } from '../../test-project-setup/custom_outputs.js'; + +defineSandboxTest(new CustomOutputsTestProjectCreator()); diff --git a/packages/integration-tests/src/test-e2e/sandbox/data_storage_auth_with_triggers.sandbox.test.ts b/packages/integration-tests/src/test-e2e/sandbox/data_storage_auth_with_triggers.sandbox.test.ts new file mode 100644 index 0000000000..b413244445 --- /dev/null +++ b/packages/integration-tests/src/test-e2e/sandbox/data_storage_auth_with_triggers.sandbox.test.ts @@ -0,0 +1,4 @@ +import { defineSandboxTest } from './sandbox.test.template.js'; +import { DataStorageAuthWithTriggerTestProjectCreator } from '../../test-project-setup/data_storage_auth_with_triggers.js'; + +defineSandboxTest(new DataStorageAuthWithTriggerTestProjectCreator()); diff --git a/packages/integration-tests/src/test-e2e/sandbox/minimal_with_typescript_idioms.sandbox.test.ts b/packages/integration-tests/src/test-e2e/sandbox/minimal_with_typescript_idioms.sandbox.test.ts new file mode 100644 index 0000000000..3f19b529d5 --- /dev/null +++ b/packages/integration-tests/src/test-e2e/sandbox/minimal_with_typescript_idioms.sandbox.test.ts @@ -0,0 +1,4 @@ +import { defineSandboxTest } from './sandbox.test.template.js'; +import { MinimalWithTypescriptIdiomTestProjectCreator } from '../../test-project-setup/minimal_with_typescript_idioms.js'; + +defineSandboxTest(new MinimalWithTypescriptIdiomTestProjectCreator()); diff --git a/packages/integration-tests/src/test-e2e/sandbox/reference_auth_project.sandbox.test.ts b/packages/integration-tests/src/test-e2e/sandbox/reference_auth_project.sandbox.test.ts new file mode 100644 index 0000000000..c7c19bdcf0 --- /dev/null +++ b/packages/integration-tests/src/test-e2e/sandbox/reference_auth_project.sandbox.test.ts @@ -0,0 +1,4 @@ +import { ReferenceAuthTestProjectCreator } from '../../test-project-setup/reference_auth_project.js'; +import { defineSandboxTest } from './sandbox.test.template.js'; + +defineSandboxTest(new ReferenceAuthTestProjectCreator()); diff --git a/packages/integration-tests/src/test-e2e/sandbox/sandbox.test.template.ts b/packages/integration-tests/src/test-e2e/sandbox/sandbox.test.template.ts new file mode 100644 index 0000000000..9b24b0926d --- /dev/null +++ b/packages/integration-tests/src/test-e2e/sandbox/sandbox.test.template.ts @@ -0,0 +1,108 @@ +import { after, before, describe, it } from 'node:test'; +import { + createTestDirectory, + deleteTestDirectory, + rootTestDir, +} from '../../setup_test_directory.js'; +import { TestProjectCreator } from '../../test-project-setup/test_project_creator.js'; +import { TestProjectBase } from '../../test-project-setup/test_project_base.js'; +import { userInfo } from 'os'; +import { ampxCli } from '../../process-controller/process_controller.js'; +import { + ensureDeploymentTimeLessThan, + interruptSandbox, + replaceFiles, + waitForConfigUpdateAfterDeployment, +} from '../../process-controller/predicated_action_macros.js'; +import { BackendIdentifier } from '@aws-amplify/plugin-types'; +import { testConcurrencyLevel } from '../test_concurrency.js'; +import { + amplifySharedSecretNameKey, + createAmplifySharedSecretName, +} from '../../shared_secret.js'; + +/** + * Defines sandbox test + */ +export const defineSandboxTest = (testProjectCreator: TestProjectCreator) => { + void describe('sandbox test', { concurrency: testConcurrencyLevel }, () => { + before(async () => { + await createTestDirectory(rootTestDir); + }); + after(async () => { + await deleteTestDirectory(rootTestDir); + }); + + void describe(`sandbox deploys ${testProjectCreator.name}`, () => { + let testProject: TestProjectBase; + let sandboxBackendIdentifier: BackendIdentifier; + + before(async () => { + testProject = await testProjectCreator.createProject(rootTestDir); + sandboxBackendIdentifier = { + type: 'sandbox', + namespace: testProject.name, + name: userInfo().username, + }; + }); + + after(async () => { + if ( + process.env.AMPLIFY_BACKEND_TESTS_RETAIN_TEST_PROJECT_DEPLOYMENT !== + 'true' + ) { + await testProject.tearDown(sandboxBackendIdentifier); + } + }); + + void describe('in sequence', { concurrency: false }, () => { + const sharedSecretsEnv = { + [amplifySharedSecretNameKey]: createAmplifySharedSecretName(), + }; + void it(`[${testProjectCreator.name}] deploys fully`, async () => { + await testProject.deploy(sandboxBackendIdentifier, sharedSecretsEnv); + await testProject.assertPostDeployment(sandboxBackendIdentifier); + }); + + void it('generates config after sandbox --once deployment', async () => { + const processController = ampxCli( + ['sandbox', '--once'], + testProject.projectDirPath, + { + env: sharedSecretsEnv, + } + ); + await processController + .do(waitForConfigUpdateAfterDeployment()) + .run(); + + await testProject.assertPostDeployment(sandboxBackendIdentifier); + }); + + void it(`[${testProjectCreator.name}] hot-swaps a change`, async () => { + const updates = await testProject.getUpdates(); + if (updates.length > 0) { + const processController = ampxCli( + ['sandbox', '--dirToWatch', 'amplify'], + testProject.projectDirPath, + { + env: sharedSecretsEnv, + } + ); + + for (const update of updates) { + processController + .do(replaceFiles(update.replacements)) + .do(ensureDeploymentTimeLessThan(update.deployThresholdSec)); + } + + // Execute the process. + await processController.do(interruptSandbox()).run(); + + await testProject.assertPostDeployment(sandboxBackendIdentifier); + } + }); + }); + }); + }); +}; diff --git a/packages/integration-tests/src/test-in-memory/data_storage_auth_with_triggers.test.ts b/packages/integration-tests/src/test-in-memory/data_storage_auth_with_triggers.test.ts index 7294be7483..59a1d17525 100644 --- a/packages/integration-tests/src/test-in-memory/data_storage_auth_with_triggers.test.ts +++ b/packages/integration-tests/src/test-in-memory/data_storage_auth_with_triggers.test.ts @@ -4,6 +4,8 @@ import { synthesizeBackendTemplates, } from '../define_backend_template_harness.js'; import { dataStorageAuthWithTriggers } from '../test-projects/data-storage-auth-with-triggers-ts/amplify/test_factories.js'; +import path from 'node:path'; +import fsp from 'fs/promises'; /** * This test suite is meant to provide a fast feedback loop to sanity check that different feature verticals are working properly together. @@ -12,7 +14,7 @@ import { dataStorageAuthWithTriggers } from '../test-projects/data-storage-auth- * Critical path interactions should be exercised in a full e2e test. */ -void it('data storage auth with triggers', () => { +void it('data storage auth with triggers', async () => { const templates = synthesizeBackendTemplates(dataStorageAuthWithTriggers); assertExpectedLogicalIds(templates.root, 'AWS::CloudFormation::Stack', [ @@ -52,13 +54,16 @@ void it('data storage auth with triggers', () => { assertExpectedLogicalIds(templates.defaultNodeFunc, 'AWS::Lambda::Function', [ 'defaultNodeFunctionlambda5C194062', 'echoFunclambdaE17DCA46', - 'funcWithAwsSdklambda5F770AD7', - 'funcWithSchedulelambda0B6E4271', - 'funcWithSsmlambda6A8824A1', 'handler2lambda1B9C7EFF', 'node16Functionlambda97ECC775', 'onUploadlambdaA252C959', 'onDeletelambda96BB6F15', ]); /* eslint-enable spellcheck/spell-checker */ + + // clean up generated env files + await fsp.rm(path.join(process.cwd(), '.amplify'), { + recursive: true, + force: true, + }); }); diff --git a/packages/integration-tests/src/test-live-dependency-health-checks/health_checks.test.ts b/packages/integration-tests/src/test-live-dependency-health-checks/health_checks.test.ts index 8aae8bd8e6..19b475a917 100644 --- a/packages/integration-tests/src/test-live-dependency-health-checks/health_checks.test.ts +++ b/packages/integration-tests/src/test-live-dependency-health-checks/health_checks.test.ts @@ -14,7 +14,6 @@ import { import { confirmDeleteSandbox, interruptSandbox, - rejectCleanupSandbox, waitForSandboxDeploymentToPrintTotalTime, } from '../process-controller/predicated_action_macros.js'; import { BackendIdentifierConversions } from '@aws-amplify/platform-core'; @@ -123,7 +122,6 @@ void describe('Live dependency health checks', { concurrency: true }, () => { await ampxCli(['sandbox'], tempDir) .do(waitForSandboxDeploymentToPrintTotalTime()) .do(interruptSandbox()) - .do(rejectCleanupSandbox()) .run(); const clientConfigStats = await fs.stat( diff --git a/packages/integration-tests/src/test-project-setup/access_testing_project.ts b/packages/integration-tests/src/test-project-setup/access_testing_project.ts index b41d4bd1b1..4430a49397 100644 --- a/packages/integration-tests/src/test-project-setup/access_testing_project.ts +++ b/packages/integration-tests/src/test-project-setup/access_testing_project.ts @@ -45,6 +45,7 @@ import { IamCredentials } from '../types.js'; import { AmplifyAuthCredentialsFactory } from '../amplify_auth_credentials_factory.js'; import { SemVer } from 'semver'; import { AmplifyClient } from '@aws-sdk/client-amplify'; +import { e2eToolingClientConfig } from '../e2e_tooling_client_config.js'; // TODO: this is a work around // it seems like as of amplify v6 , some of the code only runs in the browser ... @@ -69,11 +70,21 @@ export class AccessTestingProjectTestProjectCreator * Creates project creator. */ constructor( - private readonly cfnClient: CloudFormationClient, - private readonly amplifyClient: AmplifyClient, - private readonly cognitoIdentityClient: CognitoIdentityClient, - private readonly cognitoIdentityProviderClient: CognitoIdentityProviderClient, - private readonly stsClient: STSClient + private readonly cfnClient: CloudFormationClient = new CloudFormationClient( + e2eToolingClientConfig + ), + private readonly amplifyClient: AmplifyClient = new AmplifyClient( + e2eToolingClientConfig + ), + private readonly cognitoIdentityClient: CognitoIdentityClient = new CognitoIdentityClient( + e2eToolingClientConfig + ), + private readonly cognitoIdentityProviderClient: CognitoIdentityProviderClient = new CognitoIdentityProviderClient( + e2eToolingClientConfig + ), + private readonly stsClient: STSClient = new STSClient( + e2eToolingClientConfig + ) ) {} createProject = async (e2eProjectDir: string): Promise => { @@ -147,7 +158,7 @@ class AccessTestingProjectTestProject extends TestProjectBase { backendId: BackendIdentifier ): Promise { await super.assertPostDeployment(backendId); - const clientConfig = await generateClientConfig(backendId, '1.1'); + const clientConfig = await generateClientConfig(backendId, '1.3'); await this.assertDifferentCognitoInstanceCannotAssumeAmplifyRoles( clientConfig ); @@ -160,7 +171,7 @@ class AccessTestingProjectTestProject extends TestProjectBase { * I.e. roles not created by auth construct. */ private assertGenericIamRolesAccessToData = async ( - clientConfig: ClientConfigVersionTemplateType<'1.1'> + clientConfig: ClientConfigVersionTemplateType<'1.3'> ) => { if (!clientConfig.custom) { throw new Error('Client config is missing custom section'); @@ -262,7 +273,7 @@ class AccessTestingProjectTestProject extends TestProjectBase { * This asserts that authenticated and unauthenticated roles have relevant access to data API. */ private assertAmplifyAuthAccessToData = async ( - clientConfig: ClientConfigVersionTemplateType<'1.1'> + clientConfig: ClientConfigVersionTemplateType<'1.3'> ): Promise => { if (!clientConfig.auth) { throw new Error('Client config is missing auth section'); @@ -367,7 +378,7 @@ class AccessTestingProjectTestProject extends TestProjectBase { * unauthorized roles. I.e. it tests trust policy. */ private assertDifferentCognitoInstanceCannotAssumeAmplifyRoles = async ( - clientConfig: ClientConfigVersionTemplateType<'1.1'> + clientConfig: ClientConfigVersionTemplateType<'1.3'> ): Promise => { const simpleAuthUser = await this.createAuthenticatedSimpleAuthCognitoUser( clientConfig @@ -416,7 +427,7 @@ class AccessTestingProjectTestProject extends TestProjectBase { }; private createAuthenticatedSimpleAuthCognitoUser = async ( - clientConfig: ClientConfigVersionTemplateType<'1.1'> + clientConfig: ClientConfigVersionTemplateType<'1.3'> ): Promise => { if (!clientConfig.custom) { throw new Error('Client config is missing custom section'); @@ -496,7 +507,7 @@ class AccessTestingProjectTestProject extends TestProjectBase { }; private createAppSyncClient = ( - clientConfig: ClientConfigVersionTemplateType<'1.1'>, + clientConfig: ClientConfigVersionTemplateType<'1.3'>, credentials: IamCredentials ): ApolloClient => { if (!clientConfig.data?.url) { diff --git a/packages/integration-tests/src/test-project-setup/advanced_auth_and_functions.ts b/packages/integration-tests/src/test-project-setup/advanced_auth_and_functions.ts new file mode 100644 index 0000000000..eb8056db5a --- /dev/null +++ b/packages/integration-tests/src/test-project-setup/advanced_auth_and_functions.ts @@ -0,0 +1,335 @@ +import fs from 'fs/promises'; +import { BackendIdentifier } from '@aws-amplify/plugin-types'; +import { createEmptyAmplifyProject } from './create_empty_amplify_project.js'; +import { CloudFormationClient } from '@aws-sdk/client-cloudformation'; +import { TestProjectBase } from './test_project_base.js'; +import { TestProjectCreator } from './test_project_creator.js'; +import { DeployedResourcesFinder } from '../find_deployed_resource.js'; +import assert from 'node:assert'; +import { + GetFunctionCommand, + InvokeCommand, + LambdaClient, +} from '@aws-sdk/client-lambda'; +import { AmplifyClient } from '@aws-sdk/client-amplify'; +import { + DeleteMessageCommand, + ReceiveMessageCommand, + SQSClient, +} from '@aws-sdk/client-sqs'; +import { e2eToolingClientConfig } from '../e2e_tooling_client_config.js'; +import { TextWriter, ZipReader } from '@zip.js/zip.js'; +import { + AdminCreateUserCommand, + CognitoIdentityProviderClient, +} from '@aws-sdk/client-cognito-identity-provider'; + +/** + * Creates test projects with advanced use cases of auth and functions categories. + */ +export class AdvancedAuthAndFunctionsTestProjectCreator + implements TestProjectCreator +{ + readonly name = 'advanced-auth-and-functions'; + + /** + * Creates project creator. + */ + constructor( + private readonly cfnClient: CloudFormationClient = new CloudFormationClient( + e2eToolingClientConfig + ), + private readonly amplifyClient: AmplifyClient = new AmplifyClient( + e2eToolingClientConfig + ), + private readonly lambdaClient: LambdaClient = new LambdaClient( + e2eToolingClientConfig + ), + private readonly sqsClient: SQSClient = new SQSClient( + e2eToolingClientConfig + ), + private readonly resourceFinder: DeployedResourcesFinder = new DeployedResourcesFinder(), + private readonly cognitoClient: CognitoIdentityProviderClient = new CognitoIdentityProviderClient( + e2eToolingClientConfig + ) + ) {} + + createProject = async (e2eProjectDir: string): Promise => { + const { projectName, projectRoot, projectAmplifyDir } = + await createEmptyAmplifyProject(this.name, e2eProjectDir); + + const project = new AdvancedAuthAndFunctionsTestProject( + projectName, + projectRoot, + projectAmplifyDir, + this.cfnClient, + this.amplifyClient, + this.lambdaClient, + this.sqsClient, + this.resourceFinder, + this.cognitoClient + ); + await fs.cp( + project.sourceProjectAmplifyDirURL, + project.projectAmplifyDirPath, + { + recursive: true, + } + ); + + return project; + }; +} + +/** + * Test project with advanced use cases of auth and functions categories. + */ +class AdvancedAuthAndFunctionsTestProject extends TestProjectBase { + readonly sourceProjectRootPath = + '../../src/test-projects/advanced-auth-and-functions'; + + readonly sourceProjectAmplifyDirURL: URL = new URL( + `${this.sourceProjectRootPath}/amplify`, + import.meta.url + ); + + /** + * Create a test project instance. + */ + constructor( + name: string, + projectDirPath: string, + projectAmplifyDirPath: string, + cfnClient: CloudFormationClient, + amplifyClient: AmplifyClient, + private readonly lambdaClient: LambdaClient, + private readonly sqsClient: SQSClient, + private readonly resourceFinder: DeployedResourcesFinder, + private readonly cognitoClient: CognitoIdentityProviderClient + ) { + super( + name, + projectDirPath, + projectAmplifyDirPath, + cfnClient, + amplifyClient + ); + } + + override async assertPostDeployment( + backendId: BackendIdentifier + ): Promise { + await super.assertPostDeployment(backendId); + + // Check that deployed lambdas are working correctly + + // find lambda functions + const funcWithSsm = await this.resourceFinder.findByBackendIdentifier( + backendId, + 'AWS::Lambda::Function', + (name) => name.includes('funcWithSsm') + ); + + const funcWithAwsSdk = await this.resourceFinder.findByBackendIdentifier( + backendId, + 'AWS::Lambda::Function', + (name) => name.includes('funcWithAwsSdk') + ); + + const funcWithSchedule = await this.resourceFinder.findByBackendIdentifier( + backendId, + 'AWS::Lambda::Function', + (name) => name.includes('funcWithSchedule') + ); + + const funcNoMinify = await this.resourceFinder.findByBackendIdentifier( + backendId, + 'AWS::Lambda::Function', + (name) => name.includes('funcNoMinify') + ); + const funcCustomEmailSender = + await this.resourceFinder.findByBackendIdentifier( + backendId, + 'AWS::Lambda::Function', + (name) => name.includes('funcCustomEmailSender') + ); + + assert.equal(funcWithSsm.length, 1); + assert.equal(funcWithAwsSdk.length, 1); + assert.equal(funcWithSchedule.length, 1); + assert.equal(funcCustomEmailSender.length, 1); + + await this.checkLambdaResponse(funcWithSsm[0], 'It is working'); + + // Custom email sender assertion + await this.assertCustomEmailSenderWorks(backendId); + + await this.assertScheduleInvokesFunction(backendId); + + const expectedNoMinifyChunk = [ + 'var handler = async () => {', + ' return "No minify";', + '};', + ].join('\n'); + await this.checkLambdaCode(funcNoMinify[0], expectedNoMinifyChunk); + } + + private checkLambdaResponse = async ( + lambdaName: string, + expectedResponse: unknown + ) => { + // invoke the lambda + const response = await this.lambdaClient.send( + new InvokeCommand({ FunctionName: lambdaName }) + ); + const responsePayload = JSON.parse( + response.Payload?.transformToString() || '' + ); + + // check expected response + assert.deepStrictEqual(responsePayload, expectedResponse); + }; + + private checkLambdaCode = async ( + lambdaName: string, + expectedCode: string + ) => { + // get the lambda code + const response = await this.lambdaClient.send( + new GetFunctionCommand({ FunctionName: lambdaName }) + ); + const codeUrl = response.Code?.Location; + assert(codeUrl !== undefined); + const fetchResponse = await fetch(codeUrl); + const zipReader = new ZipReader(fetchResponse.body!); + const entries = await zipReader.getEntries(); + const entry = entries.find((entry) => entry.filename.endsWith('index.mjs')); + assert(entry !== undefined); + const sourceCode = await entry.getData!(new TextWriter()); + assert(sourceCode.includes(expectedCode)); + }; + + private assertScheduleInvokesFunction = async ( + backendId: BackendIdentifier + ) => { + const TIMEOUT_MS = 1000 * 60 * 2; // 2 minutes + const startTime = Date.now(); + let receivedMessageCount = 0; + + const queue = await this.resourceFinder.findByBackendIdentifier( + backendId, + 'AWS::SQS::Queue', + (name) => name.includes('testFuncQueue') + ); + + // wait for schedule to invoke the function one time for it to send a message + while (Date.now() - startTime < TIMEOUT_MS) { + const response = await this.sqsClient.send( + new ReceiveMessageCommand({ + QueueUrl: queue[0], + WaitTimeSeconds: 20, + MaxNumberOfMessages: 10, + }) + ); + + if (response.Messages) { + receivedMessageCount += response.Messages.length; + + // delete messages afterwards + for (const message of response.Messages) { + await this.sqsClient.send( + new DeleteMessageCommand({ + QueueUrl: queue[0], + ReceiptHandle: message.ReceiptHandle, + }) + ); + } + } + } + + if (receivedMessageCount === 0) { + assert.fail( + `The scheduled function failed to invoke and send a message to the queue.` + ); + } + }; + + private assertCustomEmailSenderWorks = async ( + backendId: BackendIdentifier + ) => { + const TIMEOUT_MS = 1000 * 60 * 2; // 2 minutes + const startTime = Date.now(); + const queue = await this.resourceFinder.findByBackendIdentifier( + backendId, + 'AWS::SQS::Queue', + (name) => name.includes('customEmailSenderQueue') + ); + + assert.strictEqual(queue.length, 1, 'Custom email sender queue not found'); + + // Trigger an email sending operation + await this.triggerEmailSending(backendId); + + // Wait for the SQS message + let messageReceived = false; + while (Date.now() - startTime < TIMEOUT_MS && !messageReceived) { + const response = await this.sqsClient.send( + new ReceiveMessageCommand({ + QueueUrl: queue[0], + WaitTimeSeconds: 20, + }) + ); + + if (response.Messages && response.Messages.length > 0) { + messageReceived = true; + // Verify the message content + const messageBody = JSON.parse(response.Messages[0].Body || '{}'); + assert.strictEqual( + messageBody.message, + 'Custom Email Sender is working', + 'Unexpected message content' + ); + + // Delete the message + await this.sqsClient.send( + new DeleteMessageCommand({ + QueueUrl: queue[0], + ReceiptHandle: response.Messages[0].ReceiptHandle!, + }) + ); + } + } + + assert.strictEqual( + messageReceived, + true, + 'Custom email sender was not triggered within the timeout period' + ); + }; + + private triggerEmailSending = async (backendId: BackendIdentifier) => { + const userPoolId = await this.resourceFinder.findByBackendIdentifier( + backendId, + 'AWS::Cognito::UserPool', + () => true + ); + + assert.strictEqual(userPoolId.length, 1, 'User pool not found'); + + const username = `testuser_${Date.now()}@example.com`; + const password = 'TestPassword123!'; + + await this.cognitoClient.send( + new AdminCreateUserCommand({ + UserPoolId: userPoolId[0], + Username: username, + TemporaryPassword: password, + UserAttributes: [ + { Name: 'email', Value: username }, + { Name: 'email_verified', Value: 'true' }, + ], + }) + ); + // The creation of a new user should trigger the custom email sender + }; +} diff --git a/packages/integration-tests/src/test-project-setup/cdk/auth_cdk_project.ts b/packages/integration-tests/src/test-project-setup/cdk/auth_cdk_project.ts index 2beb33dff0..97a3692efe 100644 --- a/packages/integration-tests/src/test-project-setup/cdk/auth_cdk_project.ts +++ b/packages/integration-tests/src/test-project-setup/cdk/auth_cdk_project.ts @@ -22,7 +22,9 @@ export class AuthTestCdkProjectCreator implements TestCdkProjectCreator { /** * Constructor. */ - constructor(private readonly resourceFinder: DeployedResourcesFinder) {} + constructor( + private readonly resourceFinder: DeployedResourcesFinder = new DeployedResourcesFinder() + ) {} createProject = async ( e2eProjectDir: string @@ -78,7 +80,7 @@ class AuthTestCdkProject extends TestCdkProjectBase { { stackName: this.stackName, }, - '1.1', //version of the config + '1.3', //version of the config awsClientProvider ); diff --git a/packages/integration-tests/src/test-project-setup/cdk/create_empty_cdk_project.ts b/packages/integration-tests/src/test-project-setup/cdk/create_empty_cdk_project.ts index 67d3b67758..72db39e0fe 100644 --- a/packages/integration-tests/src/test-project-setup/cdk/create_empty_cdk_project.ts +++ b/packages/integration-tests/src/test-project-setup/cdk/create_empty_cdk_project.ts @@ -24,5 +24,15 @@ export const createEmptyCdkProject = async ( await cdkCli(['init', 'app', '--language', 'typescript'], projectRoot).run(); + // Remove local node_modules after CDK init. + // This is to make sure that test project is using same version of + // CDK and constructs as the rest of the codebase. + // Otherwise, we might get errors about incompatible classes if + // dependencies on npm are ahead of our package-lock. + await fsp.rm(path.join(projectRoot, 'node_modules'), { + recursive: true, + force: true, + }); + return { projectName, projectRoot }; }; diff --git a/packages/integration-tests/src/test-project-setup/cdk/test_cdk_project_creator.ts b/packages/integration-tests/src/test-project-setup/cdk/test_cdk_project_creator.ts index 6f0e94efe2..c1b6d2ef6e 100644 --- a/packages/integration-tests/src/test-project-setup/cdk/test_cdk_project_creator.ts +++ b/packages/integration-tests/src/test-project-setup/cdk/test_cdk_project_creator.ts @@ -1,10 +1,6 @@ -import { CloudFormationClient } from '@aws-sdk/client-cloudformation'; -import { e2eToolingClientConfig } from '../../e2e_tooling_client_config.js'; import { TestCdkProjectBase } from './test_cdk_project_base.js'; -import { AuthTestCdkProjectCreator } from './auth_cdk_project.js'; import { fileURLToPath } from 'node:url'; import path from 'path'; -import { DeployedResourcesFinder } from '../../find_deployed_resource.js'; const dirname = path.dirname(fileURLToPath(import.meta.url)); export const testCdkProjectsSourceRoot = path.resolve( @@ -20,15 +16,3 @@ export type TestCdkProjectCreator = { readonly name: string; createProject: (e2eProjectDir: string) => Promise; }; - -/** - * Generates a list of test cdk projects. - */ -export const getTestCdkProjectCreators = (): TestCdkProjectCreator[] => { - const testCdkProjectCreators: TestCdkProjectCreator[] = []; - - const cfnClient = new CloudFormationClient(e2eToolingClientConfig); - const resourceFinder = new DeployedResourcesFinder(cfnClient); - testCdkProjectCreators.push(new AuthTestCdkProjectCreator(resourceFinder)); - return testCdkProjectCreators; -}; diff --git a/packages/integration-tests/src/test-project-setup/circular_dep_auth_data_func.ts b/packages/integration-tests/src/test-project-setup/circular_dep_auth_data_func.ts new file mode 100644 index 0000000000..b75882fb14 --- /dev/null +++ b/packages/integration-tests/src/test-project-setup/circular_dep_auth_data_func.ts @@ -0,0 +1,83 @@ +import { TestProjectBase } from './test_project_base.js'; +import fs from 'fs/promises'; +import { createEmptyAmplifyProject } from './create_empty_amplify_project.js'; +import { CloudFormationClient } from '@aws-sdk/client-cloudformation'; +import { TestProjectCreator } from './test_project_creator.js'; +import { AmplifyClient } from '@aws-sdk/client-amplify'; +import { e2eToolingClientConfig } from '../e2e_tooling_client_config.js'; + +/** + * Creates test projects with circular dependency between auth, data, and functions + */ +export class CircularDepAuthDataFuncTestProjectCreator + implements TestProjectCreator +{ + readonly name = 'circular-dep-auth-data-func'; + + /** + * Creates project creator. + */ + constructor( + private readonly cfnClient: CloudFormationClient = new CloudFormationClient( + e2eToolingClientConfig + ), + private readonly amplifyClient: AmplifyClient = new AmplifyClient( + e2eToolingClientConfig + ) + ) {} + + createProject = async (e2eProjectDir: string): Promise => { + const { projectName, projectRoot, projectAmplifyDir } = + await createEmptyAmplifyProject(this.name, e2eProjectDir); + + const project = new CircularDepAuthDataFuncTestProject( + projectName, + projectRoot, + projectAmplifyDir, + this.cfnClient, + this.amplifyClient + ); + await fs.cp( + project.sourceProjectAmplifyDirURL, + project.projectAmplifyDirPath, + { + recursive: true, + } + ); + return project; + }; +} + +/** + * Test project with circular dependency between auth, data, and functions + */ +class CircularDepAuthDataFuncTestProject extends TestProjectBase { + readonly sourceProjectDirPath = + '../../src/test-projects/circular-dep-auth-data-func'; + + readonly sourceProjectAmplifyDirSuffix = `${this.sourceProjectDirPath}/amplify`; + + readonly sourceProjectAmplifyDirURL: URL = new URL( + this.sourceProjectAmplifyDirSuffix, + import.meta.url + ); + + /** + * Create a test project instance. + */ + constructor( + name: string, + projectDirPath: string, + projectAmplifyDirPath: string, + cfnClient: CloudFormationClient, + amplifyClient: AmplifyClient + ) { + super( + name, + projectDirPath, + projectAmplifyDirPath, + cfnClient, + amplifyClient + ); + } +} diff --git a/packages/integration-tests/src/test-project-setup/circular_dep_data_func.ts b/packages/integration-tests/src/test-project-setup/circular_dep_data_func.ts new file mode 100644 index 0000000000..d00c86facd --- /dev/null +++ b/packages/integration-tests/src/test-project-setup/circular_dep_data_func.ts @@ -0,0 +1,83 @@ +import { TestProjectBase } from './test_project_base.js'; +import fs from 'fs/promises'; +import { createEmptyAmplifyProject } from './create_empty_amplify_project.js'; +import { CloudFormationClient } from '@aws-sdk/client-cloudformation'; +import { TestProjectCreator } from './test_project_creator.js'; +import { AmplifyClient } from '@aws-sdk/client-amplify'; +import { e2eToolingClientConfig } from '../e2e_tooling_client_config.js'; + +/** + * Creates test projects with circular dependency between data, and functions + */ +export class CircularDepDataFuncTestProjectCreator + implements TestProjectCreator +{ + readonly name = 'circular-dep-data-func'; + + /** + * Creates project creator. + */ + constructor( + private readonly cfnClient: CloudFormationClient = new CloudFormationClient( + e2eToolingClientConfig + ), + private readonly amplifyClient: AmplifyClient = new AmplifyClient( + e2eToolingClientConfig + ) + ) {} + + createProject = async (e2eProjectDir: string): Promise => { + const { projectName, projectRoot, projectAmplifyDir } = + await createEmptyAmplifyProject(this.name, e2eProjectDir); + + const project = new CircularDepDataFuncTestProject( + projectName, + projectRoot, + projectAmplifyDir, + this.cfnClient, + this.amplifyClient + ); + await fs.cp( + project.sourceProjectAmplifyDirURL, + project.projectAmplifyDirPath, + { + recursive: true, + } + ); + return project; + }; +} + +/** + * Test project with circular dependency between data, and functions + */ +class CircularDepDataFuncTestProject extends TestProjectBase { + readonly sourceProjectDirPath = + '../../src/test-projects/circular-dep-data-func'; + + readonly sourceProjectAmplifyDirSuffix = `${this.sourceProjectDirPath}/amplify`; + + readonly sourceProjectAmplifyDirURL: URL = new URL( + this.sourceProjectAmplifyDirSuffix, + import.meta.url + ); + + /** + * Create a test project instance. + */ + constructor( + name: string, + projectDirPath: string, + projectAmplifyDirPath: string, + cfnClient: CloudFormationClient, + amplifyClient: AmplifyClient + ) { + super( + name, + projectDirPath, + projectAmplifyDirPath, + cfnClient, + amplifyClient + ); + } +} diff --git a/packages/integration-tests/src/test-project-setup/conversation_handler_project.ts b/packages/integration-tests/src/test-project-setup/conversation_handler_project.ts index 9c1c52b5a7..4418a324b8 100644 --- a/packages/integration-tests/src/test-project-setup/conversation_handler_project.ts +++ b/packages/integration-tests/src/test-project-setup/conversation_handler_project.ts @@ -26,11 +26,15 @@ import assert from 'assert'; import { NormalizedCacheObject } from '@apollo/client'; import { bedrockModelId, + expectedRandomNumber, expectedTemperatureInDataToolScenario, - expectedTemperatureInProgrammaticToolScenario, + expectedTemperaturesInProgrammaticToolScenario, } from '../test-projects/conversation-handler/amplify/constants.js'; import { resolve } from 'path'; import { fileURLToPath } from 'url'; +import * as bedrock from '@aws-sdk/client-bedrock-runtime'; +import { e2eToolingClientConfig } from '../e2e_tooling_client_config.js'; +import { runWithRetry } from '../retry.js'; // TODO: this is a work around // it seems like as of amplify v6 , some of the code only runs in the browser ... @@ -46,26 +50,52 @@ if (process.versions.node) { type ConversationTurnAppSyncResponse = { associatedUserMessageId: string; content: string; + errors?: Array; }; -const commonEventProperties = { - responseMutation: { - name: 'createConversationMessageAssistantResponse', - inputTypeName: 'CreateConversationMessageAssistantResponseInput', - selectionSet: [ - 'id', - 'conversationId', - 'content', - 'sender', - 'owner', - 'createdAt', - 'updatedAt', - ].join('\n'), - }, - modelConfiguration: { - modelId: bedrockModelId, - systemPrompt: 'You are helpful bot.', - }, +type ConversationMessage = { + role: 'user' | 'assistant'; + content: Array; +}; + +type ConversationMessageContentBlock = + | bedrock.ContentBlock + | { + image: Omit & { + // Upstream (Appsync) may send images in a form of Base64 encoded strings + source: { bytes: string }; + }; + // These are needed so that union with other content block types works. + // See https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-bedrock-runtime/TypeAlias/ContentBlock/. + text?: never; + document?: never; + toolUse?: never; + toolResult?: never; + guardContent?: never; + $unknown?: never; + }; + +type CreateConversationMessageChatInput = ConversationMessage & { + conversationId: string; + id: string; + associatedUserMessageId?: string; +}; + +type ConversationTurnError = { + errorType: string; + message: string; +}; + +type ConversationTurnAppSyncResponseChunk = { + conversationId: string; + associatedUserMessageId: string; + contentBlockIndex: number; + contentBlockText?: string; + contentBlockDeltaIndex?: number; + contentBlockDoneAtIndex?: number; + contentBlockToolUse?: string; + stopReason?: string; + errors?: Array; }; /** @@ -80,11 +110,19 @@ export class ConversationHandlerTestProjectCreator * Creates project creator. */ constructor( - private readonly cfnClient: CloudFormationClient, - private readonly amplifyClient: AmplifyClient, - private readonly lambdaClient: LambdaClient, - private readonly cognitoIdentityProviderClient: CognitoIdentityProviderClient, - private readonly resourceFinder: DeployedResourcesFinder + private readonly cfnClient: CloudFormationClient = new CloudFormationClient( + e2eToolingClientConfig + ), + private readonly amplifyClient: AmplifyClient = new AmplifyClient( + e2eToolingClientConfig + ), + private readonly lambdaClient: LambdaClient = new LambdaClient( + e2eToolingClientConfig + ), + private readonly cognitoIdentityProviderClient: CognitoIdentityProviderClient = new CognitoIdentityProviderClient( + e2eToolingClientConfig + ), + private readonly resourceFinder: DeployedResourcesFinder = new DeployedResourcesFinder() ) {} createProject = async (e2eProjectDir: string): Promise => { @@ -135,9 +173,13 @@ class ConversationHandlerTestProject extends TestProjectBase { projectAmplifyDirPath: string, cfnClient: CloudFormationClient, amplifyClient: AmplifyClient, - private readonly lambdaClient: LambdaClient, - private readonly cognitoIdentityProviderClient: CognitoIdentityProviderClient, - private readonly resourceFinder: DeployedResourcesFinder + private readonly lambdaClient: LambdaClient = new LambdaClient( + e2eToolingClientConfig + ), + private readonly cognitoIdentityProviderClient: CognitoIdentityProviderClient = new CognitoIdentityProviderClient( + e2eToolingClientConfig + ), + private readonly resourceFinder: DeployedResourcesFinder = new DeployedResourcesFinder() ) { super( name, @@ -161,6 +203,7 @@ class ConversationHandlerTestProject extends TestProjectBase { throw new Error('Conversation handler project must include auth'); } + const dataUrl = clientConfig.data?.url; const authenticatedUserCredentials = await new AmplifyAuthCredentialsFactory( this.cognitoIdentityProviderClient, @@ -185,39 +228,149 @@ class ConversationHandlerTestProject extends TestProjectBase { cache: new InMemoryCache(), }); - await this.assertDefaultConversationHandlerCanExecuteTurn( - backendId, - authenticatedUserCredentials.accessToken, - clientConfig.data.url, - apolloClient + await this.executeWithRetry(() => + this.assertDefaultConversationHandlerCanExecuteTurn( + backendId, + authenticatedUserCredentials.accessToken, + dataUrl, + apolloClient, + false + ) ); - await this.assertCustomConversationHandlerCanExecuteTurn( - backendId, - authenticatedUserCredentials.accessToken, - clientConfig.data.url, - apolloClient + await this.executeWithRetry(() => + this.assertDefaultConversationHandlerCanExecuteTurn( + backendId, + authenticatedUserCredentials.accessToken, + dataUrl, + apolloClient, + true + ) ); - await this.assertDefaultConversationHandlerCanExecuteTurnWithDataTool( - backendId, - authenticatedUserCredentials.accessToken, - clientConfig.data.url, - apolloClient + await this.executeWithRetry(() => + this.assertDefaultConversationHandlerCanExecuteTurn( + backendId, + authenticatedUserCredentials.accessToken, + dataUrl, + apolloClient, + false, + // Simulate eventual consistency + true + ) ); - await this.assertDefaultConversationHandlerCanExecuteTurnWithClientTool( - backendId, - authenticatedUserCredentials.accessToken, - clientConfig.data.url, - apolloClient + await this.executeWithRetry(() => + this.assertCustomConversationHandlerCanExecuteTurn( + backendId, + authenticatedUserCredentials.accessToken, + dataUrl, + apolloClient, + false + ) ); - await this.assertDefaultConversationHandlerCanExecuteTurnWithImage( - backendId, - authenticatedUserCredentials.accessToken, - clientConfig.data.url, - apolloClient + await this.executeWithRetry(() => + this.assertCustomConversationHandlerCanExecuteTurn( + backendId, + authenticatedUserCredentials.accessToken, + dataUrl, + apolloClient, + true + ) + ); + + await this.executeWithRetry((attempt) => + this.assertCustomConversationHandlerCanExecuteTurnWithParameterLessTool( + backendId, + authenticatedUserCredentials.accessToken, + dataUrl, + apolloClient, + true, + attempt + ) + ); + + await this.executeWithRetry(() => + this.assertDefaultConversationHandlerCanExecuteTurnWithDataTool( + backendId, + authenticatedUserCredentials.accessToken, + dataUrl, + apolloClient, + false + ) + ); + + await this.executeWithRetry(() => + this.assertDefaultConversationHandlerCanExecuteTurnWithDataTool( + backendId, + authenticatedUserCredentials.accessToken, + dataUrl, + apolloClient, + true + ) + ); + + await this.executeWithRetry(() => + this.assertDefaultConversationHandlerCanExecuteTurnWithClientTool( + backendId, + authenticatedUserCredentials.accessToken, + dataUrl, + apolloClient, + false + ) + ); + + await this.executeWithRetry(() => + this.assertDefaultConversationHandlerCanExecuteTurnWithClientTool( + backendId, + authenticatedUserCredentials.accessToken, + dataUrl, + apolloClient, + true + ) + ); + + await this.executeWithRetry(() => + this.assertDefaultConversationHandlerCanExecuteTurnWithImage( + backendId, + authenticatedUserCredentials.accessToken, + dataUrl, + apolloClient, + false + ) + ); + + await this.executeWithRetry(() => + this.assertDefaultConversationHandlerCanExecuteTurnWithImage( + backendId, + authenticatedUserCredentials.accessToken, + dataUrl, + apolloClient, + true + ) + ); + + await this.executeWithRetry((attempt) => + this.assertDefaultConversationHandlerCanPropagateError( + backendId, + authenticatedUserCredentials.accessToken, + dataUrl, + apolloClient, + true, + attempt + ) + ); + + await this.executeWithRetry((attempt) => + this.assertDefaultConversationHandlerCanPropagateError( + backendId, + authenticatedUserCredentials.accessToken, + dataUrl, + apolloClient, + false, + attempt + ) ); } @@ -225,7 +378,9 @@ class ConversationHandlerTestProject extends TestProjectBase { backendId: BackendIdentifier, accessToken: string, graphqlApiEndpoint: string, - apolloClient: ApolloClient + apolloClient: ApolloClient, + streamResponse: boolean, + withoutMessageAvailableInTheMessageList = false ): Promise => { const defaultConversationHandlerFunction = ( await this.resourceFinder.findByBackendIdentifier( @@ -235,26 +390,36 @@ class ConversationHandlerTestProject extends TestProjectBase { ) )[0]; - // send event - const event: ConversationTurnEvent = { + const message: CreateConversationMessageChatInput = { + id: randomUUID().toString(), conversationId: randomUUID().toString(), - currentMessageId: randomUUID().toString(), - graphqlApiEndpoint: graphqlApiEndpoint, - messages: [ + role: 'user', + content: [ { - role: 'user', - content: [ - { - text: 'What is the value of PI?', - }, - ], + text: 'What is the value of PI?', }, ], + }; + + // send event + const event: ConversationTurnEvent = { + conversationId: message.conversationId, + currentMessageId: message.id, + graphqlApiEndpoint: graphqlApiEndpoint, request: { headers: { authorization: accessToken }, }, - ...commonEventProperties, + ...this.getCommonEventProperties(streamResponse), }; + + if (withoutMessageAvailableInTheMessageList) { + // This tricks conversation handler to think that message is not available in the list. + // I.e. it simulates eventually consistency read at list operation where item is not yet visible. + // In this case handler should fall back to lookup by current message id. + message.conversationId = randomUUID().toString(); + } + await this.insertMessage(apolloClient, message); + const response = await this.executeConversationTurn( event, defaultConversationHandlerFunction, @@ -267,7 +432,8 @@ class ConversationHandlerTestProject extends TestProjectBase { backendId: BackendIdentifier, accessToken: string, graphqlApiEndpoint: string, - apolloClient: ApolloClient + apolloClient: ApolloClient, + streamResponse: boolean ): Promise => { const defaultConversationHandlerFunction = ( await this.resourceFinder.findByBackendIdentifier( @@ -291,32 +457,34 @@ class ConversationHandlerTestProject extends TestProjectBase { const imageSource = await fs.readFile(imagePath, 'base64'); - // send event - const event: ConversationTurnEvent = { + const message: CreateConversationMessageChatInput = { + id: randomUUID().toString(), conversationId: randomUUID().toString(), - currentMessageId: randomUUID().toString(), - graphqlApiEndpoint: graphqlApiEndpoint, - messages: [ + role: 'user', + content: [ { - role: 'user', - content: [ - { - text: 'What is on the attached image?', - }, - { - image: { - format: 'png', - source: { bytes: imageSource }, - }, - }, - ], + text: 'What is on the attached image?', + }, + { + image: { + format: 'png', + source: { bytes: imageSource }, + }, }, ], + }; + + // send event + const event: ConversationTurnEvent = { + conversationId: message.conversationId, + currentMessageId: message.id, + graphqlApiEndpoint: graphqlApiEndpoint, request: { headers: { authorization: accessToken }, }, - ...commonEventProperties, + ...this.getCommonEventProperties(streamResponse), }; + await this.insertMessage(apolloClient, message); const response = await this.executeConversationTurn( event, defaultConversationHandlerFunction, @@ -331,7 +499,8 @@ class ConversationHandlerTestProject extends TestProjectBase { backendId: BackendIdentifier, accessToken: string, graphqlApiEndpoint: string, - apolloClient: ApolloClient + apolloClient: ApolloClient, + streamResponse: boolean ): Promise => { const defaultConversationHandlerFunction = ( await this.resourceFinder.findByBackendIdentifier( @@ -341,21 +510,23 @@ class ConversationHandlerTestProject extends TestProjectBase { ) )[0]; - // send event - const event: ConversationTurnEvent = { + const message: CreateConversationMessageChatInput = { conversationId: randomUUID().toString(), - currentMessageId: randomUUID().toString(), - graphqlApiEndpoint: graphqlApiEndpoint, - messages: [ + id: randomUUID().toString(), + role: 'user', + content: [ { - role: 'user', - content: [ - { - text: 'What is the temperature in Seattle?', - }, - ], + text: 'What is the temperature in Seattle?', }, ], + }; + await this.insertMessage(apolloClient, message); + + // send event + const event: ConversationTurnEvent = { + conversationId: message.conversationId, + currentMessageId: message.id, + graphqlApiEndpoint: graphqlApiEndpoint, request: { headers: { authorization: accessToken }, }, @@ -386,7 +557,7 @@ class ConversationHandlerTestProject extends TestProjectBase { }, ], }, - ...commonEventProperties, + ...this.getCommonEventProperties(streamResponse), }; const response = await this.executeConversationTurn( event, @@ -404,7 +575,8 @@ class ConversationHandlerTestProject extends TestProjectBase { backendId: BackendIdentifier, accessToken: string, graphqlApiEndpoint: string, - apolloClient: ApolloClient + apolloClient: ApolloClient, + streamResponse: boolean ): Promise => { const defaultConversationHandlerFunction = ( await this.resourceFinder.findByBackendIdentifier( @@ -414,21 +586,23 @@ class ConversationHandlerTestProject extends TestProjectBase { ) )[0]; - // send event - const event: ConversationTurnEvent = { + const message: CreateConversationMessageChatInput = { conversationId: randomUUID().toString(), - currentMessageId: randomUUID().toString(), - graphqlApiEndpoint: graphqlApiEndpoint, - messages: [ + id: randomUUID().toString(), + role: 'user', + content: [ { - role: 'user', - content: [ - { - text: 'What is the temperature in Seattle?', - }, - ], + text: 'What is the temperature in Seattle?', }, ], + }; + await this.insertMessage(apolloClient, message); + + // send event + const event: ConversationTurnEvent = { + conversationId: message.conversationId, + currentMessageId: message.id, + graphqlApiEndpoint: graphqlApiEndpoint, request: { headers: { authorization: accessToken }, }, @@ -452,7 +626,7 @@ class ConversationHandlerTestProject extends TestProjectBase { }, ], }, - ...commonEventProperties, + ...this.getCommonEventProperties(streamResponse), }; const response = await this.executeConversationTurn( event, @@ -468,11 +642,181 @@ class ConversationHandlerTestProject extends TestProjectBase { assert.match(response.content, /"city":"Seattle"/); }; + private executeConversationTurn = async ( + event: ConversationTurnEvent, + functionName: string, + apolloClient: ApolloClient + ): Promise<{ + content: string; + errors?: Array; + }> => { + console.log( + `Sending event conversationId=${event.conversationId} currentMessageId=${event.currentMessageId}` + ); + await this.lambdaClient.send( + new InvokeCommand({ + FunctionName: functionName, + Payload: Buffer.from(JSON.stringify(event)), + }) + ); + + // assert that response came back + if (event.streamResponse) { + let nextToken: string | undefined; + const chunks: Array = []; + do { + const queryResult = await apolloClient.query<{ + listConversationMessageAssistantStreamingResponses: { + items: Array; + nextToken: string | undefined; + }; + }>({ + query: gql` + query ListMessageChunks( + $conversationId: ID + $associatedUserMessageId: ID + $nextToken: String + ) { + listConversationMessageAssistantStreamingResponses( + limit: 1000 + nextToken: $nextToken + filter: { + conversationId: { eq: $conversationId } + associatedUserMessageId: { eq: $associatedUserMessageId } + } + ) { + items { + associatedUserMessageId + contentBlockDeltaIndex + contentBlockDoneAtIndex + contentBlockIndex + contentBlockText + contentBlockToolUse + conversationId + createdAt + errors { + errorType + message + } + id + owner + stopReason + updatedAt + } + nextToken + } + } + `, + variables: { + conversationId: event.conversationId, + associatedUserMessageId: event.currentMessageId, + nextToken, + }, + fetchPolicy: 'no-cache', + }); + nextToken = + queryResult.data.listConversationMessageAssistantStreamingResponses + .nextToken; + chunks.push( + ...queryResult.data.listConversationMessageAssistantStreamingResponses + .items + ); + } while (nextToken); + + assert.ok(chunks); + + if (chunks.length === 1 && chunks[0].errors) { + return { + content: '', + errors: chunks[0].errors, + }; + } + + chunks.sort((a, b) => { + // This is very simplified sort by message,block and delta indexes; + let aValue = 1000 * 1000 * a.contentBlockIndex; + if (a.contentBlockDeltaIndex) { + aValue += a.contentBlockDeltaIndex; + } + let bValue = 1000 * 1000 * b.contentBlockIndex; + if (b.contentBlockDeltaIndex) { + bValue += b.contentBlockDeltaIndex; + } + return aValue - bValue; + }); + + const content = chunks.reduce((accumulated, current) => { + if (current.contentBlockText) { + accumulated += current.contentBlockText; + } + if (current.contentBlockToolUse) { + accumulated += current.contentBlockToolUse; + } + return accumulated; + }, ''); + + return { content }; + } + const queryResult = await apolloClient.query<{ + listConversationMessageAssistantResponses: { + items: Array; + }; + }>({ + query: gql` + query ListMessage($conversationId: ID, $associatedUserMessageId: ID) { + listConversationMessageAssistantResponses( + filter: { + conversationId: { eq: $conversationId } + associatedUserMessageId: { eq: $associatedUserMessageId } + } + limit: 1000 + ) { + items { + conversationId + id + updatedAt + createdAt + content + errors { + errorType + message + } + associatedUserMessageId + } + nextToken + } + } + `, + variables: { + conversationId: event.conversationId, + associatedUserMessageId: event.currentMessageId, + }, + fetchPolicy: 'no-cache', + }); + assert.strictEqual( + 1, + queryResult.data.listConversationMessageAssistantResponses.items.length + ); + const response = + queryResult.data.listConversationMessageAssistantResponses.items[0]; + + if (response.errors) { + return { + content: '', + errors: response.errors, + }; + } + + assert.ok(response.content); + return { content: response.content }; + }; + private assertCustomConversationHandlerCanExecuteTurn = async ( backendId: BackendIdentifier, accessToken: string, graphqlApiEndpoint: string, - apolloClient: ApolloClient + apolloClient: ApolloClient, + streamResponse: boolean ): Promise => { const customConversationHandlerFunction = ( await this.resourceFinder.findByBackendIdentifier( @@ -482,25 +826,27 @@ class ConversationHandlerTestProject extends TestProjectBase { ) )[0]; - // send event - const event: ConversationTurnEvent = { + const message: CreateConversationMessageChatInput = { conversationId: randomUUID().toString(), - currentMessageId: randomUUID().toString(), - graphqlApiEndpoint: graphqlApiEndpoint, - messages: [ + id: randomUUID().toString(), + role: 'user', + content: [ { - role: 'user', - content: [ - { - text: 'What is the temperature in Seattle?', - }, - ], + text: 'What is the temperature in Seattle and Boston?', }, ], + }; + await this.insertMessage(apolloClient, message); + + // send event + const event: ConversationTurnEvent = { + conversationId: message.conversationId, + currentMessageId: message.id, + graphqlApiEndpoint: graphqlApiEndpoint, request: { headers: { authorization: accessToken }, }, - ...commonEventProperties, + ...this.getCommonEventProperties(streamResponse), }; const response = await this.executeConversationTurn( event, @@ -510,51 +856,209 @@ class ConversationHandlerTestProject extends TestProjectBase { // Assert that tool was used. I.e. LLM used value provided by the tool. assert.match( response.content, - new RegExp(expectedTemperatureInProgrammaticToolScenario.toString()) + new RegExp( + expectedTemperaturesInProgrammaticToolScenario.Seattle.toString() + ) + ); + assert.match( + response.content, + new RegExp( + expectedTemperaturesInProgrammaticToolScenario.Boston.toString() + ) ); }; - private executeConversationTurn = async ( - event: ConversationTurnEvent, - functionName: string, - apolloClient: ApolloClient - ): Promise => { - await this.lambdaClient.send( - new InvokeCommand({ - FunctionName: functionName, - Payload: Buffer.from(JSON.stringify(event)), - }) - ); + private assertCustomConversationHandlerCanExecuteTurnWithParameterLessTool = + async ( + backendId: BackendIdentifier, + accessToken: string, + graphqlApiEndpoint: string, + apolloClient: ApolloClient, + streamResponse: boolean, + attempt: number + ): Promise => { + const customConversationHandlerFunction = ( + await this.resourceFinder.findByBackendIdentifier( + backendId, + 'AWS::Lambda::Function', + (name) => name.includes('custom') + ) + )[0]; - // assert that response came back + // Try different questions on retry. + // Retrying same question in narrow time frame usually yields same answer. + const questions = [ + 'Give me a random number', + 'Give me a random number please', + 'Can you please give me a random number', + 'Generate and print random number', + ]; + const question = questions[attempt % questions.length]; - const queryResult = await apolloClient.query<{ - listConversationMessageAssistantResponses: { - items: Array; + const message: CreateConversationMessageChatInput = { + conversationId: randomUUID().toString(), + id: randomUUID().toString(), + role: 'user', + content: [ + { + text: question, + }, + ], }; - }>({ - query: gql` - query ListMessages { - listConversationMessageAssistantResponses { - items { - conversationId - sender - id - updatedAt - createdAt - content - associatedUserMessageId - } + await this.insertMessage(apolloClient, message); + + // send event + const event: ConversationTurnEvent = { + conversationId: message.conversationId, + currentMessageId: message.id, + graphqlApiEndpoint: graphqlApiEndpoint, + request: { + headers: { authorization: accessToken }, + }, + ...this.getCommonEventProperties(streamResponse), + }; + const response = await this.executeConversationTurn( + event, + customConversationHandlerFunction, + apolloClient + ); + // Assert that tool was used. I.e. LLM used value provided by the tool. + assert.match( + response.content, + new RegExp(expectedRandomNumber.toString()) + ); + }; + + private assertDefaultConversationHandlerCanPropagateError = async ( + backendId: BackendIdentifier, + accessToken: string, + graphqlApiEndpoint: string, + apolloClient: ApolloClient, + streamResponse: boolean, + attempt: number + ): Promise => { + const defaultConversationHandlerFunction = ( + await this.resourceFinder.findByBackendIdentifier( + backendId, + 'AWS::Lambda::Function', + (name) => name.includes('default') + ) + )[0]; + + // Try different questions on retry. + // Retrying same question in narrow time frame usually yields same answer. + const questions = [ + 'What is the value of PI?', + 'Give me the value of PI', + 'Give me the value of PI please', + 'Can you please give me the value of PI?', + ]; + const question = questions[attempt % questions.length]; + + const message: CreateConversationMessageChatInput = { + id: randomUUID().toString(), + conversationId: randomUUID().toString(), + role: 'user', + content: [ + { + text: question, + }, + ], + }; + + // send event + const event: ConversationTurnEvent = { + conversationId: message.conversationId, + currentMessageId: message.id, + graphqlApiEndpoint: graphqlApiEndpoint, + request: { + headers: { authorization: accessToken }, + }, + ...this.getCommonEventProperties(streamResponse), + }; + + // Inject failure + event.modelConfiguration.modelId = 'invalidId'; + await this.insertMessage(apolloClient, message); + + const response = await this.executeConversationTurn( + event, + defaultConversationHandlerFunction, + apolloClient + ); + assert.ok(response.errors); + assert.ok(response.errors[0]); + assert.strictEqual(response.errors[0].errorType, 'ValidationException'); + assert.match( + response.errors[0].message, + /provided model identifier is invalid/ + ); + }; + + private insertMessage = async ( + apolloClient: ApolloClient, + message: CreateConversationMessageChatInput + ): Promise => { + await apolloClient.mutate({ + mutation: gql` + mutation InsertMessage($input: CreateConversationMessageChatInput!) { + createConversationMessageChat(input: $input) { + id } } `, - fetchPolicy: 'no-cache', + variables: { + input: message, + }, }); - const response = - queryResult.data.listConversationMessageAssistantResponses.items.find( - (item) => item.associatedUserMessageId === event.currentMessageId - ); - assert.ok(response); - return response; + }; + + private getCommonEventProperties = (streamResponse: boolean) => { + const responseMutation = streamResponse + ? { + name: 'createConversationMessageAssistantStreamingResponse', + inputTypeName: + 'CreateConversationMessageAssistantStreamingResponseInput', + selectionSet: ['id', 'conversationId', 'createdAt', 'updatedAt'].join( + '\n' + ), + } + : { + name: 'createConversationMessageAssistantResponse', + inputTypeName: 'CreateConversationMessageAssistantResponseInput', + selectionSet: [ + 'id', + 'conversationId', + 'content', + 'owner', + 'createdAt', + 'updatedAt', + ].join('\n'), + }; + return { + streamResponse, + responseMutation, + messageHistoryQuery: { + getQueryName: 'getConversationMessageChat', + getQueryInputTypeName: 'ID', + listQueryName: 'listConversationMessageChats', + listQueryInputTypeName: 'ModelConversationMessageChatFilterInput', + }, + modelConfiguration: { + modelId: bedrockModelId, + systemPrompt: 'You are helpful bot.', + }, + }; + }; + + /** + * Bedrock sometimes produces empty response or half backed response. + * On the other hand we have to run some assertions on those responses. + * Therefore, we wrap transactions in retry loop. + */ + private executeWithRetry = async ( + callable: (attempt: number) => Promise + ) => { + await runWithRetry(callable, () => true, 4); }; } diff --git a/packages/integration-tests/src/test-project-setup/create_empty_amplify_project.ts b/packages/integration-tests/src/test-project-setup/create_empty_amplify_project.ts index 97afcf198b..b86f280f32 100644 --- a/packages/integration-tests/src/test-project-setup/create_empty_amplify_project.ts +++ b/packages/integration-tests/src/test-project-setup/create_empty_amplify_project.ts @@ -19,7 +19,12 @@ export const createEmptyAmplifyProject = async ( projectDotAmplifyDir: string; }> => { const projectRoot = await fs.mkdtemp(path.join(parentDir, projectDirName)); - const projectName = `${TEST_PROJECT_PREFIX}-${projectDirName}-${shortUuid()}`; + let projectName = `${TEST_PROJECT_PREFIX}-${projectDirName}`; + if ( + process.env.AMPLIFY_BACKEND_TESTS_RETAIN_TEST_PROJECT_DEPLOYMENT !== 'true' + ) { + projectName += `-${shortUuid()}`; + } await fs.writeFile( path.join(projectRoot, 'package.json'), JSON.stringify({ name: projectName, type: 'module' }, null, 2) diff --git a/packages/integration-tests/src/test-project-setup/custom_outputs.ts b/packages/integration-tests/src/test-project-setup/custom_outputs.ts index 7e6b367e5a..0a20230e25 100644 --- a/packages/integration-tests/src/test-project-setup/custom_outputs.ts +++ b/packages/integration-tests/src/test-project-setup/custom_outputs.ts @@ -12,6 +12,7 @@ import { } from '@aws-amplify/client-config'; import assert from 'node:assert'; import { AmplifyClient } from '@aws-sdk/client-amplify'; +import { e2eToolingClientConfig } from '../e2e_tooling_client_config.js'; /** * Creates minimal test projects with custom outputs. @@ -23,8 +24,12 @@ export class CustomOutputsTestProjectCreator implements TestProjectCreator { * Creates project creator. */ constructor( - private readonly cfnClient: CloudFormationClient, - private readonly amplifyClient: AmplifyClient + private readonly cfnClient: CloudFormationClient = new CloudFormationClient( + e2eToolingClientConfig + ), + private readonly amplifyClient: AmplifyClient = new AmplifyClient( + e2eToolingClientConfig + ) ) {} createProject = async (e2eProjectDir: string): Promise => { diff --git a/packages/integration-tests/src/test-project-setup/data_storage_auth_with_triggers.ts b/packages/integration-tests/src/test-project-setup/data_storage_auth_with_triggers.ts index 588f5c7f56..5567a5958e 100644 --- a/packages/integration-tests/src/test-project-setup/data_storage_auth_with_triggers.ts +++ b/packages/integration-tests/src/test-project-setup/data_storage_auth_with_triggers.ts @@ -1,5 +1,5 @@ import fs from 'fs/promises'; -import { SecretClient } from '@aws-amplify/backend-secret'; +import { SecretClient, getSecretClient } from '@aws-amplify/backend-secret'; import { BackendIdentifier } from '@aws-amplify/plugin-types'; import { createEmptyAmplifyProject } from './create_empty_amplify_project.js'; import { CloudFormationClient } from '@aws-sdk/client-cloudformation'; @@ -17,12 +17,13 @@ import { import { HeadBucketCommand, S3Client } from '@aws-sdk/client-s3'; import { GetRoleCommand, IAMClient } from '@aws-sdk/client-iam'; import { AmplifyClient } from '@aws-sdk/client-amplify'; + import { - DeleteMessageCommand, - ReceiveMessageCommand, - SQSClient, -} from '@aws-sdk/client-sqs'; + CloudTrailClient, + LookupEventsCommand, +} from '@aws-sdk/client-cloudtrail'; import { e2eToolingClientConfig } from '../e2e_tooling_client_config.js'; +import isMatch from 'lodash.ismatch'; /** * Creates test projects with data, storage, and auth categories. @@ -36,14 +37,26 @@ export class DataStorageAuthWithTriggerTestProjectCreator * Creates project creator. */ constructor( - private readonly cfnClient: CloudFormationClient, - private readonly amplifyClient: AmplifyClient, - private readonly secretClient: SecretClient, - private readonly lambdaClient: LambdaClient, - private readonly s3Client: S3Client, - private readonly iamClient: IAMClient, - private readonly sqsClient: SQSClient, - private readonly resourceFinder: DeployedResourcesFinder + private readonly cfnClient: CloudFormationClient = new CloudFormationClient( + e2eToolingClientConfig + ), + private readonly amplifyClient: AmplifyClient = new AmplifyClient( + e2eToolingClientConfig + ), + private readonly secretClient: SecretClient = getSecretClient( + e2eToolingClientConfig + ), + private readonly lambdaClient: LambdaClient = new LambdaClient( + e2eToolingClientConfig + ), + private readonly s3Client: S3Client = new S3Client(e2eToolingClientConfig), + private readonly iamClient: IAMClient = new IAMClient( + e2eToolingClientConfig + ), + private readonly cloudTrailClient: CloudTrailClient = new CloudTrailClient( + e2eToolingClientConfig + ), + private readonly resourceFinder: DeployedResourcesFinder = new DeployedResourcesFinder() ) {} createProject = async (e2eProjectDir: string): Promise => { @@ -60,7 +73,7 @@ export class DataStorageAuthWithTriggerTestProjectCreator this.lambdaClient, this.s3Client, this.iamClient, - this.sqsClient, + this.cloudTrailClient, this.resourceFinder ); await fs.cp( @@ -80,7 +93,7 @@ export class DataStorageAuthWithTriggerTestProjectCreator */ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { // Note that this is pointing to the non-compiled project directory - // This allows us to test that we are able to deploy js, cjs, ts, etc without compiling with tsc first + // This allows us to test that we are able to deploy js, cjs, ts, etc. without compiling with tsc first readonly sourceProjectRootPath = '../../src/test-projects/data-storage-auth-with-triggers-ts'; @@ -126,7 +139,7 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { private readonly lambdaClient: LambdaClient, private readonly s3Client: S3Client, private readonly iamClient: IAMClient, - private readonly sqsClient: SQSClient, + private readonly cloudTrailClient: CloudTrailClient, private readonly resourceFinder: DeployedResourcesFinder ) { super( @@ -150,10 +163,12 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { ? environment[amplifySharedSecretNameKey] : createAmplifySharedSecretName(); const { region } = e2eToolingClientConfig; - const env = { + const env: Record = { [amplifySharedSecretNameKey]: this.amplifySharedSecret, - AWS_REGION: region ?? '', }; + if (region) { + env.AWS_REGION = region; + } await this.setUpDeployEnvironment(backendIdentifier); await super.deploy(backendIdentifier, env); @@ -212,29 +227,8 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { (name) => name.includes('node16Function') ); - const funcWithSsm = await this.resourceFinder.findByBackendIdentifier( - backendId, - 'AWS::Lambda::Function', - (name) => name.includes('funcWithSsm') - ); - - const funcWithAwsSdk = await this.resourceFinder.findByBackendIdentifier( - backendId, - 'AWS::Lambda::Function', - (name) => name.includes('funcWithAwsSdk') - ); - - const funcWithSchedule = await this.resourceFinder.findByBackendIdentifier( - backendId, - 'AWS::Lambda::Function', - (name) => name.includes('funcWithSchedule') - ); - assert.equal(defaultNodeLambda.length, 1); assert.equal(node16Lambda.length, 1); - assert.equal(funcWithSsm.length, 1); - assert.equal(funcWithAwsSdk.length, 1); - assert.equal(funcWithSchedule.length, 1); const expectedResponse = { s3TestContent: 'this is some test content', @@ -245,10 +239,6 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { await this.checkLambdaResponse(defaultNodeLambda[0], expectedResponse); await this.checkLambdaResponse(node16Lambda[0], expectedResponse); - await this.checkLambdaResponse(funcWithSsm[0], 'It is working'); - await this.checkLambdaResponse(funcWithAwsSdk[0], 'It is working'); - - await this.assertScheduleInvokesFunction(backendId); const bucketName = await this.resourceFinder.findByBackendIdentifier( backendId, @@ -298,6 +288,46 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { ); assert.ok(fileContent.includes('newKey: string;')); // Env var added via addEnvironment assert.ok(fileContent.includes('TEST_SECRET: string;')); // Env var added via defineFunction + + // assert specific config are correct in the outputs file + const outputsObject = JSON.parse( + await fs.readFile( + path.join(this.projectDirPath, 'amplify_outputs.json'), + 'utf-8' + ) + ); + assert.ok( + isMatch(outputsObject.storage.buckets[0].paths, { + 'public/*': { + guest: ['get', 'list'], + authenticated: ['get', 'list', 'write'], + groupsAdmins: ['get', 'list', 'write', 'delete'], + }, + 'protected/*': { + authenticated: ['get', 'list'], + groupsAdmins: ['get', 'list', 'write', 'delete'], + }, + 'protected/${cognito-identity.amazonaws.com:sub}/*': { + // eslint-disable-next-line spellcheck/spell-checker + entityidentity: ['get', 'list', 'write', 'delete'], + }, + }) + ); + + assert.ok( + isMatch(outputsObject.auth.groups, [ + { + Editors: { + precedence: 2, // previously 0 but was overwritten + }, + }, + { + Admins: { + precedence: 1, + }, + }, + ]) + ); } private getUpdateReplacementDefinition = (suffix: string) => ({ @@ -364,21 +394,42 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { /** * There is some eventual consistency between deleting a bucket and when HeadBucket returns NotFound - * So we are polling HeadBucket until it returns NotFound or until we time out (after 30 seconds) + * So we are polling HeadBucket and CloudTrail events + * until it returns NotFound or until we time out (after 3 minutes) */ private waitForBucketDeletion = async (bucketName: string): Promise => { - const TIMEOUT_MS = 1000 * 30; // 30 seconds + // Poll for 3 minutes. + // If HeadBucket doesn't become eventually consistent then + // there's at least pretty good chance that BucketDelete event + // managed to arrive at CloudTrail. + const TIMEOUT_MS = 1000 * 60 * 3; const startTime = Date.now(); - while (Date.now() - startTime < TIMEOUT_MS) { + let elapsedTimeMs = 0; + let pollingIntervalMs = 1000; + do { const bucketExists = await this.checkBucketExists(bucketName); if (!bucketExists) { // bucket has been deleted return; } - // wait a second before polling again - await new Promise((resolve) => setTimeout(resolve, 1000)); - } + // Start querying Cloud Trail after a minute. + // So that we don't burn down request quota unnecessarily. + if (elapsedTimeMs >= 1000 * 60) { + // Bump polling interval to wait 10 seconds before polling again. + // Cloud trail has low TPS quota. + pollingIntervalMs = 10000; + const deleteBucketEventArrived = + await this.checkIfDeleteBucketEventArrived(bucketName); + if (deleteBucketEventArrived) { + // bucket has been deleted + return; + } + } + + await new Promise((resolve) => setTimeout(resolve, pollingIntervalMs)); + elapsedTimeMs = Date.now() - startTime; + } while (elapsedTimeMs < TIMEOUT_MS); assert.fail(`Timed out waiting for ${bucketName} to be deleted`); }; @@ -395,6 +446,39 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { } }; + private checkIfDeleteBucketEventArrived = async ( + bucketName: string + ): Promise => { + try { + const lookupEventsResponse = await this.cloudTrailClient.send( + new LookupEventsCommand({ + LookupAttributes: [ + { + AttributeKey: 'EventName', + AttributeValue: 'DeleteBucket', + }, + { + AttributeKey: 'ResourceType', + AttributeValue: 'AWS::S3::Bucket', + }, + { + AttributeKey: 'ResourceName', + AttributeValue: bucketName, + }, + ], + }) + ); + return (lookupEventsResponse.Events?.length ?? 0) > 0; + } catch (err) { + if (err instanceof Error && err.name === 'ThrottlingException') { + // This is a best effort check. + // If we get throttled pretend that we haven't seen event yet. + return false; + } + throw err; + } + }; + private assertRolesDoNotExist = async (roleNames: string[]) => { const TIMEOUT_MS = 1000 * 60 * 5; // IAM Role stabilization can take up to 2 minutes and we are waiting in between each GetRole call to avoid throttling const startTime = Date.now(); @@ -444,42 +528,4 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { throw err; } }; - - private assertScheduleInvokesFunction = async ( - backendId: BackendIdentifier - ) => { - const TIMEOUT_MS = 1000 * 60 * 2; // 2 minutes - const startTime = Date.now(); - let messageCount = 0; - - const queue = await this.resourceFinder.findByBackendIdentifier( - backendId, - 'AWS::SQS::Queue', - (name) => name.includes('testFuncQueue') - ); - - // wait for schedule to invoke the function one time for it to send a message - while (Date.now() - startTime < TIMEOUT_MS && messageCount < 1) { - const response = await this.sqsClient.send( - new ReceiveMessageCommand({ - QueueUrl: queue[0], - WaitTimeSeconds: 20, - }) - ); - - if (response.Messages) { - messageCount += response.Messages.length; - - // delete messages afterwards - for (const message of response.Messages) { - await this.sqsClient.send( - new DeleteMessageCommand({ - QueueUrl: queue[0], - ReceiptHandle: message.ReceiptHandle, - }) - ); - } - } - } - }; } diff --git a/packages/integration-tests/src/test-project-setup/minimal_with_typescript_idioms.ts b/packages/integration-tests/src/test-project-setup/minimal_with_typescript_idioms.ts index 983f11a211..b7c1886df1 100644 --- a/packages/integration-tests/src/test-project-setup/minimal_with_typescript_idioms.ts +++ b/packages/integration-tests/src/test-project-setup/minimal_with_typescript_idioms.ts @@ -4,6 +4,7 @@ import { createEmptyAmplifyProject } from './create_empty_amplify_project.js'; import { CloudFormationClient } from '@aws-sdk/client-cloudformation'; import { TestProjectCreator } from './test_project_creator.js'; import { AmplifyClient } from '@aws-sdk/client-amplify'; +import { e2eToolingClientConfig } from '../e2e_tooling_client_config.js'; /** * Creates minimal test projects with typescript idioms. @@ -17,8 +18,12 @@ export class MinimalWithTypescriptIdiomTestProjectCreator * Creates project creator. */ constructor( - private readonly cfnClient: CloudFormationClient, - private readonly amplifyClient: AmplifyClient + private readonly cfnClient: CloudFormationClient = new CloudFormationClient( + e2eToolingClientConfig + ), + private readonly amplifyClient: AmplifyClient = new AmplifyClient( + e2eToolingClientConfig + ) ) {} createProject = async (e2eProjectDir: string): Promise => { diff --git a/packages/integration-tests/src/test-project-setup/reference_auth_project.ts b/packages/integration-tests/src/test-project-setup/reference_auth_project.ts new file mode 100644 index 0000000000..66c893c9c1 --- /dev/null +++ b/packages/integration-tests/src/test-project-setup/reference_auth_project.ts @@ -0,0 +1,340 @@ +import { TestProjectBase } from './test_project_base.js'; +import fsp from 'fs/promises'; +import { createEmptyAmplifyProject } from './create_empty_amplify_project.js'; +import { CloudFormationClient } from '@aws-sdk/client-cloudformation'; +import { TestProjectCreator } from './test_project_creator.js'; +import { AmplifyClient } from '@aws-sdk/client-amplify'; +import { AuthResourceCreator } from '../resource-creation/auth_resource_creator.js'; +import { CognitoIdentityProviderClient } from '@aws-sdk/client-cognito-identity-provider'; +import { CognitoIdentityClient } from '@aws-sdk/client-cognito-identity'; +import { IAMClient } from '@aws-sdk/client-iam'; +import { BackendIdentifier } from '@aws-amplify/plugin-types'; +import { e2eToolingClientConfig } from '../e2e_tooling_client_config.js'; + +/** + * Creates a reference auth project + */ +export class ReferenceAuthTestProjectCreator implements TestProjectCreator { + readonly name = 'reference-auth'; + + /** + * Creates project creator. + */ + constructor( + private readonly cfnClient: CloudFormationClient = new CloudFormationClient( + e2eToolingClientConfig + ), + private readonly amplifyClient: AmplifyClient = new AmplifyClient( + e2eToolingClientConfig + ), + private readonly cognitoIdentityProviderClient: CognitoIdentityProviderClient = new CognitoIdentityProviderClient( + e2eToolingClientConfig + ), + private readonly cognitoIdentityClient: CognitoIdentityClient = new CognitoIdentityClient( + e2eToolingClientConfig + ), + private readonly iamClient: IAMClient = new IAMClient( + e2eToolingClientConfig + ) + ) {} + + createProject = async (e2eProjectDir: string): Promise => { + const { projectName, projectRoot, projectAmplifyDir } = + await createEmptyAmplifyProject(this.name, e2eProjectDir); + + const project = new ReferenceAuthTestProject( + projectName, + projectRoot, + projectAmplifyDir, + this.cfnClient, + this.amplifyClient, + this.cognitoIdentityProviderClient, + this.cognitoIdentityClient, + this.iamClient + ); + + await fsp.cp( + project.sourceProjectAmplifyDirURL, + project.projectAmplifyDirPath, + { + recursive: true, + } + ); + + // generate resources + const { + userPool, + userPoolClient, + identityPool, + authRole, + unauthRole, + adminGroup, + } = await project.setupTestResources(); + // copy generated resource ids into project's auth/resource.ts file + const authResourceFilePath = `${project.projectAmplifyDirPath}/auth/resource.ts`; + await fsp.writeFile( + authResourceFilePath, + `import { referenceAuth } from '@aws-amplify/backend'; + import { addUserToGroup } from "../data/add-user-to-group/resource.js"; + + export const auth = referenceAuth({ + identityPoolId: "${identityPool.IdentityPoolId}", + authRoleArn: "${authRole.Arn}", + unauthRoleArn: "${unauthRole.Arn}", + userPoolId: "${userPool.Id}", + userPoolClientId: "${userPoolClient.ClientId}", + groups: { + "ADMINS": '${adminGroup.RoleArn}', + }, + access: (allow) => [ + allow.resource(addUserToGroup).to(["addUserToGroup"]) + ], + })` + ); + return project; + }; +} + +/** + * The minimal test with typescript idioms. + */ +class ReferenceAuthTestProject extends TestProjectBase { + readonly sourceProjectDirPath = '../../src/test-projects/reference-auth'; + + readonly sourceProjectAmplifyDirSuffix = `${this.sourceProjectDirPath}/amplify`; + + readonly sourceProjectAmplifyDirURL: URL = new URL( + this.sourceProjectAmplifyDirSuffix, + import.meta.url + ); + + authResourceCreator: AuthResourceCreator; + + /** + * Create a test project instance. + */ + constructor( + name: string, + projectDirPath: string, + projectAmplifyDirPath: string, + cfnClient: CloudFormationClient, + amplifyClient: AmplifyClient, + cognitoIdentityProviderClient: CognitoIdentityProviderClient, + private cognitoIdentityClient: CognitoIdentityClient, + iamClient: IAMClient + ) { + super( + name, + projectDirPath, + projectAmplifyDirPath, + cfnClient, + amplifyClient + ); + this.authResourceCreator = new AuthResourceCreator( + cognitoIdentityProviderClient, + cognitoIdentityClient, + iamClient + ); + } + + setupTestResources = async () => { + try { + const userPool = await this.authResourceCreator.createUserPoolBase({ + PoolName: `RefUserPool`, + AccountRecoverySetting: { + RecoveryMechanisms: [ + { + Name: 'verified_email', + Priority: 1, + }, + ], + }, + AdminCreateUserConfig: { + AllowAdminCreateUserOnly: false, + }, + AutoVerifiedAttributes: ['email'], + UserAttributeUpdateSettings: { + AttributesRequireVerificationBeforeUpdate: ['email'], + }, + EmailConfiguration: { + EmailSendingAccount: 'COGNITO_DEFAULT', + }, + Schema: [ + { + Name: 'email', + Required: true, + }, + ], + Policies: { + PasswordPolicy: { + MinimumLength: 8, + RequireUppercase: true, + RequireLowercase: true, + RequireNumbers: true, + RequireSymbols: true, + TemporaryPasswordValidityDays: 7, + }, + }, + UsernameAttributes: ['email'], + UsernameConfiguration: { + CaseSensitive: false, + }, + MfaConfiguration: 'OFF', + DeletionProtection: 'INACTIVE', + }); + + const domain = await this.authResourceCreator.createUserPoolDomainBase({ + UserPoolId: userPool.Id, + Domain: `ref-auth`, + }); + + await this.authResourceCreator.createIdentityProviderBase({ + UserPoolId: userPool.Id, + ProviderType: 'Facebook', + ProviderDetails: { + client_id: 'clientId', + client_secret: 'clientSecret', + authorize_scopes: 'openid,email', + api_version: 'v17.0', + }, + AttributeMapping: { + email: 'email', + }, + ProviderName: 'Facebook', + }); + + await this.authResourceCreator.createIdentityProviderBase({ + UserPoolId: userPool.Id, + ProviderType: 'Google', + ProviderDetails: { + client_id: 'clientId', + client_secret: 'clientSecret', + authorize_scopes: 'openid,email', + }, + AttributeMapping: { + email: 'email', + }, + ProviderName: 'Google', + }); + + const userPoolClient = + await this.authResourceCreator.createUserPoolClientBase({ + ClientName: `ref-auth-client`, + UserPoolId: userPool.Id, + ExplicitAuthFlows: [ + 'ALLOW_REFRESH_TOKEN_AUTH', + 'ALLOW_USER_SRP_AUTH', + ], + AuthSessionValidity: 3, + RefreshTokenValidity: 30, + AccessTokenValidity: 60, + IdTokenValidity: 60, + TokenValidityUnits: { + RefreshToken: 'days', + AccessToken: 'minutes', + IdToken: 'minutes', + }, + EnableTokenRevocation: true, + PreventUserExistenceErrors: 'ENABLED', + AllowedOAuthFlows: ['code'], + AllowedOAuthScopes: ['openid', 'phone', 'email'], + SupportedIdentityProviders: ['COGNITO', 'Facebook', 'Google'], + CallbackURLs: ['https://callback.com'], + LogoutURLs: ['https://logout.com'], + AllowedOAuthFlowsUserPoolClient: true, + GenerateSecret: false, + ReadAttributes: [ + 'address', + 'birthdate', + 'email', + 'email_verified', + 'family_name', + 'gender', + 'given_name', + 'locale', + 'middle_name', + 'name', + 'nickname', + 'phone_number', + 'phone_number_verified', + 'picture', + 'preferred_username', + 'profile', + 'updated_at', + 'website', + 'zoneinfo', + ], + WriteAttributes: [ + 'address', + 'birthdate', + 'email', + 'family_name', + 'gender', + 'given_name', + 'locale', + 'middle_name', + 'name', + 'nickname', + 'phone_number', + 'picture', + 'preferred_username', + 'profile', + 'updated_at', + 'website', + 'zoneinfo', + ], + }); + + const region = await this.cognitoIdentityClient.config.region(); + const identityPool = + await this.authResourceCreator.createIdentityPoolBase({ + AllowUnauthenticatedIdentities: true, + IdentityPoolName: `ref-auth-ip`, + AllowClassicFlow: false, + CognitoIdentityProviders: [ + { + ClientId: userPoolClient.ClientId, + ProviderName: `cognito-idp.${region}.amazonaws.com/${userPool.Id}`, + ServerSideTokenCheck: false, + }, + ], + SupportedLoginProviders: { + 'graph.facebook.com': 'clientId', + 'accounts.google.com': 'clientId', + }, + }); + + const roles = await this.authResourceCreator.setupIdentityPoolRoles( + userPool.Id!, + userPoolClient.ClientId!, + identityPool.IdentityPoolId + ); + + const adminGroup = await this.authResourceCreator.setupUserPoolGroup( + 'ADMINS', + userPool.Id!, + identityPool.IdentityPoolId + ); + return { + userPool, + userPoolClient, + domain, + identityPool, + authRole: roles.authRole, + unauthRole: roles.unauthRole, + adminGroup, + }; + } catch (e) { + await this.authResourceCreator.cleanupResources(); + throw e; + } + }; + + /** + * @inheritdoc + */ + override async tearDown(backendIdentifier: BackendIdentifier) { + await super.tearDown(backendIdentifier, true); + await this.authResourceCreator.cleanupResources(); + } +} diff --git a/packages/integration-tests/src/test-project-setup/setup_deployed_backend_client.ts b/packages/integration-tests/src/test-project-setup/setup_deployed_backend_client.ts index 618ec72b26..66704f60cf 100644 --- a/packages/integration-tests/src/test-project-setup/setup_deployed_backend_client.ts +++ b/packages/integration-tests/src/test-project-setup/setup_deployed_backend_client.ts @@ -1,4 +1,10 @@ import { execa } from 'execa'; +import fsp from 'fs/promises'; +import { fileURLToPath } from 'node:url'; + +const packageLockPath = fileURLToPath( + new URL('../../../../package-lock.json', import.meta.url) +); /** * Configures package.json for testing the specified project directory with the version of deployed-backend-client on npm @@ -9,4 +15,14 @@ export const setupDeployedBackendClient = async ( await execa('npm', ['install', '@aws-amplify/deployed-backend-client'], { cwd: projectRootDirPath, }); + + // Install constructs version that is matching our package lock. + // Otherwise, the test might fail due to incompatible properties + // when two definitions are present. + const packageLock = JSON.parse(await fsp.readFile(packageLockPath, 'utf-8')); + const constructsVersion = + packageLock.packages['node_modules/constructs'].version; + await execa('npm', ['install', `constructs@${constructsVersion}`], { + cwd: projectRootDirPath, + }); }; diff --git a/packages/integration-tests/src/test-project-setup/test_project_base.ts b/packages/integration-tests/src/test-project-setup/test_project_base.ts index c6ab0284fe..706d68114c 100644 --- a/packages/integration-tests/src/test-project-setup/test_project_base.ts +++ b/packages/integration-tests/src/test-project-setup/test_project_base.ts @@ -9,13 +9,14 @@ import { ampxCli } from '../process-controller/process_controller.js'; import { confirmDeleteSandbox, interruptSandbox, - rejectCleanupSandbox, waitForSandboxDeploymentToPrintTotalTime, } from '../process-controller/predicated_action_macros.js'; import { CloudFormationClient, + CloudFormationServiceException, DeleteStackCommand, + DescribeStacksCommand, } from '@aws-sdk/client-cloudformation'; import fsp from 'fs/promises'; import assert from 'node:assert'; @@ -77,7 +78,6 @@ export abstract class TestProjectBase { }) .do(waitForSandboxDeploymentToPrintTotalTime()) .do(interruptSandbox()) - .do(rejectCleanupSandbox()) .run(); } else { await ampxCli( @@ -102,19 +102,80 @@ export abstract class TestProjectBase { /** * Tear down the project. */ - async tearDown(backendIdentifier: BackendIdentifier) { + async tearDown( + backendIdentifier: BackendIdentifier, + waitForStackDeletion: boolean = false + ) { if (backendIdentifier.type === 'sandbox') { await ampxCli(['sandbox', 'delete'], this.projectDirPath) .do(confirmDeleteSandbox()) .run(); } else { + const stackName = + BackendIdentifierConversions.toStackName(backendIdentifier); await this.cfnClient.send( new DeleteStackCommand({ - StackName: - BackendIdentifierConversions.toStackName(backendIdentifier), + StackName: stackName, }) ); + if (waitForStackDeletion) { + await this.waitForStackDeletion(stackName); + } + } + } + + /** + * Wait for a stack to be deleted, returns true if deleted within allotted time. + * @param stackName name of the stack + * @returns true if delete completes within allotted time (3 minutes) + */ + async waitForStackDeletion( + stackName: string, + timeoutInMS: number = 3 * 60 * 1000 + ): Promise { + let attempts = 0; + let totalTimeWaitedMs = 0; + const maxIntervalMs = 32 * 1000; + while (totalTimeWaitedMs < timeoutInMS) { + attempts++; + const intervalMs = Math.min(Math.pow(2, attempts) * 1000, maxIntervalMs); + console.log(`waiting: ${intervalMs} milliseconds`); + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + totalTimeWaitedMs += intervalMs; + try { + const status = await this.cfnClient.send( + new DescribeStacksCommand({ + StackName: stackName, + }) + ); + console.log( + JSON.stringify(status.Stacks?.map((s) => s.StackName) ?? []) + ); + if (!status.Stacks || status.Stacks.length == 0) { + console.log(`Stack ${stackName} was deleted successfully.`); + return true; + } + } catch (e) { + if ( + e instanceof CloudFormationServiceException && + e.message.includes('does not exist') + ) { + console.log(`Stack ${stackName} was deleted successfully.`); + return true; + } + console.error( + `Could not describe stack ${stackName} while waiting for deletion.`, + e + ); + throw e; + } } + console.error( + `Stack ${stackName} did not delete within ${ + timeoutInMS / 1000 + } seconds, continuing.` + ); + return false; } /** diff --git a/packages/integration-tests/src/test-project-setup/test_project_creator.ts b/packages/integration-tests/src/test-project-setup/test_project_creator.ts index 4f8ad607a2..7327156759 100644 --- a/packages/integration-tests/src/test-project-setup/test_project_creator.ts +++ b/packages/integration-tests/src/test-project-setup/test_project_creator.ts @@ -1,75 +1,6 @@ import { TestProjectBase } from './test_project_base.js'; -import { CloudFormationClient } from '@aws-sdk/client-cloudformation'; -import { getSecretClient } from '@aws-amplify/backend-secret'; -import { DataStorageAuthWithTriggerTestProjectCreator } from './data_storage_auth_with_triggers.js'; -import { MinimalWithTypescriptIdiomTestProjectCreator } from './minimal_with_typescript_idioms.js'; -import { ConversationHandlerTestProjectCreator } from './conversation_handler_project.js'; -import { LambdaClient } from '@aws-sdk/client-lambda'; -import { DeployedResourcesFinder } from '../find_deployed_resource.js'; -import { e2eToolingClientConfig } from '../e2e_tooling_client_config.js'; -import { CustomOutputsTestProjectCreator } from './custom_outputs.js'; -import { S3Client } from '@aws-sdk/client-s3'; -import { IAMClient } from '@aws-sdk/client-iam'; -import { AccessTestingProjectTestProjectCreator } from './access_testing_project.js'; -import { CognitoIdentityProviderClient } from '@aws-sdk/client-cognito-identity-provider'; -import { CognitoIdentityClient } from '@aws-sdk/client-cognito-identity'; -import { STSClient } from '@aws-sdk/client-sts'; -import { AmplifyClient } from '@aws-sdk/client-amplify'; -import { SQSClient } from '@aws-sdk/client-sqs'; export type TestProjectCreator = { readonly name: string; createProject: (e2eProjectDir: string) => Promise; }; - -/** - * Generates a list of test projects. - */ -export const getTestProjectCreators = (): TestProjectCreator[] => { - const testProjectCreators: TestProjectCreator[] = []; - - const cfnClient = new CloudFormationClient(e2eToolingClientConfig); - const amplifyClient = new AmplifyClient(e2eToolingClientConfig); - const cognitoIdentityClient = new CognitoIdentityClient( - e2eToolingClientConfig - ); - const cognitoIdentityProviderClient = new CognitoIdentityProviderClient( - e2eToolingClientConfig - ); - const lambdaClient = new LambdaClient(e2eToolingClientConfig); - const s3Client = new S3Client(e2eToolingClientConfig); - const iamClient = new IAMClient(e2eToolingClientConfig); - const sqsClient = new SQSClient(e2eToolingClientConfig); - const resourceFinder = new DeployedResourcesFinder(cfnClient); - const stsClient = new STSClient(e2eToolingClientConfig); - const secretClient = getSecretClient(e2eToolingClientConfig); - testProjectCreators.push( - new DataStorageAuthWithTriggerTestProjectCreator( - cfnClient, - amplifyClient, - secretClient, - lambdaClient, - s3Client, - iamClient, - sqsClient, - resourceFinder - ), - new MinimalWithTypescriptIdiomTestProjectCreator(cfnClient, amplifyClient), - new CustomOutputsTestProjectCreator(cfnClient, amplifyClient), - new AccessTestingProjectTestProjectCreator( - cfnClient, - amplifyClient, - cognitoIdentityClient, - cognitoIdentityProviderClient, - stsClient - ), - new ConversationHandlerTestProjectCreator( - cfnClient, - amplifyClient, - lambdaClient, - cognitoIdentityProviderClient, - resourceFinder - ) - ); - return testProjectCreators; -}; diff --git a/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/auth/resource.ts b/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/auth/resource.ts new file mode 100644 index 0000000000..d4676a5390 --- /dev/null +++ b/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/auth/resource.ts @@ -0,0 +1,15 @@ +import { defineAuth } from '@aws-amplify/backend'; +import { funcCustomEmailSender } from '../function.js'; + +const customEmailSenderFunction = { + handler: funcCustomEmailSender, +}; + +export const auth = defineAuth({ + loginWith: { + email: true, + }, + senders: { + email: customEmailSenderFunction, + }, +}); diff --git a/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/backend.ts b/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/backend.ts new file mode 100644 index 0000000000..406c9390e8 --- /dev/null +++ b/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/backend.ts @@ -0,0 +1,48 @@ +import { defineBackend } from '@aws-amplify/backend'; +import { authAndFunctions } from './test_factories.js'; +import { Queue } from 'aws-cdk-lib/aws-sqs'; +import { Role } from 'aws-cdk-lib/aws-iam'; +import { Stack } from 'aws-cdk-lib'; + +const backend = defineBackend(authAndFunctions); + +const scheduleFunctionLambda = backend.funcWithSchedule.resources.lambda; +const scheduleFunctionLambdaRole = scheduleFunctionLambda.role; +const queueStack = Stack.of(scheduleFunctionLambda); + +const queue = new Queue(queueStack, 'amplify-testFuncQueue'); + +if (scheduleFunctionLambdaRole) { + queue.grantSendMessages( + Role.fromRoleArn( + queueStack, + 'LambdaExecutionRole', + scheduleFunctionLambdaRole.roleArn + ) + ); +} +backend.funcWithSchedule.addEnvironment('SQS_QUEUE_URL', queue.queueUrl); + +// Queue setup for customEmailSender + +const customEmailSenderLambda = backend.funcCustomEmailSender.resources.lambda; +const customEmailSenderLambdaRole = customEmailSenderLambda.role; +const customEmailSenderQueueStack = Stack.of(customEmailSenderLambda); +const emailSenderQueue = new Queue( + customEmailSenderQueueStack, + 'amplify-customEmailSenderQueue' +); + +if (customEmailSenderLambdaRole) { + emailSenderQueue.grantSendMessages( + Role.fromRoleArn( + customEmailSenderQueueStack, + 'CustomEmailSenderLambdaExecutionRole', + customEmailSenderLambdaRole.roleArn + ) + ); +} +backend.funcCustomEmailSender.addEnvironment( + 'CUSTOM_EMAIL_SENDER_SQS_QUEUE_URL', + emailSenderQueue.queueUrl +); diff --git a/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/func-src/handler_custom_email_sender.ts b/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/func-src/handler_custom_email_sender.ts new file mode 100644 index 0000000000..e52be2ea0a --- /dev/null +++ b/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/func-src/handler_custom_email_sender.ts @@ -0,0 +1,28 @@ +import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs'; + +/** + * This function asserts that custom email sender function is working properly + */ +export const handler = async () => { + const sqsClient = new SQSClient({ region: process.env.region }); + + const queueUrl = process.env.CUSTOM_EMAIL_SENDER_SQS_QUEUE_URL; + + if (!queueUrl) { + throw new Error('SQS_QUEUE_URL is not set in environment variables'); + } + + const messageBody = JSON.stringify({ + message: 'Custom Email Sender is working', + timeStamp: new Date().toISOString(), + }); + + await sqsClient.send( + new SendMessageCommand({ + QueueUrl: queueUrl, + MessageBody: messageBody, + }) + ); + + return 'It is working'; +}; diff --git a/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/func-src/handler_no_minify.ts b/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/func-src/handler_no_minify.ts new file mode 100644 index 0000000000..f5a3fff455 --- /dev/null +++ b/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/func-src/handler_no_minify.ts @@ -0,0 +1,6 @@ +/** + * This function asserts that the code is not minified. + */ +export const handler = async () => { + return 'No minify'; +}; diff --git a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/func-src/handler_with_aws_sdk.ts b/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/func-src/handler_with_aws_sdk.ts similarity index 100% rename from packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/func-src/handler_with_aws_sdk.ts rename to packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/func-src/handler_with_aws_sdk.ts diff --git a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/func-src/handler_with_aws_sqs.ts b/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/func-src/handler_with_aws_sqs.ts similarity index 100% rename from packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/func-src/handler_with_aws_sqs.ts rename to packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/func-src/handler_with_aws_sqs.ts diff --git a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/func-src/handler_with_ssm.ts b/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/func-src/handler_with_ssm.ts similarity index 100% rename from packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/func-src/handler_with_ssm.ts rename to packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/func-src/handler_with_ssm.ts diff --git a/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/function.ts b/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/function.ts new file mode 100644 index 0000000000..a4f30ef66f --- /dev/null +++ b/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/function.ts @@ -0,0 +1,30 @@ +import { defineFunction } from '@aws-amplify/backend'; + +export const funcWithSsm = defineFunction({ + name: 'funcWithSsm', + entry: './func-src/handler_with_ssm.ts', +}); + +export const funcWithAwsSdk = defineFunction({ + name: 'funcWithAwsSdk', + entry: './func-src/handler_with_aws_sdk.ts', +}); + +export const funcWithSchedule = defineFunction({ + name: 'funcWithSchedule', + entry: './func-src/handler_with_aws_sqs.ts', + schedule: '* * * * ?', +}); + +export const funcNoMinify = defineFunction({ + name: 'funcNoMinify', + entry: './func-src/handler_no_minify.ts', + bundling: { + minify: false, + }, +}); + +export const funcCustomEmailSender = defineFunction({ + name: 'funcCustomEmailSender', + entry: './func-src/handler_custom_email_sender.ts', +}); diff --git a/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/test_factories.ts b/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/test_factories.ts new file mode 100644 index 0000000000..4e72f34424 --- /dev/null +++ b/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/test_factories.ts @@ -0,0 +1,17 @@ +import { + funcCustomEmailSender, + funcNoMinify, + funcWithAwsSdk, + funcWithSchedule, + funcWithSsm, +} from './function.js'; +import { auth } from './auth/resource.js'; + +export const authAndFunctions = { + auth, + funcWithSsm, + funcWithAwsSdk, + funcWithSchedule, + funcNoMinify, + funcCustomEmailSender, +}; diff --git a/packages/integration-tests/src/test-projects/circular-dep-auth-data-func/amplify/auth/resource.ts b/packages/integration-tests/src/test-projects/circular-dep-auth-data-func/amplify/auth/resource.ts new file mode 100644 index 0000000000..284ced6656 --- /dev/null +++ b/packages/integration-tests/src/test-projects/circular-dep-auth-data-func/amplify/auth/resource.ts @@ -0,0 +1,11 @@ +import { defineAuth } from '@aws-amplify/backend'; +import { preSignUp } from '../functions/pre-sign-up/resource.js'; + +export const auth = defineAuth({ + loginWith: { + email: true, + }, + triggers: { + preSignUp, + }, +}); diff --git a/packages/integration-tests/src/test-projects/circular-dep-auth-data-func/amplify/backend.ts b/packages/integration-tests/src/test-projects/circular-dep-auth-data-func/amplify/backend.ts new file mode 100644 index 0000000000..57a0b4da49 --- /dev/null +++ b/packages/integration-tests/src/test-projects/circular-dep-auth-data-func/amplify/backend.ts @@ -0,0 +1,23 @@ +import { defineBackend } from '@aws-amplify/backend'; +import { auth } from './auth/resource.js'; +import { data } from './data/resource.js'; +import { apiFunction } from './functions/api-function/resource.js'; +import { preSignUp } from './functions/pre-sign-up/resource.js'; +import { DynamoEventSource } from 'aws-cdk-lib/aws-lambda-event-sources'; +import { StartingPosition } from 'aws-cdk-lib/aws-lambda'; + +const backend = defineBackend({ + auth, + data, + apiFunction, + preSignUp, +}); + +const eventSource = new DynamoEventSource( + backend.data.resources.tables['Todo'], + { + startingPosition: StartingPosition.LATEST, + } +); + +backend.apiFunction.resources.lambda.addEventSource(eventSource); diff --git a/packages/integration-tests/src/test-projects/circular-dep-auth-data-func/amplify/data/resource.ts b/packages/integration-tests/src/test-projects/circular-dep-auth-data-func/amplify/data/resource.ts new file mode 100644 index 0000000000..fdee70ae7d --- /dev/null +++ b/packages/integration-tests/src/test-projects/circular-dep-auth-data-func/amplify/data/resource.ts @@ -0,0 +1,25 @@ +import { type ClientSchema, a, defineData } from '@aws-amplify/backend'; + +const schema = a.schema({ + Todo: a + .model({ + content: a.string(), + }) + .authorization((allow) => [allow.publicApiKey()]), +}) as never; // Not 100% sure why TS is complaining here. The error I'm getting is "The inferred type of 'schema' references an inaccessible 'unique symbol' type. A type annotation is necessary." + +// ^ appears to be caused by these 2 rules in tsconfig.base.json: https://github.com/aws-amplify/amplify-backend/blob/8d9a7a4c3033c474b0fc78379cdd4c1854d890ce/tsconfig.base.json#L7-L8 +// Possibly something to do with all the `references` in the nested configs. Using the same tsconfig in a new amplify app doesn't cause the error. + +export type Schema = ClientSchema; + +export const data = defineData({ + schema, + authorizationModes: { + defaultAuthorizationMode: 'apiKey', + // API Key is used for a.allow.public() rules + apiKeyAuthorizationMode: { + expiresInDays: 30, + }, + }, +}); diff --git a/packages/integration-tests/src/test-projects/circular-dep-auth-data-func/amplify/functions/api-function/resource.ts b/packages/integration-tests/src/test-projects/circular-dep-auth-data-func/amplify/functions/api-function/resource.ts new file mode 100644 index 0000000000..7bf6de291a --- /dev/null +++ b/packages/integration-tests/src/test-projects/circular-dep-auth-data-func/amplify/functions/api-function/resource.ts @@ -0,0 +1,7 @@ +import { defineFunction } from '@aws-amplify/backend'; + +export const apiFunction = defineFunction({ + name: 'apiFunction', + entry: '../handler.ts', + resourceGroupName: 'data', +}); diff --git a/packages/integration-tests/src/test-projects/circular-dep-auth-data-func/amplify/functions/handler.ts b/packages/integration-tests/src/test-projects/circular-dep-auth-data-func/amplify/functions/handler.ts new file mode 100644 index 0000000000..ad5a6a9ead --- /dev/null +++ b/packages/integration-tests/src/test-projects/circular-dep-auth-data-func/amplify/functions/handler.ts @@ -0,0 +1 @@ +export const handler = () => {}; diff --git a/packages/integration-tests/src/test-projects/circular-dep-auth-data-func/amplify/functions/pre-sign-up/resource.ts b/packages/integration-tests/src/test-projects/circular-dep-auth-data-func/amplify/functions/pre-sign-up/resource.ts new file mode 100644 index 0000000000..d62f7c08be --- /dev/null +++ b/packages/integration-tests/src/test-projects/circular-dep-auth-data-func/amplify/functions/pre-sign-up/resource.ts @@ -0,0 +1,7 @@ +import { defineFunction } from '@aws-amplify/backend'; + +export const preSignUp = defineFunction({ + name: 'preSignUp', + entry: '../handler.ts', + resourceGroupName: 'auth', +}); diff --git a/packages/integration-tests/src/test-projects/circular-dep-data-func/amplify/auth/resource.ts b/packages/integration-tests/src/test-projects/circular-dep-data-func/amplify/auth/resource.ts new file mode 100644 index 0000000000..cd2d859508 --- /dev/null +++ b/packages/integration-tests/src/test-projects/circular-dep-data-func/amplify/auth/resource.ts @@ -0,0 +1,7 @@ +import { defineAuth } from '@aws-amplify/backend'; + +export const auth = defineAuth({ + loginWith: { + email: true, + }, +}); diff --git a/packages/integration-tests/src/test-projects/circular-dep-data-func/amplify/backend.ts b/packages/integration-tests/src/test-projects/circular-dep-data-func/amplify/backend.ts new file mode 100644 index 0000000000..68068eaff1 --- /dev/null +++ b/packages/integration-tests/src/test-projects/circular-dep-data-func/amplify/backend.ts @@ -0,0 +1,23 @@ +import { defineBackend } from '@aws-amplify/backend'; +import { auth } from './auth/resource.js'; +import { data } from './data/resource.js'; +import { apiFunction } from './functions/api-function/resource.js'; +import { DynamoEventSource } from 'aws-cdk-lib/aws-lambda-event-sources'; +import { StartingPosition } from 'aws-cdk-lib/aws-lambda'; +import { queryFunction } from './functions/query-function/resource.js'; + +const backend = defineBackend({ + auth, + data, + apiFunction, + queryFunction, +}); + +const eventSource = new DynamoEventSource( + backend.data.resources.tables['Todo'], + { + startingPosition: StartingPosition.LATEST, + } +); + +backend.apiFunction.resources.lambda.addEventSource(eventSource); diff --git a/packages/integration-tests/src/test-projects/circular-dep-data-func/amplify/data/resource.ts b/packages/integration-tests/src/test-projects/circular-dep-data-func/amplify/data/resource.ts new file mode 100644 index 0000000000..7d121dbf40 --- /dev/null +++ b/packages/integration-tests/src/test-projects/circular-dep-data-func/amplify/data/resource.ts @@ -0,0 +1,32 @@ +import { type ClientSchema, a, defineData } from '@aws-amplify/backend'; +import { queryFunction } from '../functions/query-function/resource.js'; + +const schema = a.schema({ + Todo: a + .model({ + content: a.string(), + }) + .authorization((allow) => [allow.publicApiKey()]), + query: a + .query() + .arguments({ content: a.string() }) + .returns(a.string()) + .authorization((allow) => [allow.publicApiKey()]) + .handler(a.handler.function(queryFunction)), +}) as never; // Not 100% sure why TS is complaining here. The error I'm getting is "The inferred type of 'schema' references an inaccessible 'unique symbol' type. A type annotation is necessary." + +// ^ appears to be caused by these 2 rules in tsconfig.base.json: https://github.com/aws-amplify/amplify-backend/blob/8d9a7a4c3033c474b0fc78379cdd4c1854d890ce/tsconfig.base.json#L7-L8 +// Possibly something to do with all the `references` in the nested configs. Using the same tsconfig in a new amplify app doesn't cause the error. + +export type Schema = ClientSchema; + +export const data = defineData({ + schema, + authorizationModes: { + defaultAuthorizationMode: 'apiKey', + // API Key is used for a.allow.public() rules + apiKeyAuthorizationMode: { + expiresInDays: 30, + }, + }, +}); diff --git a/packages/integration-tests/src/test-projects/circular-dep-data-func/amplify/functions/api-function/resource.ts b/packages/integration-tests/src/test-projects/circular-dep-data-func/amplify/functions/api-function/resource.ts new file mode 100644 index 0000000000..7bf6de291a --- /dev/null +++ b/packages/integration-tests/src/test-projects/circular-dep-data-func/amplify/functions/api-function/resource.ts @@ -0,0 +1,7 @@ +import { defineFunction } from '@aws-amplify/backend'; + +export const apiFunction = defineFunction({ + name: 'apiFunction', + entry: '../handler.ts', + resourceGroupName: 'data', +}); diff --git a/packages/integration-tests/src/test-projects/circular-dep-data-func/amplify/functions/handler.ts b/packages/integration-tests/src/test-projects/circular-dep-data-func/amplify/functions/handler.ts new file mode 100644 index 0000000000..ad5a6a9ead --- /dev/null +++ b/packages/integration-tests/src/test-projects/circular-dep-data-func/amplify/functions/handler.ts @@ -0,0 +1 @@ +export const handler = () => {}; diff --git a/packages/integration-tests/src/test-projects/circular-dep-data-func/amplify/functions/query-function/resource.ts b/packages/integration-tests/src/test-projects/circular-dep-data-func/amplify/functions/query-function/resource.ts new file mode 100644 index 0000000000..dba0d24f03 --- /dev/null +++ b/packages/integration-tests/src/test-projects/circular-dep-data-func/amplify/functions/query-function/resource.ts @@ -0,0 +1,7 @@ +import { defineFunction } from '@aws-amplify/backend'; + +export const queryFunction = defineFunction({ + name: 'queryFunction', + entry: '../handler.ts', + resourceGroupName: 'data', +}); diff --git a/packages/integration-tests/src/test-projects/conversation-handler/amplify/backend.ts b/packages/integration-tests/src/test-projects/conversation-handler/amplify/backend.ts index 3c6fa81c2e..f6914ace4e 100644 --- a/packages/integration-tests/src/test-projects/conversation-handler/amplify/backend.ts +++ b/packages/integration-tests/src/test-projects/conversation-handler/amplify/backend.ts @@ -9,11 +9,24 @@ const backend = defineBackend({ auth, data, customConversationHandler }); const stack = backend.createStack('conversationHandlerStack'); -new ConversationHandlerFunction(stack, 'defaultConversationHandlerFunction', { - models: [ - { - modelId: bedrockModelId, - region: stack.region, - }, - ], -}); +const defaultConversationHandler = new ConversationHandlerFunction( + stack, + 'defaultConversationHandlerFunction', + { + models: [ + { + modelId: bedrockModelId, + region: stack.region, + }, + ], + } +); + +defaultConversationHandler.resources.cfnResources.cfnFunction.addPropertyOverride( + 'LoggingConfig.ApplicationLogLevel', + 'DEBUG' +); +backend.customConversationHandler.resources.cfnResources.cfnFunction.addPropertyOverride( + 'LoggingConfig.ApplicationLogLevel', + 'DEBUG' +); diff --git a/packages/integration-tests/src/test-projects/conversation-handler/amplify/constants.ts b/packages/integration-tests/src/test-projects/conversation-handler/amplify/constants.ts index af652ce890..8a2256051e 100644 --- a/packages/integration-tests/src/test-projects/conversation-handler/amplify/constants.ts +++ b/packages/integration-tests/src/test-projects/conversation-handler/amplify/constants.ts @@ -8,6 +8,11 @@ */ export const bedrockModelId = 'anthropic.claude-3-haiku-20240307-v1:0'; -export const expectedTemperatureInProgrammaticToolScenario = 75; +export const expectedTemperaturesInProgrammaticToolScenario = { + Seattle: 75, + Boston: 58, +}; export const expectedTemperatureInDataToolScenario = 85; + +export const expectedRandomNumber = 42; diff --git a/packages/integration-tests/src/test-projects/conversation-handler/amplify/custom-conversation-handler/custom_handler.ts b/packages/integration-tests/src/test-projects/conversation-handler/amplify/custom-conversation-handler/custom_handler.ts index 53d2944f5b..2382dc82d9 100644 --- a/packages/integration-tests/src/test-projects/conversation-handler/amplify/custom-conversation-handler/custom_handler.ts +++ b/packages/integration-tests/src/test-projects/conversation-handler/amplify/custom-conversation-handler/custom_handler.ts @@ -1,30 +1,67 @@ import { ConversationTurnEvent, - ExecutableTool, - ToolResultContentBlock, + createExecutableTool, handleConversationTurnEvent, } from '@aws-amplify/backend-ai/conversation/runtime'; -import { expectedTemperatureInProgrammaticToolScenario } from '../constants.js'; +import { + expectedRandomNumber, + expectedTemperaturesInProgrammaticToolScenario, +} from '../constants.js'; + +const thermometerInputSchema = { + type: 'object', + properties: { + city: { type: 'string' }, + }, + required: ['city'], +} as const; + +const thermometer = createExecutableTool( + 'thermometer', + 'Returns current temperature in cities', + { + json: thermometerInputSchema, + }, + (input) => { + const city = input.city; + if (city === 'Seattle' || city === 'Boston') { + return Promise.resolve({ + // We use this value in test assertion. + // LLM uses tool to get temperature and serves this value in final response. + // We're matching number only as LLM may translate unit to something more descriptive. + text: `${expectedTemperaturesInProgrammaticToolScenario[city]}F`, + }); + } + throw new Error(`Unknown city ${input.city}`); + } +); -const thermometer: ExecutableTool = { - name: 'thermometer', - description: 'Returns current temperature in Seattle', - execute: (): Promise => { +// Parameter-less tool. +const randomNumberGeneratorInputSchema = { + type: 'object', + properties: {}, + required: [], +} as const; + +const randomNumberGenerator = createExecutableTool( + 'randomNumberGenerator', + 'Returns a random number', + { + json: randomNumberGeneratorInputSchema, + }, + () => { return Promise.resolve({ // We use this value in test assertion. - // LLM uses tool to get temperature and serves this value in final response. - // We're matching number only as LLM may translate unit to something more descriptive. - text: `${expectedTemperatureInProgrammaticToolScenario}F`, + text: `${expectedRandomNumber}`, }); - }, - inputSchema: { json: { type: 'object' } }, -}; + } +); /** * Handler with simple tool. */ export const handler = async (event: ConversationTurnEvent) => { await handleConversationTurnEvent(event, { - tools: [thermometer], + tools: [randomNumberGenerator, thermometer], }); }; diff --git a/packages/integration-tests/src/test-projects/conversation-handler/amplify/data/resource.ts b/packages/integration-tests/src/test-projects/conversation-handler/amplify/data/resource.ts index 2ef65c955b..07c19400c2 100644 --- a/packages/integration-tests/src/test-projects/conversation-handler/amplify/data/resource.ts +++ b/packages/integration-tests/src/test-projects/conversation-handler/amplify/data/resource.ts @@ -19,14 +19,124 @@ const schema = a.schema({ ) ), - // This schema mocks expected model where conversation responses are supposed to be recorded. + // These schemas below mock models normally generated by conversational routes. + MockConversationParticipantRole: a.enum(['user', 'assistant']), + + MockDocumentBlockSource: a.customType({ + bytes: a.string(), + }), + + MockDocumentBlock: a.customType({ + format: a.string().required(), + name: a.string().required(), + source: a.ref('MockDocumentBlockSource').required(), + }), + + MockImageBlockSource: a.customType({ + bytes: a.string(), + }), + + MockImageBlock: a.customType({ + format: a.string().required(), + source: a.ref('MockImageBlockSource').required(), + }), + + MockToolResultContentBlock: a.customType({ + document: a.ref('MockDocumentBlock'), + image: a.ref('MockImageBlock'), + json: a.json(), + text: a.string(), + }), + + MockToolResultBlock: a.customType({ + toolUseId: a.string().required(), + status: a.string(), + content: a.ref('MockToolResultContentBlock').array().required(), + }), + + MockToolUseBlock: a.customType({ + toolUseId: a.string().required(), + name: a.string().required(), + input: a.json().required(), + }), + + MockContentBlock: a.customType({ + text: a.string(), + document: a.ref('MockDocumentBlock'), + image: a.ref('MockImageBlock'), + toolResult: a.ref('MockToolResultBlock'), + toolUse: a.ref('MockToolUseBlock'), + }), + + MockToolInputSchema: a.customType({ + json: a.json(), + }), + + MockToolSpecification: a.customType({ + name: a.string().required(), + description: a.string(), + inputSchema: a.ref('MockToolInputSchema').required(), + }), + + MockTool: a.customType({ + toolSpec: a.ref('MockToolSpecification'), + }), + + MockToolConfiguration: a.customType({ + tools: a.ref('MockTool').array(), + }), + + MockConversationTurnError: a.customType({ + errorType: a.string(), + message: a.string(), + }), + ConversationMessageAssistantResponse: a .model({ conversationId: a.id(), associatedUserMessageId: a.id(), content: a.string(), - sender: a.enum(['user', 'assistant']), + errors: a.ref('MockConversationTurnError').array(), + }) + .authorization((allow) => [allow.authenticated(), allow.owner()]), + + ConversationMessageAssistantStreamingResponse: a + .model({ + // always + conversationId: a.id().required(), + associatedUserMessageId: a.id().required(), + contentBlockIndex: a.integer(), + accumulatedTurnContent: a.ref('MockContentBlock').array(), + + // these describe chunks or end of block + contentBlockText: a.string(), + contentBlockToolUse: a.string(), + contentBlockDeltaIndex: a.integer(), + contentBlockDoneAtIndex: a.integer(), + + // when message is complete + stopReason: a.string(), + + // error + errors: a.ref('MockConversationTurnError').array(), + }) + .secondaryIndexes((index) => [ + index('conversationId').sortKeys(['associatedUserMessageId']), + ]) + .authorization((allow) => [allow.authenticated(), allow.owner()]), + + ConversationMessageChat: a + .model({ + conversationId: a.id(), + associatedUserMessageId: a.id(), + role: a.ref('MockConversationParticipantRole'), + content: a.ref('MockContentBlock').array(), + aiContext: a.json(), + toolConfiguration: a.ref('MockToolConfiguration'), }) + .secondaryIndexes((index) => [ + index('conversationId').sortKeys(['associatedUserMessageId']), + ]) .authorization((allow) => [allow.authenticated(), allow.owner()]), }); diff --git a/packages/integration-tests/src/test-projects/custom-outputs/amplify/backend.ts b/packages/integration-tests/src/test-projects/custom-outputs/amplify/backend.ts index 2ae7efb16f..ceed80b636 100644 --- a/packages/integration-tests/src/test-projects/custom-outputs/amplify/backend.ts +++ b/packages/integration-tests/src/test-projects/custom-outputs/amplify/backend.ts @@ -16,7 +16,7 @@ const sampleIdentityPoolId = 'test_identity_pool_id'; const sampleUserPoolClientId = 'test_user_pool_client_id'; backend.addOutput({ - version: '1.1', + version: '1.3', custom: { // test deploy time values restApiUrl: restApi.url, @@ -26,7 +26,7 @@ backend.addOutput({ }); backend.addOutput({ - version: '1.1', + version: '1.3', custom: { // test synth time values // and composition of config @@ -36,7 +36,7 @@ backend.addOutput({ const fakeCognitoUserPoolId = 'fakeCognitoUserPoolId'; backend.addOutput({ - version: '1.1', + version: '1.3', // test reserved key auth: { aws_region: sampleRegion, diff --git a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/auth/resource.ts b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/auth/resource.ts index e5ff3baa41..92733f12ec 100644 --- a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/auth/resource.ts +++ b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/auth/resource.ts @@ -24,4 +24,5 @@ export const auth = defineAuth({ triggers: { postConfirmation: defaultNodeFunc, }, + groups: ['Editors', 'Admins'], }); diff --git a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/backend.ts b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/backend.ts index 4cd85ed1e3..8fdf38f0d2 100644 --- a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/backend.ts +++ b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/backend.ts @@ -1,25 +1,8 @@ import { defineBackend } from '@aws-amplify/backend'; import { dataStorageAuthWithTriggers } from './test_factories.js'; -import { Queue } from 'aws-cdk-lib/aws-sqs'; -import { Role } from 'aws-cdk-lib/aws-iam'; -import { Stack } from 'aws-cdk-lib'; const backend = defineBackend(dataStorageAuthWithTriggers); backend.defaultNodeFunc.addEnvironment('newKey', 'newValue'); -const scheduleFunctionLambda = backend.funcWithSchedule.resources.lambda; -const scheduleFunctionLambdaRole = scheduleFunctionLambda.role; -const queueStack = Stack.of(scheduleFunctionLambda); - -const queue = new Queue(queueStack, 'amplify-testFuncQueue'); - -if (scheduleFunctionLambdaRole) { - queue.grantSendMessages( - Role.fromRoleArn( - queueStack, - 'LambdaExecutionRole', - scheduleFunctionLambdaRole.roleArn - ) - ); -} -backend.funcWithSchedule.addEnvironment('SQS_QUEUE_URL', queue.queueUrl); +// Change precedence of Editors group so Admins group has the lowest precedence +backend.auth.resources.groups['Editors'].cfnUserGroup.precedence = 2; diff --git a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/function.ts b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/function.ts index 2405878c75..cfaf6da0a4 100644 --- a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/function.ts +++ b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/function.ts @@ -34,19 +34,3 @@ export const onUpload = defineFunction({ name: 'onUpload', entry: './func-src/handler.ts', }); - -export const funcWithSsm = defineFunction({ - name: 'funcWithSsm', - entry: './func-src/handler_with_ssm.ts', -}); - -export const funcWithAwsSdk = defineFunction({ - name: 'funcWithAwsSdk', - entry: './func-src/handler_with_aws_sdk.ts', -}); - -export const funcWithSchedule = defineFunction({ - name: 'funcWithSchedule', - entry: './func-src/handler_with_aws_sqs.ts', - schedule: '* * * * ?', -}); diff --git a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/storage/resource.ts b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/storage/resource.ts index cfd30953e2..3af6c5fecf 100644 --- a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/storage/resource.ts +++ b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/storage/resource.ts @@ -16,6 +16,14 @@ export const storage = defineStorage({ 'public/*': [ allow.resource(defaultNodeFunc).to(['read', 'write']), allow.resource(node16Func).to(['read', 'write']), + allow.guest.to(['read']), + allow.authenticated.to(['read', 'write']), + allow.groups(['Admins']).to(['read', 'write', 'delete']), + ], + 'protected/{entity_id}/*': [ + allow.authenticated.to(['read']), + allow.entity('identity').to(['read', 'write', 'delete']), + allow.groups(['Admins']).to(['read', 'write', 'delete']), ], }), }); diff --git a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/test_factories.ts b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/test_factories.ts index 49227af615..fa024e13fc 100644 --- a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/test_factories.ts +++ b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/amplify/test_factories.ts @@ -1,11 +1,5 @@ import { data } from './data/resource.js'; -import { - defaultNodeFunc, - funcWithSsm, - funcWithAwsSdk, - node16Func, - funcWithSchedule, -} from './function.js'; +import { defaultNodeFunc, node16Func } from './function.js'; import { storage } from './storage/resource.js'; import { auth } from './auth/resource.js'; @@ -15,7 +9,4 @@ export const dataStorageAuthWithTriggers = { defaultNodeFunc, data, node16Func, - funcWithSsm, - funcWithAwsSdk, - funcWithSchedule, }; diff --git a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/function.ts b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/function.ts index 61a9171a16..6032d8c60f 100644 --- a/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/function.ts +++ b/packages/integration-tests/src/test-projects/data-storage-auth-with-triggers-ts/hotswap-update-files/function.ts @@ -36,19 +36,3 @@ export const onUpload = defineFunction({ name: 'onUpload', entry: './func-src/handler.ts', }); - -export const funcWithSsm = defineFunction({ - name: 'funcWithSsm', - entry: './func-src/handler_with_ssm.ts', -}); - -export const funcWithAwsSdk = defineFunction({ - name: 'funcWithAwsSdk', - entry: './func-src/handler_with_aws_sdk.ts', -}); - -export const funcWithSchedule = defineFunction({ - name: 'funcWithSchedule', - entry: './func-src/handler_with_aws_sqs.ts', - schedule: '* * * * ?', -}); diff --git a/packages/integration-tests/src/test-projects/reference-auth/amplify/auth/resource.ts b/packages/integration-tests/src/test-projects/reference-auth/amplify/auth/resource.ts new file mode 100644 index 0000000000..bb04424328 --- /dev/null +++ b/packages/integration-tests/src/test-projects/reference-auth/amplify/auth/resource.ts @@ -0,0 +1,14 @@ +import { referenceAuth } from '@aws-amplify/backend'; +import { addUserToGroup } from '../data/add-user-to-group/resource.js'; + +export const auth = referenceAuth({ + identityPoolId: '', + authRoleArn: '', + unauthRoleArn: '', + userPoolId: '', + userPoolClientId: '', + groups: { + ADMINS: '', + }, + access: (allow) => [allow.resource(addUserToGroup).to(['addUserToGroup'])], +}); diff --git a/packages/integration-tests/src/test-projects/reference-auth/amplify/backend.ts b/packages/integration-tests/src/test-projects/reference-auth/amplify/backend.ts new file mode 100644 index 0000000000..8aac23b543 --- /dev/null +++ b/packages/integration-tests/src/test-projects/reference-auth/amplify/backend.ts @@ -0,0 +1,10 @@ +import { defineBackend } from '@aws-amplify/backend'; +import { auth } from './auth/resource.js'; +import { data } from './data/resource.js'; +import { storage } from './storage/resource.js'; + +defineBackend({ + auth, + data, + storage, +}); diff --git a/packages/integration-tests/src/test-projects/reference-auth/amplify/data/add-user-to-group/handler.ts b/packages/integration-tests/src/test-projects/reference-auth/amplify/data/add-user-to-group/handler.ts new file mode 100644 index 0000000000..48a0db7cb6 --- /dev/null +++ b/packages/integration-tests/src/test-projects/reference-auth/amplify/data/add-user-to-group/handler.ts @@ -0,0 +1,3 @@ +export const handler = async (event: any) => { + return 'Hello world!'; +}; diff --git a/packages/integration-tests/src/test-projects/reference-auth/amplify/data/add-user-to-group/resource.ts b/packages/integration-tests/src/test-projects/reference-auth/amplify/data/add-user-to-group/resource.ts new file mode 100644 index 0000000000..dd1d930b07 --- /dev/null +++ b/packages/integration-tests/src/test-projects/reference-auth/amplify/data/add-user-to-group/resource.ts @@ -0,0 +1,5 @@ +import { defineFunction } from '@aws-amplify/backend'; + +export const addUserToGroup = defineFunction({ + name: 'add-user-to-group', +}); diff --git a/packages/integration-tests/src/test-projects/reference-auth/amplify/data/resource.ts b/packages/integration-tests/src/test-projects/reference-auth/amplify/data/resource.ts new file mode 100644 index 0000000000..d6842ab5d0 --- /dev/null +++ b/packages/integration-tests/src/test-projects/reference-auth/amplify/data/resource.ts @@ -0,0 +1,24 @@ +import { type ClientSchema, a, defineData } from '@aws-amplify/backend'; +import { addUserToGroup } from './add-user-to-group/resource.js'; + +const schema = a.schema({ + Todo: a + .model({ + name: a.string(), + description: a.string(), + }) + .authorization((allow) => allow.group('ADMINS')), + addUserToGroup: a + .mutation() + .arguments({ + userId: a.string().required(), + groupName: a.string().required(), + }) + .authorization((allow) => [allow.group('ADMINS')]) + .handler(a.handler.function(addUserToGroup)) + .returns(a.json()), +}) as never; + +export type Schema = ClientSchema; + +export const data = defineData({ schema }); diff --git a/packages/integration-tests/src/test-projects/reference-auth/amplify/storage/resource.ts b/packages/integration-tests/src/test-projects/reference-auth/amplify/storage/resource.ts new file mode 100644 index 0000000000..9404344b2b --- /dev/null +++ b/packages/integration-tests/src/test-projects/reference-auth/amplify/storage/resource.ts @@ -0,0 +1,15 @@ +import { defineStorage } from '@aws-amplify/backend'; +export const storage = defineStorage({ + name: 'amplifyTeamDrive', + access: (allow) => ({ + 'profile-pictures/{entity_id}/*': [ + allow.guest.to(['read']), + allow.groups(['ADMINS']).to(['read']), + allow.entity('identity').to(['read', 'write', 'delete']), + ], + 'picture-submissions/*': [ + allow.authenticated.to(['read', 'write']), + allow.guest.to(['read', 'write']), + ], + }), +}); diff --git a/packages/integration-tests/src/test-projects/reference-auth/test-types/env/add-user-to-group.ts b/packages/integration-tests/src/test-projects/reference-auth/test-types/env/add-user-to-group.ts new file mode 100644 index 0000000000..f58ac10994 --- /dev/null +++ b/packages/integration-tests/src/test-projects/reference-auth/test-types/env/add-user-to-group.ts @@ -0,0 +1,10 @@ +export const env = process.env as { + TEST_NAME_BUCKET_NAME: string; + AWS_REGION: string; + AWS_ACCESS_KEY_ID: string; + AWS_SECRET_ACCESS_KEY: string; + AWS_SESSION_TOKEN: string; + TEST_SECRET: string; + TEST_SHARED_SECRET: string; + AMPLIFY_AUTH_USERPOOL_ID: string; +}; diff --git a/packages/model-generator/CHANGELOG.md b/packages/model-generator/CHANGELOG.md index 56dd0a8f93..01d352bb5f 100644 --- a/packages/model-generator/CHANGELOG.md +++ b/packages/model-generator/CHANGELOG.md @@ -1,5 +1,34 @@ # @aws-amplify/model-generator +## 1.0.9 + +### Patch Changes + +- 443e2ff: bump graphql-generator dependency version to 0.5.1 +- Updated dependencies [90a7c49] + - @aws-amplify/plugin-types@1.4.0 + +## 1.0.8 + +### Patch Changes + +- e325044: Prefer amplify errors in generators +- Updated dependencies [87dbf41] + - @aws-amplify/plugin-types@1.3.0 + +## 1.0.7 + +### Patch Changes + +- e648e8e: added main field to package.json so these packages are resolvable +- 8dd7286: fixed errors in plugin-types and cli-core along with any extraneous dependencies in other packages +- e648e8e: added main field to packages known to lack one +- Updated dependencies [e648e8e] +- Updated dependencies [8dd7286] +- Updated dependencies [e648e8e] + - @aws-amplify/deployed-backend-client@1.4.1 + - @aws-amplify/plugin-types@1.2.2 + ## 1.0.6 ### Patch Changes diff --git a/packages/model-generator/package.json b/packages/model-generator/package.json index 2f6efcbd50..e8633f937a 100644 --- a/packages/model-generator/package.json +++ b/packages/model-generator/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/model-generator", - "version": "1.0.6", + "version": "1.0.9", "type": "module", "publishConfig": { "access": "public" @@ -20,11 +20,11 @@ "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^1.1.0", - "@aws-amplify/deployed-backend-client": "^1.3.0", - "@aws-amplify/graphql-generator": "^0.4.0", + "@aws-amplify/deployed-backend-client": "^1.4.1", + "@aws-amplify/graphql-generator": "^0.5.1", "@aws-amplify/graphql-types-generator": "^3.6.0", "@aws-amplify/platform-core": "^1.0.5", - "@aws-amplify/plugin-types": "^1.2.1", + "@aws-amplify/plugin-types": "^1.4.0", "@aws-sdk/client-appsync": "^3.624.0", "@aws-sdk/client-s3": "^3.624.0", "@aws-sdk/credential-providers": "^3.624.0", diff --git a/packages/model-generator/src/create_graphql_document_generator.ts b/packages/model-generator/src/create_graphql_document_generator.ts index 2de7652a8f..b2a6b3aef4 100644 --- a/packages/model-generator/src/create_graphql_document_generator.ts +++ b/packages/model-generator/src/create_graphql_document_generator.ts @@ -29,9 +29,11 @@ export const createGraphqlDocumentGenerator = ({ awsClientProvider, }: GraphqlDocumentGeneratorFactoryParams): GraphqlDocumentGenerator => { if (!backendIdentifier) { + // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors throw new Error('`backendIdentifier` must be defined'); } if (!awsClientProvider) { + // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors throw new Error('`awsClientProvider` must be defined'); } @@ -44,6 +46,7 @@ export const createGraphqlDocumentGenerator = ({ ); const apiId = output[graphqlOutputKey]?.payload.awsAppsyncApiId; if (!apiId) { + // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors throw new Error(`Unable to determine AppSync API ID.`); } diff --git a/packages/model-generator/src/create_graphql_models_generator.ts b/packages/model-generator/src/create_graphql_models_generator.ts index b0f8d0cd08..c1a2920242 100644 --- a/packages/model-generator/src/create_graphql_models_generator.ts +++ b/packages/model-generator/src/create_graphql_models_generator.ts @@ -61,9 +61,11 @@ const createGraphqlModelsGeneratorFromBackendIdentifier = ({ awsClientProvider, }: GraphqlModelsFromBackendIdentifierParams): GraphqlModelsGenerator => { if (!backendIdentifier) { + // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors throw new Error('`backendIdentifier` must be defined'); } if (!awsClientProvider) { + // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors throw new Error('`awsClientProvider` must be defined'); } @@ -90,9 +92,11 @@ export const createGraphqlModelsFromS3UriGenerator = ({ awsClientProvider, }: GraphqlModelsFromS3UriGeneratorFactoryParams): GraphqlModelsGenerator => { if (!modelSchemaS3Uri) { + // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors throw new Error('`modelSchemaS3Uri` must be defined'); } if (!awsClientProvider) { + // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors throw new Error('`awsClientProvider` must be defined'); } return new StackMetadataGraphqlModelsGenerator( @@ -118,6 +122,7 @@ const getModelSchema = async ( const modelSchemaS3Uri = output[graphqlOutputKey]?.payload.amplifyApiModelSchemaS3Uri; if (!modelSchemaS3Uri) { + // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors throw new Error(`Cannot find model schema at amplifyApiModelSchemaS3Uri`); } diff --git a/packages/model-generator/src/create_graphql_types_generator.ts b/packages/model-generator/src/create_graphql_types_generator.ts index a6c7247970..17f0e799e2 100644 --- a/packages/model-generator/src/create_graphql_types_generator.ts +++ b/packages/model-generator/src/create_graphql_types_generator.ts @@ -29,9 +29,11 @@ export const createGraphqlTypesGenerator = ({ awsClientProvider, }: GraphqlTypesGeneratorFactoryParams): GraphqlTypesGenerator => { if (!backendIdentifier) { + // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors throw new Error('`backendIdentifier` must be defined'); } if (!awsClientProvider) { + // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors throw new Error('`awsClientProvider` must be defined'); } @@ -44,6 +46,7 @@ export const createGraphqlTypesGenerator = ({ ); const apiId = output[graphqlOutputKey]?.payload.awsAppsyncApiId; if (!apiId) { + // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors throw new Error(`Unable to determine AppSync API ID.`); } diff --git a/packages/model-generator/src/generate_api_code.ts b/packages/model-generator/src/generate_api_code.ts index 29ad2fa766..43ca386433 100644 --- a/packages/model-generator/src/generate_api_code.ts +++ b/packages/model-generator/src/generate_api_code.ts @@ -145,6 +145,7 @@ export class ApiCodeGenerator { return this.generateIntrospectionApiCode(); } default: + // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors throw new Error( `${ (props as GenerateApiCodeProps).format as string diff --git a/packages/model-generator/src/get_backend_output_with_error_handling.ts b/packages/model-generator/src/get_backend_output_with_error_handling.ts index 1c5e9feb84..98fb6ffc70 100644 --- a/packages/model-generator/src/get_backend_output_with_error_handling.ts +++ b/packages/model-generator/src/get_backend_output_with_error_handling.ts @@ -20,7 +20,6 @@ export const getBackendOutputWithErrorHandling = async ( error instanceof BackendOutputClientError && error.code === BackendOutputClientErrorType.DEPLOYMENT_IN_PROGRESS ) { - // eslint-disable-next-line amplify-backend-rules/no-amplify-errors throw new AmplifyUserError( 'DeploymentInProgressError', { @@ -34,7 +33,6 @@ export const getBackendOutputWithErrorHandling = async ( error instanceof BackendOutputClientError && error.code === BackendOutputClientErrorType.NO_STACK_FOUND ) { - // eslint-disable-next-line amplify-backend-rules/no-amplify-errors throw new AmplifyUserError( 'StackDoesNotExistError', { @@ -49,7 +47,6 @@ export const getBackendOutputWithErrorHandling = async ( error instanceof BackendOutputClientError && error.code === BackendOutputClientErrorType.CREDENTIALS_ERROR ) { - // eslint-disable-next-line amplify-backend-rules/no-amplify-errors throw new AmplifyUserError( 'CredentialsError', { @@ -64,7 +61,6 @@ export const getBackendOutputWithErrorHandling = async ( error instanceof BackendOutputClientError && error.code === BackendOutputClientErrorType.ACCESS_DENIED ) { - // eslint-disable-next-line amplify-backend-rules/no-amplify-errors throw new AmplifyUserError( 'AccessDeniedError', { diff --git a/packages/model-generator/src/graphql_document_generator.ts b/packages/model-generator/src/graphql_document_generator.ts index 82ee5c33c0..8ac6dbff5c 100644 --- a/packages/model-generator/src/graphql_document_generator.ts +++ b/packages/model-generator/src/graphql_document_generator.ts @@ -27,6 +27,7 @@ export class AppSyncGraphqlDocumentGenerator const schema = await this.fetchSchema(); if (!schema) { + // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors throw new Error('Invalid schema'); } diff --git a/packages/model-generator/src/graphql_models_generator.ts b/packages/model-generator/src/graphql_models_generator.ts index 534c2af8c0..d116e2b33f 100644 --- a/packages/model-generator/src/graphql_models_generator.ts +++ b/packages/model-generator/src/graphql_models_generator.ts @@ -33,6 +33,7 @@ export class StackMetadataGraphqlModelsGenerator const schema = await this.fetchSchema(); if (!schema) { + // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors throw new Error('Invalid schema'); } diff --git a/packages/model-generator/src/graphql_types_generator.ts b/packages/model-generator/src/graphql_types_generator.ts index 33c9285b5d..38a9c7e671 100644 --- a/packages/model-generator/src/graphql_types_generator.ts +++ b/packages/model-generator/src/graphql_types_generator.ts @@ -30,6 +30,7 @@ export class AppSyncGraphqlTypesGenerator implements GraphqlTypesGenerator { const schema = await this.fetchSchema(); if (!schema) { + // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors throw new Error('Invalid schema'); } diff --git a/packages/model-generator/src/s3_string_object_fetcher.ts b/packages/model-generator/src/s3_string_object_fetcher.ts index 0b29d0e6c2..3ae3759592 100644 --- a/packages/model-generator/src/s3_string_object_fetcher.ts +++ b/packages/model-generator/src/s3_string_object_fetcher.ts @@ -19,6 +19,7 @@ export class S3StringObjectFetcher { ); const schema = await getSchemaCommandResult.Body?.transformToString(); if (!schema) { + // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors throw new Error('Error on parsing output schema'); } return schema; diff --git a/packages/platform-core/API.md b/packages/platform-core/API.md index 928d4ada67..286ae41620 100644 --- a/packages/platform-core/API.md +++ b/packages/platform-core/API.md @@ -21,9 +21,10 @@ export abstract class AmplifyError extends Error { // (undocumented) readonly details?: string; // (undocumented) - static fromError: (error: unknown) => AmplifyError<'UnknownFault' | 'CredentialsError' | 'InvalidCommandInputError' | 'DomainNotFoundError' | 'SyntaxError'>; + static fromError: (error: unknown) => AmplifyError<'UnknownFault' | 'CredentialsError' | 'InsufficientDiskSpaceError' | 'InvalidCommandInputError' | 'DomainNotFoundError' | 'SyntaxError'>; // (undocumented) static fromStderr: (_stderr: string) => AmplifyError | undefined; + static isAmplifyError: (error: unknown) => error is AmplifyError; // (undocumented) readonly link?: string; // (undocumented) diff --git a/packages/platform-core/CHANGELOG.md b/packages/platform-core/CHANGELOG.md index 51c7f3c8f4..2f8cc589ed 100644 --- a/packages/platform-core/CHANGELOG.md +++ b/packages/platform-core/CHANGELOG.md @@ -1,5 +1,19 @@ # @aws-amplify/platform-core +## 1.2.1 + +### Patch Changes + +- 71ef398: Report npm user agent +- Updated dependencies [f1db886] + - @aws-amplify/plugin-types@1.5.0 + +## 1.2.0 + +### Minor Changes + +- 583a3f2: Fix detection of AmplifyErrors + ## 1.1.0 ### Minor Changes diff --git a/packages/platform-core/package.json b/packages/platform-core/package.json index d62d258aa9..ae7d72a0e6 100644 --- a/packages/platform-core/package.json +++ b/packages/platform-core/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/platform-core", - "version": "1.1.0", + "version": "1.2.1", "type": "commonjs", "publishConfig": { "access": "public" @@ -24,7 +24,7 @@ "@types/uuid": "9.0.7" }, "dependencies": { - "@aws-amplify/plugin-types": "^1.2.1", + "@aws-amplify/plugin-types": "^1.5.0", "@aws-sdk/client-sts": "^3.624.0", "is-ci": "^3.0.1", "lodash.mergewith": "^4.6.2", diff --git a/packages/platform-core/src/errors/amplify_error.test.ts b/packages/platform-core/src/errors/amplify_error.test.ts index 1f19ac057d..94980ab213 100644 --- a/packages/platform-core/src/errors/amplify_error.test.ts +++ b/packages/platform-core/src/errors/amplify_error.test.ts @@ -165,7 +165,7 @@ void describe('AmplifyError.fromError', async () => { yargsErrors.forEach((error) => { const actual = AmplifyError.fromError(error); assert.ok( - actual instanceof AmplifyError && + AmplifyError.isAmplifyError(actual) && actual.name === 'InvalidCommandInputError', `Failed the test for error ${error.message}` ); @@ -175,7 +175,8 @@ void describe('AmplifyError.fromError', async () => { const error = new Error('getaddrinfo ENOTFOUND some-domain.com'); const actual = AmplifyError.fromError(error); assert.ok( - actual instanceof AmplifyError && actual.name === 'DomainNotFoundError', + AmplifyError.isAmplifyError(actual) && + actual.name === 'DomainNotFoundError', `Failed the test for error ${error.message}` ); }); @@ -184,8 +185,24 @@ void describe('AmplifyError.fromError', async () => { error.name = 'SyntaxError'; const actual = AmplifyError.fromError(error); assert.ok( - actual instanceof AmplifyError && actual.name === 'SyntaxError', + AmplifyError.isAmplifyError(actual) && actual.name === 'SyntaxError', `Failed the test for error ${error.message}` ); }); + void it('wraps InsufficientDiskSpaceError in AmplifyUserError', () => { + const insufficientDiskSpaceErrors = [ + new Error( + "ENOSPC: no space left on device, open '/some/path/amplify_outputs.json'" + ), + new Error('npm ERR! code ENOSPC'), + ]; + insufficientDiskSpaceErrors.forEach((error) => { + const actual = AmplifyError.fromError(error); + assert.ok( + AmplifyError.isAmplifyError(actual) && + actual.name === 'InsufficientDiskSpaceError', + `Failed the test for error ${error.message}` + ); + }); + }); }); diff --git a/packages/platform-core/src/errors/amplify_error.ts b/packages/platform-core/src/errors/amplify_error.ts index e9c524d572..9ea73c8ff1 100644 --- a/packages/platform-core/src/errors/amplify_error.ts +++ b/packages/platform-core/src/errors/amplify_error.ts @@ -44,7 +44,7 @@ export abstract class AmplifyError extends Error { this.code = options.code; this.link = options.link; - if (cause && cause instanceof AmplifyError) { + if (cause && AmplifyError.isAmplifyError(cause)) { cause.serializedError = undefined; } this.serializedError = JSON.stringify( @@ -98,11 +98,32 @@ export abstract class AmplifyError extends Error { return undefined; }; + /** + * This function is a type predicate for AmplifyError. + * See https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates. + * + * Checks if error is an AmplifyError by inspecting if required properties are set. + * This is recommended instead of instanceof operator. + * The instance of operator does not work as expected if AmplifyError class is loaded + * from multiple sources, for example when package manager decides to not de-duplicate dependencies. + * See https://github.com/nodejs/node/issues/17943. + */ + static isAmplifyError = (error: unknown): error is AmplifyError => { + return ( + error instanceof Error && + 'classification' in error && + (error.classification === 'ERROR' || error.classification === 'FAULT') && + typeof error.name === 'string' && + typeof error.message === 'string' + ); + }; + static fromError = ( error: unknown ): AmplifyError< | 'UnknownFault' | 'CredentialsError' + | 'InsufficientDiskSpaceError' | 'InvalidCommandInputError' | 'DomainNotFoundError' | 'SyntaxError' @@ -159,6 +180,17 @@ export abstract class AmplifyError extends Error { error ); } + if (error instanceof Error && isInsufficientDiskSpaceError(error)) { + return new AmplifyUserError( + 'InsufficientDiskSpaceError', + { + message: error.message, + resolution: + 'There appears to be insufficient space on your system to finish. Clear up some disk space and try again.', + }, + error + ); + } return new AmplifyFault( 'UnknownFault', { @@ -200,6 +232,15 @@ const isSyntaxError = (err?: Error): boolean => { return !!err && err.name === 'SyntaxError'; }; +const isInsufficientDiskSpaceError = (err?: Error): boolean => { + return ( + !!err && + ['ENOSPC: no space left on device', 'code ENOSPC'].some((message) => + err.message.includes(message) + ) + ); +}; + /** * Amplify exception classifications */ diff --git a/packages/platform-core/src/usage-data/usage_data.ts b/packages/platform-core/src/usage-data/usage_data.ts index 31190a5ffa..68773639ed 100644 --- a/packages/platform-core/src/usage-data/usage_data.ts +++ b/packages/platform-core/src/usage-data/usage_data.ts @@ -16,4 +16,5 @@ export type UsageData = { accountId: string; input: { command: string; plugin: string }; codePathDurations: { platformStartup?: number; totalDuration?: number }; + projectSetting: { editor?: string }; }; diff --git a/packages/platform-core/src/usage-data/usage_data_emitter.test.ts b/packages/platform-core/src/usage-data/usage_data_emitter.test.ts index 0054232702..89c53c5ad4 100644 --- a/packages/platform-core/src/usage-data/usage_data_emitter.test.ts +++ b/packages/platform-core/src/usage-data/usage_data_emitter.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, mock, test } from 'node:test'; +import { after, afterEach, before, describe, mock, test } from 'node:test'; import assert from 'node:assert'; import { DefaultUsageDataEmitter } from './usage_data_emitter'; import { v4, validate } from 'uuid'; @@ -12,6 +12,9 @@ import { UsageData } from './usage_data'; import isCI from 'is-ci'; import { AmplifyError, AmplifyUserError } from '..'; +const originalNpmUserAgent = process.env.npm_config_user_agent; +const testNpmUserAgent = 'testNpmUserAgent'; + void describe('UsageDataEmitter', () => { let usageDataEmitter: DefaultUsageDataEmitter; @@ -39,6 +42,14 @@ void describe('UsageDataEmitter', () => { mock.method(https, 'request', () => reqMock); + before(() => { + process.env.npm_config_user_agent = testNpmUserAgent; + }); + + after(() => { + process.env.npm_config_user_agent = originalNpmUserAgent; + }); + afterEach(() => { onReqEndMock.mock.resetCalls(); onReqEndMock.mock.restore(); @@ -72,6 +83,10 @@ void describe('UsageDataEmitter', () => { assert.deepStrictEqual(usageDataSent.isCi, isCI); assert.deepStrictEqual(usageDataSent.osPlatform, os.platform()); assert.deepStrictEqual(usageDataSent.osRelease, os.release()); + assert.deepStrictEqual( + usageDataSent.projectSetting.editor, + testNpmUserAgent + ); assert.ok(validate(usageDataSent.sessionUuid)); assert.ok(validate(usageDataSent.installationUuid)); assert.ok(usageDataSent.error == undefined); @@ -105,6 +120,10 @@ void describe('UsageDataEmitter', () => { assert.deepStrictEqual(usageDataSent.isCi, isCI); assert.deepStrictEqual(usageDataSent.osPlatform, os.platform()); assert.deepStrictEqual(usageDataSent.osRelease, os.release()); + assert.deepStrictEqual( + usageDataSent.projectSetting.editor, + testNpmUserAgent + ); assert.ok(validate(usageDataSent.sessionUuid)); assert.ok(validate(usageDataSent.installationUuid)); assert.strictEqual(usageDataSent.error?.message, 'some error message'); diff --git a/packages/platform-core/src/usage-data/usage_data_emitter.ts b/packages/platform-core/src/usage-data/usage_data_emitter.ts index d108dc9fc7..2677c9b447 100644 --- a/packages/platform-core/src/usage-data/usage_data_emitter.ts +++ b/packages/platform-core/src/usage-data/usage_data_emitter.ts @@ -64,7 +64,7 @@ export class DefaultUsageDataEmitter implements UsageDataEmitter { metrics?: Record; dimensions?: Record; error?: AmplifyError; - }) => { + }): Promise => { return { accountId: await this.accountIdFetcher.fetch(), sessionUuid: this.sessionUuid, @@ -86,6 +86,9 @@ export class DefaultUsageDataEmitter implements UsageDataEmitter { codePathDurations: this.translateMetricsToUsageData(options.metrics), input: this.translateDimensionsToUsageData(options.dimensions), isCi: isCI, + projectSetting: { + editor: process.env.npm_config_user_agent, + }, }; }; diff --git a/packages/plugin-types/API.md b/packages/plugin-types/API.md index 58b1302c88..2b78dd2ae1 100644 --- a/packages/plugin-types/API.md +++ b/packages/plugin-types/API.md @@ -26,6 +26,11 @@ import { Stack } from 'aws-cdk-lib'; // @public (undocumented) export type AmplifyFunction = ResourceProvider; +// @public +export type AmplifyResourceGroupName = 'auth' | 'data' | 'storage' | (string & { + resourceGroupNameLike?: any; +}); + // @public export type AppId = string; @@ -43,6 +48,7 @@ export type AuthResources = { userPoolClient: IUserPoolClient; authenticatedUserIamRole: IRole; unauthenticatedUserIamRole: IRole; + identityPoolId: string; cfnResources: AuthCfnResources; groups: { [groupName: string]: { @@ -192,6 +198,20 @@ export type PackageManagerController = { // @public (undocumented) export type ProjectName = string; +// @public +export type ReferenceAuthResources = { + userPool: IUserPool; + userPoolClient: IUserPoolClient; + authenticatedUserIamRole: IRole; + unauthenticatedUserIamRole: IRole; + identityPoolId: string; + groups: { + [groupName: string]: { + role: IRole; + }; + }; +}; + // @public (undocumented) export type ResolvePathResult = { branchSecretPath: string; @@ -238,6 +258,11 @@ export type StableBackendIdentifiers = { getStableBackendHash: () => string; }; +// @public (undocumented) +export type StackProvider = { + stack: Stack; +}; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/plugin-types/CHANGELOG.md b/packages/plugin-types/CHANGELOG.md index 1fe3d4c027..88512aac06 100644 --- a/packages/plugin-types/CHANGELOG.md +++ b/packages/plugin-types/CHANGELOG.md @@ -1,5 +1,35 @@ # @aws-amplify/plugin-types +## 1.5.0 + +### Minor Changes + +- f1db886: add resourceGroupName prop to function + +## 1.4.0 + +### Minor Changes + +- 90a7c49: Add support for referenceAuth. + +## 1.3.1 + +### Patch Changes + +- b56d344: update aws-cdk lib to ^2.158.0 + +## 1.3.0 + +### Minor Changes + +- 87dbf41: add new type to handle exposing stack + +## 1.2.2 + +### Patch Changes + +- 8dd7286: fixed errors in plugin-types and cli-core along with any extraneous dependencies in other packages + ## 1.2.1 ### Patch Changes diff --git a/packages/plugin-types/package.json b/packages/plugin-types/package.json index 81a4624688..d60ad1902c 100644 --- a/packages/plugin-types/package.json +++ b/packages/plugin-types/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/plugin-types", - "version": "1.2.1", + "version": "1.5.0", "types": "lib/index.d.ts", "type": "commonjs", "publishConfig": { @@ -11,7 +11,7 @@ }, "license": "Apache-2.0", "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0", "@aws-sdk/types": "^3.609.0" }, diff --git a/packages/plugin-types/src/amplify_resource_group_name.ts b/packages/plugin-types/src/amplify_resource_group_name.ts new file mode 100644 index 0000000000..45bfbabcd9 --- /dev/null +++ b/packages/plugin-types/src/amplify_resource_group_name.ts @@ -0,0 +1,12 @@ +/** + * Represents the types of resource group name + */ +export type AmplifyResourceGroupName = + | 'auth' + | 'data' + | 'storage' + // eslint-disable-next-line spellcheck/spell-checker + // `(string & { resourceGroupNameLike?: any} )` is a workaround to allow default resource group names to show up in IntelliSense while allowing any string to be passed. + // See https://github.com/microsoft/TypeScript/issues/29729#issuecomment-460346421. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | (string & { resourceGroupNameLike?: any }); diff --git a/packages/plugin-types/src/auth_resources.ts b/packages/plugin-types/src/auth_resources.ts index 3b571a497c..0112e06a31 100644 --- a/packages/plugin-types/src/auth_resources.ts +++ b/packages/plugin-types/src/auth_resources.ts @@ -51,6 +51,10 @@ export type AuthResources = { * The generated unauth role. */ unauthenticatedUserIamRole: IRole; + /** + * Identity pool Id + */ + identityPoolId: string; /** * L1 Cfn Resources, for when dipping down a level of abstraction is desirable. */ @@ -72,6 +76,43 @@ export type AuthResources = { }; }; +/** + * Reference auth resources + */ +export type ReferenceAuthResources = { + /** + * The referenced UserPool L2 Resource. + */ + userPool: IUserPool; + /** + * The referenced UserPoolClient L2 Resource. + */ + userPoolClient: IUserPoolClient; + /** + * The referenced auth role. + */ + authenticatedUserIamRole: IRole; + /** + * The referenced unauth role. + */ + unauthenticatedUserIamRole: IRole; + /** + * Identity pool Id + */ + identityPoolId: string; + /** + * A map of existing group names and their associated group role. + */ + groups: { + [groupName: string]: { + /** + * The generated Role for this group + */ + role: IRole; + }; + }; +}; + export type AuthRoleName = keyof Pick< AuthResources, 'authenticatedUserIamRole' | 'unauthenticatedUserIamRole' diff --git a/packages/plugin-types/src/index.ts b/packages/plugin-types/src/index.ts index 3b8c7bf18e..780c52ec02 100644 --- a/packages/plugin-types/src/index.ts +++ b/packages/plugin-types/src/index.ts @@ -20,3 +20,5 @@ export * from './deep_partial.js'; export * from './stable_backend_identifiers.js'; export * from './resource_name_validator.js'; export * from './aws_client_provider.js'; +export * from './stack_provider.js'; +export * from './amplify_resource_group_name.js'; diff --git a/packages/plugin-types/src/stack_provider.ts b/packages/plugin-types/src/stack_provider.ts new file mode 100644 index 0000000000..1c88482f7b --- /dev/null +++ b/packages/plugin-types/src/stack_provider.ts @@ -0,0 +1,5 @@ +import { Stack } from 'aws-cdk-lib'; + +export type StackProvider = { + stack: Stack; +}; diff --git a/packages/sandbox/CHANGELOG.md b/packages/sandbox/CHANGELOG.md index a129089dab..42bdfd2209 100644 --- a/packages/sandbox/CHANGELOG.md +++ b/packages/sandbox/CHANGELOG.md @@ -1,5 +1,63 @@ # @aws-amplify/sandbox +## 1.2.6 + +### Patch Changes + +- 8c8fc5e: Print bootstrap url when browser cannot be opened +- Updated dependencies [f1db886] +- Updated dependencies [71ef398] + - @aws-amplify/plugin-types@1.5.0 + - @aws-amplify/platform-core@1.2.1 + +## 1.2.5 + +### Patch Changes + +- 583a3f2: Fix detection of AmplifyErrors +- Updated dependencies [583a3f2] + - @aws-amplify/platform-core@1.2.0 + - @aws-amplify/backend-deployer@1.1.8 + +## 1.2.4 + +### Patch Changes + +- b56d344: update aws-cdk lib to ^2.158.0 +- Updated dependencies [c3c3057] +- Updated dependencies [b56d344] + - @aws-amplify/cli-core@1.2.0 + - @aws-amplify/backend-deployer@1.1.6 + - @aws-amplify/client-config@1.5.1 + - @aws-amplify/plugin-types@1.3.1 + +## 1.2.3 + +### Patch Changes + +- 0a5e51c: Stream conversation logs in sandbox + +## 1.2.2 + +### Patch Changes + +- e648e8e: added main field to package.json so these packages are resolvable +- 0ff73ec: add ExpiredToken in the list of credentials error +- 8dd7286: fixed errors in plugin-types and cli-core along with any extraneous dependencies in other packages +- e648e8e: added main field to packages known to lack one +- Updated dependencies [e648e8e] +- Updated dependencies [0ff73ec] +- Updated dependencies [c9c873c] +- Updated dependencies [cbac105] +- Updated dependencies [8dd7286] +- Updated dependencies [e648e8e] + - @aws-amplify/deployed-backend-client@1.4.1 + - @aws-amplify/backend-deployer@1.1.3 + - @aws-amplify/backend-secret@1.1.2 + - @aws-amplify/client-config@1.3.1 + - @aws-amplify/plugin-types@1.2.2 + - @aws-amplify/cli-core@1.1.3 + ## 1.2.1 ### Patch Changes diff --git a/packages/sandbox/package.json b/packages/sandbox/package.json index 36bfaf4c41..ff96f11959 100644 --- a/packages/sandbox/package.json +++ b/packages/sandbox/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/sandbox", - "version": "1.2.1", + "version": "1.2.6", "type": "module", "publishConfig": { "access": "public" @@ -19,20 +19,18 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/backend-deployer": "^1.1.0", - "@aws-amplify/backend-secret": "^1.1.1", - "@aws-amplify/cli-core": "^1.1.2", - "@aws-amplify/client-config": "^1.1.3", - "@aws-amplify/deployed-backend-client": "^1.3.0", - "@aws-amplify/platform-core": "^1.0.6", - "@aws-amplify/plugin-types": "^1.2.1", - "@aws-sdk/client-cloudformation": "^3.624.0", + "@aws-amplify/backend-deployer": "^1.1.8", + "@aws-amplify/backend-secret": "^1.1.2", + "@aws-amplify/cli-core": "^1.2.0", + "@aws-amplify/client-config": "^1.5.1", + "@aws-amplify/deployed-backend-client": "^1.4.1", + "@aws-amplify/platform-core": "^1.2.1", + "@aws-amplify/plugin-types": "^1.5.0", "@aws-sdk/client-cloudwatch-logs": "^3.624.0", "@aws-sdk/client-lambda": "^3.624.0", "@aws-sdk/client-ssm": "^3.624.0", "@aws-sdk/credential-providers": "^3.624.0", "@aws-sdk/types": "^3.609.0", - "@aws-sdk/util-arn-parser": "^3.568.0", "@parcel/watcher": "^2.4.1", "debounce-promise": "^3.1.2", "glob": "^10.2.7", @@ -44,6 +42,6 @@ "@types/parse-gitignore": "^1.0.0" }, "peerDependencies": { - "aws-cdk": "^2.152.0" + "aws-cdk": "^2.158.0" } } diff --git a/packages/sandbox/src/file_watching_sandbox.test.ts b/packages/sandbox/src/file_watching_sandbox.test.ts index 390d917426..68e410a56f 100644 --- a/packages/sandbox/src/file_watching_sandbox.test.ts +++ b/packages/sandbox/src/file_watching_sandbox.test.ts @@ -187,6 +187,53 @@ void describe('Sandbox to check if region is bootstrapped', () => { openMock.mock.calls[0].arguments[0], getBootstrapUrl(region) ); + assert.strictEqual(printer.log.mock.callCount(), 1); + assert.strictEqual( + printer.log.mock.calls[0].arguments[0], + 'The given region has not been bootstrapped. Sign in to console as a Root user or Admin to complete the bootstrap process, then restart the sandbox.' + ); + assert.strictEqual(printer.log.mock.calls[0].arguments[1], undefined); + }); + + void it('when region has not bootstrapped, and opening console url fails prints url to initiate bootstrap', async () => { + ssmClientSendMock.mock.mockImplementationOnce(() => { + throw new ParameterNotFound({ + $metadata: {}, + message: 'Parameter not found', + }); + }); + + openMock.mock.mockImplementationOnce(() => + Promise.reject(new Error('open error')) + ); + + await sandboxInstance.start({ + dir: 'testDir', + exclude: ['exclude1', 'exclude2'], + }); + + assert.strictEqual(ssmClientSendMock.mock.callCount(), 1); + assert.strictEqual(openMock.mock.callCount(), 1); + assert.strictEqual( + openMock.mock.calls[0].arguments[0], + getBootstrapUrl(region) + ); + assert.strictEqual(printer.log.mock.callCount(), 3); + assert.strictEqual( + printer.log.mock.calls[0].arguments[0], + 'The given region has not been bootstrapped. Sign in to console as a Root user or Admin to complete the bootstrap process, then restart the sandbox.' + ); + assert.strictEqual(printer.log.mock.calls[0].arguments[1], undefined); + assert.strictEqual( + printer.log.mock.calls[1].arguments[0], + 'Unable to open bootstrap url, open error' + ); + assert.strictEqual(printer.log.mock.calls[1].arguments[1], LogLevel.DEBUG); + assert.strictEqual( + printer.log.mock.calls[2].arguments[0], + `Open ${getBootstrapUrl(region)} in the browser.` + ); + assert.strictEqual(printer.log.mock.calls[2].arguments[1], undefined); }); void it('when user does not have proper credentials throw user error', async () => { diff --git a/packages/sandbox/src/file_watching_sandbox.ts b/packages/sandbox/src/file_watching_sandbox.ts index 39891c1347..aa1bb1c6a1 100644 --- a/packages/sandbox/src/file_watching_sandbox.ts +++ b/packages/sandbox/src/file_watching_sandbox.ts @@ -37,6 +37,7 @@ import { BackendIdentifierConversions, } from '@aws-amplify/platform-core'; import { LambdaFunctionLogStreamer } from './lambda_function_log_streamer.js'; + /** * CDK stores bootstrap version in parameter store. Example parameter name looks like /cdk-bootstrap//version. * The default value for qualifier is hnb659fds, i.e. default parameter path is /cdk-bootstrap/hnb659fds/version. @@ -125,7 +126,23 @@ export class FileWatchingSandbox extends EventEmitter implements Sandbox { ); // get region from an available sdk client; const region = await this.ssmClient.config.region(); - await this.open(getBootstrapUrl(region)); + const bootstrapUrl = getBootstrapUrl(region); + try { + await this.open(bootstrapUrl); + } catch (e) { + // If opening the link fails for any reason we fall back to + // printing the url in the console. + // This might happen: + // - in headless environments + // - if user does not have any app to open URL + // - if browser crashes + let logEntry = 'Unable to open bootstrap url'; + if (e instanceof Error) { + logEntry = `${logEntry}, ${e.message}`; + } + this.printer.log(logEntry, LogLevel.DEBUG); + this.printer.log(`Open ${bootstrapUrl} in the browser.`); + } return; } @@ -273,7 +290,7 @@ export class FileWatchingSandbox extends EventEmitter implements Sandbox { // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cognito-userpool.html#cfn-cognito-userpool-aliasattributes // offer to recreate the sandbox or revert the change if ( - error instanceof AmplifyError && + AmplifyError.isAmplifyError(error) && error.name === 'CFNUpdateNotSupportedError' ) { await this.handleUnsupportedDestructiveChanges(options); @@ -385,7 +402,7 @@ export class FileWatchingSandbox extends EventEmitter implements Sandbox { message = `${message}\nCaused By: ${error.cause.message}\n`; } - if (error instanceof AmplifyError && error.resolution) { + if (AmplifyError.isAmplifyError(error) && error.resolution) { message = `${message}\nResolution: ${error.resolution}\n`; } } else message = String(error); diff --git a/packages/sandbox/src/lambda_function_log_streamer.test.ts b/packages/sandbox/src/lambda_function_log_streamer.test.ts index 7ba897fea2..6072e57adc 100644 --- a/packages/sandbox/src/lambda_function_log_streamer.test.ts +++ b/packages/sandbox/src/lambda_function_log_streamer.test.ts @@ -3,21 +3,16 @@ import { LambdaFunctionLogStreamer } from './lambda_function_log_streamer.js'; import assert from 'node:assert'; import { BackendOutputClient } from '@aws-amplify/deployed-backend-client'; -import { - CloudFormationClient, - DescribeStacksOutput, -} from '@aws-sdk/client-cloudformation'; import { CloudWatchLogsClient } from '@aws-sdk/client-cloudwatch-logs'; import { + GetFunctionCommand, + GetFunctionCommandOutput, LambdaClient, - ListTagsCommand, - ListTagsCommandOutput, } from '@aws-sdk/client-lambda'; import { CloudWatchLogEventMonitor } from './cloudwatch_logs_monitor.js'; import { Printer } from '@aws-amplify/cli-core'; import { BackendIdentifier, BackendOutput } from '@aws-amplify/plugin-types'; import { TagName } from '@aws-amplify/platform-core'; -import { parse as parseArn } from '@aws-sdk/util-arn-parser'; void describe('LambdaFunctionLogStreamer', () => { const region = 'test-region'; @@ -25,20 +20,10 @@ void describe('LambdaFunctionLogStreamer', () => { 'func1FullName', 'func2FullName', ]); - - // CFN default implementation - const cfnClientMock = new CloudFormationClient({ region }); - const cfnClientSendMock = mock.fn(() => { - return Promise.resolve({ - Stacks: [ - { - StackId: - 'arn:aws:cloudformation:us-west-2:123456789012:stack/stack-name/uuid', - }, - ], - } as DescribeStacksOutput); - }); - mock.method(cfnClientMock, 'send', cfnClientSendMock); + const definedConversationHandlers = JSON.stringify([ + 'conversationHandler1FullName', + 'conversationHandler2FullName', + ]); // CW default implementation const cloudWatchClientMock = new CloudWatchLogsClient({ region }); @@ -48,15 +33,26 @@ void describe('LambdaFunctionLogStreamer', () => { // Lambda default implementation. // Given a resource Arn with lambda function name with `FullName` suffix, this will return the function name with `friendlyName` as suffix const lambdaClientMock = new LambdaClient({ region }); - const lambdaClientSendMock = mock.fn((listTagsCommand: ListTagsCommand) => { - return Promise.resolve({ - Tags: { - [TagName.FRIENDLY_NAME]: parseArn(listTagsCommand.input.Resource ?? '') - .resource?.split(':')[1] - .replace('FullName', 'FriendlyName'), - } as unknown as ListTagsCommandOutput, - }); - }); + const lambdaClientSendMock = mock.fn( + (getFunctionCommand: GetFunctionCommand) => { + return Promise.resolve({ + Configuration: { + LoggingConfig: { + LogGroup: `/aws/lambda/${ + getFunctionCommand.input.FunctionName ?? '' + }`, + }, + }, + Tags: { + [TagName.FRIENDLY_NAME]: + getFunctionCommand.input.FunctionName?.replace( + 'FullName', + 'FriendlyName' + ), + } as unknown as GetFunctionCommandOutput, + }); + } + ); mock.method(lambdaClientMock, 'send', lambdaClientSendMock); // backendOutputClient default implementation @@ -74,6 +70,12 @@ void describe('LambdaFunctionLogStreamer', () => { }, version: '1', }, + ['AWS::Amplify::AI::Conversation']: { + payload: { + definedConversationHandlers: definedConversationHandlers, + }, + version: '1', + }, } as BackendOutput); }), }; @@ -91,14 +93,12 @@ void describe('LambdaFunctionLogStreamer', () => { const classUnderTest = new LambdaFunctionLogStreamer( lambdaClientMock, - cfnClientMock, cloudWatchLogMonitorMock as unknown as CloudWatchLogEventMonitor, backendOutputClientMock as unknown as BackendOutputClient, printer as unknown as Printer ); beforeEach(() => { - cfnClientSendMock.mock.resetCalls(); cloudWatchClientSendMock.mock.resetCalls(); lambdaClientSendMock.mock.resetCalls(); backendOutputClientMock.getOutput.mock.resetCalls(); @@ -147,26 +147,34 @@ void describe('LambdaFunctionLogStreamer', () => { assert.strictEqual(lambdaClientSendMock.mock.callCount(), 0); }); - void it('calls logs monitor with all the customer defined functions if no function name filter is provided', async () => { + void it('calls logs monitor with all the customer defined functions and conversation handlers if no function name filter is provided', async () => { await classUnderTest.startStreamingLogs(testSandboxBackendId, { enabled: true, }); // assert that lambda calls to retrieve tags were with the right function arn - assert.strictEqual(lambdaClientSendMock.mock.callCount(), 2); + assert.strictEqual(lambdaClientSendMock.mock.callCount(), 4); assert.strictEqual( - lambdaClientSendMock.mock.calls[0].arguments[0].input.Resource, - 'arn:aws:lambda:us-west-2:123456789012:function:func1FullName' + lambdaClientSendMock.mock.calls[0].arguments[0].input.FunctionName, + 'func1FullName' ); assert.strictEqual( - lambdaClientSendMock.mock.calls[1].arguments[0].input.Resource, - 'arn:aws:lambda:us-west-2:123456789012:function:func2FullName' + lambdaClientSendMock.mock.calls[1].arguments[0].input.FunctionName, + 'func2FullName' + ); + assert.strictEqual( + lambdaClientSendMock.mock.calls[2].arguments[0].input.FunctionName, + 'conversationHandler1FullName' + ); + assert.strictEqual( + lambdaClientSendMock.mock.calls[3].arguments[0].input.FunctionName, + 'conversationHandler2FullName' ); // assert that logs groups were added to the monitor and was then called activate assert.strictEqual( cloudWatchLogMonitorMock.addLogGroups.mock.callCount(), - 2 + 4 ); assert.strictEqual( cloudWatchLogMonitorMock.addLogGroups.mock.calls[0].arguments[0], @@ -184,6 +192,22 @@ void describe('LambdaFunctionLogStreamer', () => { cloudWatchLogMonitorMock.addLogGroups.mock.calls[1].arguments[1], '/aws/lambda/func2FullName' ); + assert.strictEqual( + cloudWatchLogMonitorMock.addLogGroups.mock.calls[2].arguments[0], + 'conversationHandler1FriendlyName' + ); + assert.strictEqual( + cloudWatchLogMonitorMock.addLogGroups.mock.calls[2].arguments[1], + '/aws/lambda/conversationHandler1FullName' + ); + assert.strictEqual( + cloudWatchLogMonitorMock.addLogGroups.mock.calls[3].arguments[0], + 'conversationHandler2FriendlyName' + ); + assert.strictEqual( + cloudWatchLogMonitorMock.addLogGroups.mock.calls[3].arguments[1], + '/aws/lambda/conversationHandler2FullName' + ); assert.strictEqual(cloudWatchLogMonitorMock.activate.mock.callCount(), 1); }); @@ -197,14 +221,22 @@ void describe('LambdaFunctionLogStreamer', () => { // assert that lambda calls to retrieve tags were with the right function arn // We do it for all customer defined functions, filtering happens after - assert.strictEqual(lambdaClientSendMock.mock.callCount(), 2); + assert.strictEqual(lambdaClientSendMock.mock.callCount(), 4); + assert.strictEqual( + lambdaClientSendMock.mock.calls[0].arguments[0].input.FunctionName, + 'func1FullName' + ); assert.strictEqual( - lambdaClientSendMock.mock.calls[0].arguments[0].input.Resource, - 'arn:aws:lambda:us-west-2:123456789012:function:func1FullName' + lambdaClientSendMock.mock.calls[1].arguments[0].input.FunctionName, + 'func2FullName' ); assert.strictEqual( - lambdaClientSendMock.mock.calls[1].arguments[0].input.Resource, - 'arn:aws:lambda:us-west-2:123456789012:function:func2FullName' + lambdaClientSendMock.mock.calls[2].arguments[0].input.FunctionName, + 'conversationHandler1FullName' + ); + assert.strictEqual( + lambdaClientSendMock.mock.calls[3].arguments[0].input.FunctionName, + 'conversationHandler2FullName' ); // assert that logs groups were added to the monitor for only filtered functions and was then called activate @@ -231,14 +263,22 @@ void describe('LambdaFunctionLogStreamer', () => { // assert that lambda calls to retrieve tags were with the right function arn // We do it for all customer defined functions, filtering happens after - assert.strictEqual(lambdaClientSendMock.mock.callCount(), 2); + assert.strictEqual(lambdaClientSendMock.mock.callCount(), 4); + assert.strictEqual( + lambdaClientSendMock.mock.calls[0].arguments[0].input.FunctionName, + 'func1FullName' + ); + assert.strictEqual( + lambdaClientSendMock.mock.calls[1].arguments[0].input.FunctionName, + 'func2FullName' + ); assert.strictEqual( - lambdaClientSendMock.mock.calls[0].arguments[0].input.Resource, - 'arn:aws:lambda:us-west-2:123456789012:function:func1FullName' + lambdaClientSendMock.mock.calls[2].arguments[0].input.FunctionName, + 'conversationHandler1FullName' ); assert.strictEqual( - lambdaClientSendMock.mock.calls[1].arguments[0].input.Resource, - 'arn:aws:lambda:us-west-2:123456789012:function:func2FullName' + lambdaClientSendMock.mock.calls[3].arguments[0].input.FunctionName, + 'conversationHandler2FullName' ); // assert that logs groups were added to the monitor for only filtered functions and was then called activate @@ -273,14 +313,22 @@ void describe('LambdaFunctionLogStreamer', () => { // assert that lambda calls to retrieve tags were with the right function arn // We do it for all customer defined functions, filtering happens after - assert.strictEqual(lambdaClientSendMock.mock.callCount(), 2); + assert.strictEqual(lambdaClientSendMock.mock.callCount(), 4); + assert.strictEqual( + lambdaClientSendMock.mock.calls[0].arguments[0].input.FunctionName, + 'func1FullName' + ); + assert.strictEqual( + lambdaClientSendMock.mock.calls[1].arguments[0].input.FunctionName, + 'func2FullName' + ); assert.strictEqual( - lambdaClientSendMock.mock.calls[0].arguments[0].input.Resource, - 'arn:aws:lambda:us-west-2:123456789012:function:func1FullName' + lambdaClientSendMock.mock.calls[2].arguments[0].input.FunctionName, + 'conversationHandler1FullName' ); assert.strictEqual( - lambdaClientSendMock.mock.calls[1].arguments[0].input.Resource, - 'arn:aws:lambda:us-west-2:123456789012:function:func2FullName' + lambdaClientSendMock.mock.calls[3].arguments[0].input.FunctionName, + 'conversationHandler2FullName' ); // assert that no logs groups were added to the monitor diff --git a/packages/sandbox/src/lambda_function_log_streamer.ts b/packages/sandbox/src/lambda_function_log_streamer.ts index 7468849596..ee3ac62019 100644 --- a/packages/sandbox/src/lambda_function_log_streamer.ts +++ b/packages/sandbox/src/lambda_function_log_streamer.ts @@ -1,17 +1,10 @@ import { LogLevel, Printer } from '@aws-amplify/cli-core'; import { BackendOutputClient } from '@aws-amplify/deployed-backend-client'; -import { - BackendIdentifierConversions, - TagName, -} from '@aws-amplify/platform-core'; +import { TagName } from '@aws-amplify/platform-core'; import { BackendIdentifier, BackendOutput } from '@aws-amplify/plugin-types'; -import { - CloudFormationClient, - DescribeStacksCommand, -} from '@aws-sdk/client-cloudformation'; -import { LambdaClient, ListTagsCommand } from '@aws-sdk/client-lambda'; + +import { GetFunctionCommand, LambdaClient } from '@aws-sdk/client-lambda'; import { CloudWatchLogEventMonitor } from './cloudwatch_logs_monitor.js'; -import { build as buildArn, parse as parseArn } from '@aws-sdk/util-arn-parser'; import { SandboxFunctionStreamingOptions } from './sandbox.js'; /** @@ -24,7 +17,6 @@ export class LambdaFunctionLogStreamer { */ constructor( private readonly lambda: LambdaClient, - private readonly cfnClient: CloudFormationClient, private readonly logsMonitor: CloudWatchLogEventMonitor, private readonly backendOutputClient: BackendOutputClient, private readonly printer: Printer @@ -50,37 +42,38 @@ export class LambdaFunctionLogStreamer { const definedFunctionsPayload = backendOutput['AWS::Amplify::Function']?.payload.definedFunctions; + const definedConversationHandlersPayload = + backendOutput['AWS::Amplify::AI::Conversation']?.payload + .definedConversationHandlers; const deployedFunctionNames = definedFunctionsPayload ? (JSON.parse(definedFunctionsPayload) as string[]) : []; - - // To use list-tags API we need to convert function name to function Arn since it only accepts ARN as input - const deployedFunctionNameToArnMap = await this.getFunctionArnFromNames( - sandboxBackendId, - deployedFunctionNames + deployedFunctionNames.push( + ...(definedConversationHandlersPayload + ? (JSON.parse(definedConversationHandlersPayload) as string[]) + : []) ); - if (!deployedFunctionNameToArnMap) { - this.printer.log( - `[Sandbox] Could not find any function in stack ${BackendIdentifierConversions.toStackName( - sandboxBackendId - )}. Streaming function logs will be turned off.`, - LogLevel.DEBUG - ); - return; - } - - for (const entry of deployedFunctionNameToArnMap) { - const listTagsResponse = await this.lambda.send( - new ListTagsCommand({ - Resource: entry.arn, + for (const functionName of deployedFunctionNames) { + const getFunctionResponse = await this.lambda.send( + new GetFunctionCommand({ + FunctionName: functionName, }) ); + const logGroupName = + getFunctionResponse.Configuration?.LoggingConfig?.LogGroup; + if (!logGroupName) { + this.printer.log( + `[Sandbox] Could not find logGroup for lambda function ${functionName}. Logs will not be streamed for this function.`, + LogLevel.DEBUG + ); + continue; + } const friendlyFunctionName = - listTagsResponse.Tags?.[TagName.FRIENDLY_NAME]; + getFunctionResponse.Tags?.[TagName.FRIENDLY_NAME]; if (!friendlyFunctionName) { this.printer.log( - `[Sandbox] Could not find user defined name for lambda function ${entry.name}. Logs will not be streamed for this function.`, + `[Sandbox] Could not find user defined name for lambda function ${functionName}. Logs will not be streamed for this function.`, LogLevel.DEBUG ); continue; @@ -109,11 +102,7 @@ export class LambdaFunctionLogStreamer { } if (shouldStreamLogs) { - this.logsMonitor?.addLogGroups( - friendlyFunctionName, - // a CW log group is implicitly created for each lambda function with the lambda function's name - `/aws/lambda/${entry.name}` - ); + this.logsMonitor?.addLogGroups(friendlyFunctionName, logGroupName); } else { this.printer.log( `[Sandbox] Skipping logs streaming for function ${friendlyFunctionName} since it did not match any filters. To stream logs for this function, ensure at least one of your logs-filters match this function name.`, @@ -136,50 +125,4 @@ export class LambdaFunctionLogStreamer { ); this.logsMonitor?.pause(); }; - - /** - * Adds functionArn for each function name provided. All the ARN components are taken from the root stack Arn - * @param sandboxBackendId backendId for retrieving the root stack - * @param functionNames Name of the functions for which ARN needs to be generated - * @returns An object containing function name and ARN for each function name provided - */ - private getFunctionArnFromNames = async ( - sandboxBackendId: BackendIdentifier, - functionNames?: string[] - ) => { - if (!functionNames || functionNames.length === 0) { - return; - } - - const rootStackResources = await this.cfnClient.send( - new DescribeStacksCommand({ - StackName: BackendIdentifierConversions.toStackName(sandboxBackendId), - }) - ); - - if (!rootStackResources?.Stacks?.[0]?.StackId) { - this.printer.log( - `[Sandbox] Cannot load root stack for Id ${BackendIdentifierConversions.toStackName( - sandboxBackendId - )}. Streaming function logs will be turned off.`, - LogLevel.DEBUG - ); - return; - } - - const arnParts = parseArn(rootStackResources.Stacks[0].StackId); - - return functionNames.map((name) => { - return { - name, - arn: buildArn({ - resource: `function:${name}`, - service: 'lambda', - accountId: arnParts.accountId, - partition: arnParts.partition, - region: arnParts.region, - }), - }; - }); - }; } diff --git a/packages/sandbox/src/sandbox_singleton_factory.ts b/packages/sandbox/src/sandbox_singleton_factory.ts index b4d872ab27..f1f36344e8 100644 --- a/packages/sandbox/src/sandbox_singleton_factory.ts +++ b/packages/sandbox/src/sandbox_singleton_factory.ts @@ -14,7 +14,6 @@ import { LambdaClient } from '@aws-sdk/client-lambda'; import { BackendOutputClientFactory } from '@aws-amplify/deployed-backend-client'; import { LambdaFunctionLogStreamer } from './lambda_function_log_streamer.js'; import { CloudWatchLogEventMonitor } from './cloudwatch_logs_monitor.js'; -import { CloudFormationClient } from '@aws-sdk/client-cloudformation'; /** * Factory to create a new sandbox @@ -41,7 +40,6 @@ export class SandboxSingletonFactory { packageManagerControllerFactory.getPackageManagerController(), this.format ); - const cfnClient = new CloudFormationClient(); this.instance = new FileWatchingSandbox( this.sandboxIdResolver, new AmplifySandboxExecutor( @@ -52,7 +50,6 @@ export class SandboxSingletonFactory { new SSMClient(), new LambdaFunctionLogStreamer( new LambdaClient(), - cfnClient, new CloudWatchLogEventMonitor(new CloudWatchLogsClient()), BackendOutputClientFactory.getInstance(), this.printer diff --git a/packages/schema-generator/CHANGELOG.md b/packages/schema-generator/CHANGELOG.md index 767c6a2687..488a2cbb71 100644 --- a/packages/schema-generator/CHANGELOG.md +++ b/packages/schema-generator/CHANGELOG.md @@ -1,5 +1,26 @@ # @aws-amplify/schema-generator +## 1.2.5 + +### Patch Changes + +- b56d344: update aws-cdk lib to ^2.158.0 +- b56d344: Upgrade @aws-amplify/graphql-schema-generator to v0.11.0 + +## 1.2.4 + +### Patch Changes + +- e325044: Prefer amplify errors in generators +- f6b1943: Handle schema errors + +## 1.2.3 + +### Patch Changes + +- e648e8e: added main field to package.json so these packages are resolvable +- e648e8e: added main field to packages known to lack one + ## 1.2.2 ### Patch Changes diff --git a/packages/schema-generator/package.json b/packages/schema-generator/package.json index 160643bd6f..f860dbc562 100644 --- a/packages/schema-generator/package.json +++ b/packages/schema-generator/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/schema-generator", - "version": "1.2.2", + "version": "1.2.5", "type": "module", "publishConfig": { "access": "public" @@ -18,7 +18,7 @@ "update:api": "api-extractor run --local" }, "dependencies": { - "@aws-amplify/graphql-schema-generator": "^0.9.4", + "@aws-amplify/graphql-schema-generator": "^0.11.0", "@aws-amplify/platform-core": "^1.0.5" }, "license": "Apache-2.0" diff --git a/packages/schema-generator/src/generate_schema.test.ts b/packages/schema-generator/src/generate_schema.test.ts index e281560cdd..625cdb4077 100644 --- a/packages/schema-generator/src/generate_schema.test.ts +++ b/packages/schema-generator/src/generate_schema.test.ts @@ -2,10 +2,13 @@ import { beforeEach, describe, it, mock } from 'node:test'; import { SchemaGenerator, parseDatabaseUrl } from './generate_schema.js'; import assert from 'node:assert'; import { + EmptySchemaError, + InvalidSchemaError, TypescriptDataSchemaGenerator, TypescriptDataSchemaGeneratorConfig, } from '@aws-amplify/graphql-schema-generator'; import fs from 'fs/promises'; +import { AmplifyUserError } from '@aws-amplify/platform-core'; const mockGenerateMethod = mock.fn<(config: TypescriptDataSchemaGeneratorConfig) => Promise>(); @@ -154,4 +157,63 @@ void describe('SchemaGenerator', () => { 'Unable to parse the database URL. One or more parts of the database URL is missing. Missing [username, password].', }); }); + + void it('should throw error if database engine is incorrect', async () => { + const parse = () => + parseDatabaseUrl('incorrect://user:password@test-host-name/db'); + assert.throws(parse, { + name: 'DatabaseUrlParseError', + message: + 'Unable to parse the database URL. Unsupported database engine: incorrect', + }); + }); + + void it('should throw error if database schema is incorrect', async () => { + mockGenerateMethod.mock.mockImplementationOnce(() => { + throw new InvalidSchemaError([{}], ['missingColumn']); + }); + const schemaGenerator = new SchemaGenerator(); + await assert.rejects( + () => + schemaGenerator.generate({ + connectionUri: { + secretName: 'FAKE_SECRET_NAME', + value: 'mysql://user:password@hostname:3306/db', + }, + out: 'schema.ts', + }), + (error: AmplifyUserError) => { + assert.strictEqual(error.name, 'DatabaseSchemaError'); + assert.strictEqual( + error.message, + 'Imported SQL schema is invalid. Imported schema is missing columns: missingColumn' + ); + assert.strictEqual(error.resolution, 'Check the database schema.'); + return true; + } + ); + }); + + void it('should throw error if database schema is empty', async () => { + mockGenerateMethod.mock.mockImplementationOnce(() => { + throw new EmptySchemaError(); + }); + const schemaGenerator = new SchemaGenerator(); + await assert.rejects( + () => + schemaGenerator.generate({ + connectionUri: { + secretName: 'FAKE_SECRET_NAME', + value: 'mysql://user:password@hostname:3306/db', + }, + out: 'schema.ts', + }), + (error: AmplifyUserError) => { + assert.strictEqual(error.name, 'DatabaseSchemaError'); + assert.strictEqual(error.message, 'Imported SQL schema is empty.'); + assert.strictEqual(error.resolution, 'Check the database schema.'); + return true; + } + ); + }); }); diff --git a/packages/schema-generator/src/generate_schema.ts b/packages/schema-generator/src/generate_schema.ts index bfe5e28ea8..49fc857b86 100644 --- a/packages/schema-generator/src/generate_schema.ts +++ b/packages/schema-generator/src/generate_schema.ts @@ -1,4 +1,8 @@ -import { TypescriptDataSchemaGenerator } from '@aws-amplify/graphql-schema-generator'; +import { + EmptySchemaError, + InvalidSchemaError, + TypescriptDataSchemaGenerator, +} from '@aws-amplify/graphql-schema-generator'; import fs from 'fs/promises'; import { AmplifyUserError } from '@aws-amplify/platform-core'; @@ -16,6 +20,8 @@ export type SchemaGeneratorConfig = { type AmplifyGenerateSchemaError = | 'DatabaseConnectionError' + | 'DatabaseSchemaError' + | 'DatabaseUnsupportedEngineError' | 'DatabaseUrlParseError'; /** @@ -37,9 +43,22 @@ export class SchemaGenerator { }); await fs.writeFile(props.out, schema); } catch (err) { + if ( + err instanceof EmptySchemaError || + err instanceof InvalidSchemaError + ) { + throw new AmplifyUserError( + 'DatabaseSchemaError', + { + // the message already contains descriptive error. + message: err.message, + resolution: 'Check the database schema.', + }, + err + ); + } const databaseError = err as DatabaseConnectError; if (databaseError.code === 'ETIMEDOUT') { - // eslint-disable-next-line amplify-backend-rules/no-amplify-errors throw new AmplifyUserError( 'DatabaseConnectionError', { @@ -118,7 +137,6 @@ export const parseDatabaseUrl = (databaseUrl: string): SQLDataSourceConfig => { ).filter((part) => !config[part]); if (missingParts.length > 0) { - // eslint-disable-next-line amplify-backend-rules/no-amplify-errors throw new AmplifyUserError( 'DatabaseUrlParseError', { @@ -134,7 +152,6 @@ export const parseDatabaseUrl = (databaseUrl: string): SQLDataSourceConfig => { return config; } catch (err) { const error = err as Error; - // eslint-disable-next-line amplify-backend-rules/no-amplify-errors throw new AmplifyUserError( 'DatabaseUrlParseError', { @@ -153,7 +170,14 @@ const constructDBEngine = (engine: string): SQLEngine => { case 'postgres': return 'postgresql'; default: - throw new Error(`Unsupported database engine: ${engine}`); + throw new AmplifyUserError( + 'DatabaseUnsupportedEngineError', + { + message: `Unsupported database engine: ${engine}`, + resolution: + 'Ensure that database URL specifies supported engine. Supported engines are "mysql", "postgresql", "postgres".', + } + ); } }; diff --git a/scripts/check_api_changes.ts b/scripts/check_api_changes.ts index e80682f4fd..b574e8d23c 100644 --- a/scripts/check_api_changes.ts +++ b/scripts/check_api_changes.ts @@ -47,6 +47,18 @@ console.log( const packagePaths = await glob(`${latestRepositoryPath}/packages/*`); +const excludedTypesByPackageName: Record> = { + 'ai-constructs': [ + // FromJSONSchema is complex enough to trigger + // index.ts(113,9): error TS2589: Type instantiation is excessively deep and possibly infinite. + // index.ts(113,87): error TS2589: Type instantiation is excessively deep and possibly infinite. + // index.ts(113,87): error TS2590: Expression produces a union type that is too complex to represent. + // See https://github.com/ThomasAribart/json-schema-to-ts/blob/main/documentation/FAQs/i-get-a-type-instantiation-is-excessively-deep-and-potentially-infinite-error-what-should-i-do.md. + // Therefore, excluding this type from checks. + 'FromJSONSchema', + ], +}; + const validationResults = await Promise.allSettled( packagePaths.map(async (packagePath) => { const packageName = path.basename(packagePath); @@ -70,7 +82,8 @@ const validationResults = await Promise.allSettled( await new ApiChangesValidator( packagePath, baselinePackageApiReportPath, - workingDirectory + workingDirectory, + excludedTypesByPackageName[packageName] ).validate(); console.log(`Validation of ${packageName} completed successfully`); }) diff --git a/scripts/check_changeset_completeness.ts b/scripts/check_changeset_completeness.ts index 0dbd34d7f9..28cc61a3be 100644 --- a/scripts/check_changeset_completeness.ts +++ b/scripts/check_changeset_completeness.ts @@ -2,51 +2,136 @@ import getReleasePlan from '@changesets/get-release-plan'; import { GitClient } from './components/git_client.js'; import { readPackageJson } from './components/package-json/package_json.js'; import { EOL } from 'os'; +import { ReleasePlan, VersionType } from '@changesets/types'; -const gitClient = new GitClient(); - -const baseRef = process.argv[2]; -if (baseRef === undefined) { - throw new Error('No base ref specified for changeset completeness check'); +enum VersionTypeEnum { + 'NONE' = 0, + 'PATCH' = 1, + 'MINOR' = 2, + 'MAJOR' = 3, } -const releasePlan = await getReleasePlan(process.cwd()); +const checkForMissingChangesets = async ( + releasePlan: ReleasePlan, + gitClient: GitClient, + baseRef: string +) => { + const packagesWithChangeset = new Set( + releasePlan.releases.map((release) => release.name) + ); -const packagesWithChangeset = new Set( - releasePlan.releases.map((release) => release.name) -); + const changedFiles = await gitClient.getChangedFiles(baseRef); + const modifiedPackageDirs = new Set(); -const changedFiles = await gitClient.getChangedFiles(baseRef); + changedFiles + .filter( + (changedFile) => + changedFile.startsWith('packages/') && !changedFile.endsWith('test.ts') + ) + .forEach((changedPackageFile) => { + modifiedPackageDirs.add( + changedPackageFile.split('/').slice(0, 2).join('/') + ); + }); -const modifiedPackageDirs = new Set(); + const packagesMissingChangesets = []; + for (const modifiedPackageDir of modifiedPackageDirs) { + const { name: modifiedPackageName, private: isPrivate } = + await readPackageJson(modifiedPackageDir); + if (isPrivate) { + continue; + } + if (!packagesWithChangeset.has(modifiedPackageName)) { + packagesMissingChangesets.push(modifiedPackageName); + } + } -changedFiles - .filter( - (changedFile) => - changedFile.startsWith('packages/') && !changedFile.endsWith('test.ts') - ) - .forEach((changedPackageFile) => { - modifiedPackageDirs.add( - changedPackageFile.split('/').slice(0, 2).join('/') + if (packagesMissingChangesets.length > 0) { + throw new Error( + `The following packages have changes but are not included in any changeset:${EOL}${EOL}${packagesMissingChangesets.join( + EOL + )}${EOL}${EOL}Add a changeset using 'npx changeset add'.` ); - }); - -const packagesMissingChangesets = []; -for (const modifiedPackageDir of modifiedPackageDirs) { - const { name: modifiedPackageName, private: isPrivate } = - await readPackageJson(modifiedPackageDir); - if (isPrivate) { - continue; } - if (!packagesWithChangeset.has(modifiedPackageName)) { - packagesMissingChangesets.push(modifiedPackageName); +}; + +const convertVersionType = (version: VersionType): VersionTypeEnum => { + switch (version) { + case 'major': + return VersionTypeEnum.MAJOR; + case 'minor': + return VersionTypeEnum.MINOR; + case 'patch': + return VersionTypeEnum.PATCH; + case 'none': + return VersionTypeEnum.NONE; } -} +}; -if (packagesMissingChangesets.length > 0) { - throw new Error( - `The following packages have changes but are not included in any changeset:${EOL}${EOL}${packagesMissingChangesets.join( - EOL - )}${EOL}${EOL}Add a changeset using 'npx changeset add'.` +const findEffectiveVersion = ( + releasePlan: ReleasePlan, + packageName: string +): VersionTypeEnum => { + let effectiveVersion: VersionTypeEnum = VersionTypeEnum.NONE; + + for (const changeset of releasePlan.changesets) { + for (const release of changeset.releases) { + if (release.name === packageName) { + const releaseVersionType = convertVersionType(release.type); + if (releaseVersionType > effectiveVersion) { + effectiveVersion = releaseVersionType; + } + } + } + } + return effectiveVersion; +}; + +const checkBackendDependenciesVersion = (releasePlan: ReleasePlan) => { + const backendVersion: VersionTypeEnum = findEffectiveVersion( + releasePlan, + '@aws-amplify/backend' + ); + const backendAuthVersion: VersionTypeEnum = findEffectiveVersion( + releasePlan, + '@aws-amplify/backend-auth' + ); + const backendDataVersion: VersionTypeEnum = findEffectiveVersion( + releasePlan, + '@aws-amplify/backend-data' ); + const backendFunctionVersion: VersionTypeEnum = findEffectiveVersion( + releasePlan, + '@aws-amplify/backend-function' + ); + const backendStorageVersion: VersionTypeEnum = findEffectiveVersion( + releasePlan, + '@aws-amplify/backend-storage' + ); + + if ( + backendVersion < + Math.max( + backendAuthVersion, + backendDataVersion, + backendFunctionVersion, + backendStorageVersion + ) + ) { + throw new Error( + `@aws-amplify/backend has a version bump of a different kind from its dependencies (@aws-amplify/backend-auth, @aws-amplify/backend-data, @aws-amplify/backend-function, or @aws-amplify/backend-storage) but is expected to have a version bump of the same kind.${EOL}` + ); + } +}; + +const gitClient = new GitClient(); + +const baseRef = process.argv[2]; +if (baseRef === undefined) { + throw new Error('No base ref specified for changeset completeness check'); } + +const releasePlan = await getReleasePlan(process.cwd()); + +await checkForMissingChangesets(releasePlan, gitClient, baseRef); +checkBackendDependenciesVersion(releasePlan); diff --git a/scripts/check_package_versions.ts b/scripts/check_package_versions.ts index a35822819a..149cf3fd72 100644 --- a/scripts/check_package_versions.ts +++ b/scripts/check_package_versions.ts @@ -11,8 +11,6 @@ const packagePaths = await glob('./packages/*'); const getExpectedMajorVersion = (packageName: string) => { switch (packageName) { case 'ampx': - case '@aws-amplify/ai-constructs': - case '@aws-amplify/backend-ai': return '0.'; default: return '1.'; diff --git a/scripts/cleanup_e2e_resources.ts b/scripts/cleanup_e2e_resources.ts index 536b7b41d2..97463ce744 100644 --- a/scripts/cleanup_e2e_resources.ts +++ b/scripts/cleanup_e2e_resources.ts @@ -6,6 +6,13 @@ import { StackStatus, StackSummary, } from '@aws-sdk/client-cloudformation'; +import { + CloudWatchLogsClient, + DeleteLogGroupCommand, + DescribeLogGroupsCommand, + DescribeLogGroupsCommandOutput, + LogGroup, +} from '@aws-sdk/client-cloudwatch-logs'; import { Bucket, DeleteBucketCommand, @@ -70,6 +77,9 @@ const amplifyClient = new AmplifyClient({ const cfnClient = new CloudFormationClient({ maxAttempts: 5, }); +const cloudWatchClient = new CloudWatchLogsClient({ + maxAttempts: 5, +}); const cognitoClient = new CognitoIdentityProviderClient({ maxAttempts: 5, }); @@ -91,6 +101,7 @@ const TEST_CDK_RESOURCE_PREFIX = 'test-cdk'; /** * Stacks are considered stale after 2 hours. + * Log groups are considered stale after 7 days. For troubleshooting purposes. * Other resources are considered stale after 3 hours. * * Stack deletion triggers asynchronous resource deletion while this script is running. @@ -100,6 +111,7 @@ const TEST_CDK_RESOURCE_PREFIX = 'test-cdk'; */ const stackStaleDurationInMilliseconds = 2 * 60 * 60 * 1000; // 2 hours in milliseconds const staleDurationInMilliseconds = 3 * 60 * 60 * 1000; // 3 hours in milliseconds +const logGroupStaleDurationInMilliseconds = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds const isStackStale = ( stackSummary: StackSummary | undefined @@ -113,6 +125,17 @@ const isStackStale = ( ); }; +const isLogGroupStale = ( + logGroup: LogGroup | undefined +): boolean | undefined => { + if (!logGroup?.creationTime) { + return; + } + return ( + now.getTime() - logGroup.creationTime > logGroupStaleDurationInMilliseconds + ); +}; + const isStale = (creationDate: Date | undefined): boolean | undefined => { if (!creationDate) { return; @@ -546,3 +569,47 @@ for (const staleDynamoDBTable of allStaleDynamoDBTables) { ); } } + +const listAllStaleTestLogGroups = async (): Promise> => { + let nextToken: string | undefined = undefined; + const logGroups: Array = []; + do { + const listLogGroupsResponse: DescribeLogGroupsCommandOutput = + await cloudWatchClient.send( + new DescribeLogGroupsCommand({ + nextToken, + }) + ); + nextToken = listLogGroupsResponse.nextToken; + listLogGroupsResponse.logGroups + ?.filter( + (logGroup) => + (logGroup.logGroupName?.startsWith(TEST_AMPLIFY_RESOURCE_PREFIX) || + logGroup.logGroupName?.startsWith( + `/aws/lambda/${TEST_AMPLIFY_RESOURCE_PREFIX}` + )) && + isLogGroupStale(logGroup) + ) + .forEach((item) => { + logGroups.push(item); + }); + } while (nextToken); + return logGroups; +}; + +const allStaleLogGroups = await listAllStaleTestLogGroups(); +for (const logGroup of allStaleLogGroups) { + try { + await cloudWatchClient.send( + new DeleteLogGroupCommand({ + logGroupName: logGroup.logGroupName, + }) + ); + console.log(`Successfully deleted ${logGroup.logGroupName} log group`); + } catch (e) { + const errorMessage = e instanceof Error ? e.message : ''; + console.log( + `Failed to delete ${logGroup.logGroupName} log group. ${errorMessage}` + ); + } +} diff --git a/scripts/components/api-changes-validator/api_changes_validator.test.ts b/scripts/components/api-changes-validator/api_changes_validator.test.ts index 1e8e02f40a..f6aa9e19c3 100644 --- a/scripts/components/api-changes-validator/api_changes_validator.test.ts +++ b/scripts/components/api-changes-validator/api_changes_validator.test.ts @@ -61,6 +61,7 @@ void describe('Api changes validator', { concurrency: true }, () => { latestPackagePath, baselinePackageApiReportPath, workingDirectory, + [], 'npmLocalLink' ); @@ -92,6 +93,7 @@ void describe('Api changes validator', { concurrency: true }, () => { latestPackagePath, baselinePackageApiReportPath, workingDirectory, + ['SampleIgnoredType'], 'npmLocalLink' ); diff --git a/scripts/components/api-changes-validator/api_changes_validator.ts b/scripts/components/api-changes-validator/api_changes_validator.ts index e024aeec5a..9fc94d6031 100644 --- a/scripts/components/api-changes-validator/api_changes_validator.ts +++ b/scripts/components/api-changes-validator/api_changes_validator.ts @@ -30,6 +30,7 @@ export class ApiChangesValidator { private readonly latestPackagePath: string, private readonly baselinePackageApiReportPath: string, private readonly workingDirectory: string, + private readonly excludedTypes: Array = [], private readonly latestPackageDependencyDeclarationStrategy: | 'npmRegistry' | 'npmLocalLink' = 'npmRegistry' @@ -103,7 +104,8 @@ export class ApiChangesValidator { const apiReportAST = ApiReportParser.parse(apiReportContent); const usage = new ApiUsageGenerator( latestPackageJson.name, - apiReportAST + apiReportAST, + this.excludedTypes ).generate(); await fsp.writeFile(path.join(this.testProjectPath, 'index.ts'), usage); await execa('npm', ['install'], { cwd: this.testProjectPath }); diff --git a/scripts/components/api-changes-validator/api_usage_generator.test.ts b/scripts/components/api-changes-validator/api_usage_generator.test.ts index d48de411c7..de70ba1a81 100644 --- a/scripts/components/api-changes-validator/api_usage_generator.test.ts +++ b/scripts/components/api-changes-validator/api_usage_generator.test.ts @@ -337,6 +337,15 @@ const someTypeUnderSubNamespaceUsageFunction = (someTypeUnderSubNamespaceFunctio } `, }, + { + description: 'Skips ignored type', + apiReportCode: ` +export type SampleIgnoredType = { + someProperty: string; +} + `, + expectedApiUsage: '', + }, ]; const nestInMarkdownCodeBlock = (apiReportCode: string) => { @@ -351,9 +360,14 @@ void describe('Api usage generator', () => { ); const apiUsage = new ApiUsageGenerator( 'samplePackageName', - apiReportAST + apiReportAST, + ['SampleIgnoredType'] ).generate(); - assert.strictEqual(apiUsage.trim(), testCase.expectedApiUsage.trim()); + assert.strictEqual( + // .replace() removes EOL differences between Windows and other OS so output matches for all + apiUsage.replace(/[\r]/g, '').trim(), + testCase.expectedApiUsage.trim() + ); }); } }); diff --git a/scripts/components/api-changes-validator/api_usage_generator.ts b/scripts/components/api-changes-validator/api_usage_generator.ts index 3e69d7c7bf..ffb9b20783 100644 --- a/scripts/components/api-changes-validator/api_usage_generator.ts +++ b/scripts/components/api-changes-validator/api_usage_generator.ts @@ -25,7 +25,8 @@ export class ApiUsageGenerator { */ constructor( private readonly packageName: string, - private readonly apiReportAST: ts.SourceFile + private readonly apiReportAST: ts.SourceFile, + private readonly excludedTypes: Array ) { this.namespaceDefinitions = this.getNamespaceDefinitions(); } @@ -65,7 +66,8 @@ export class ApiUsageGenerator { case ts.SyntaxKind.TypeAliasDeclaration: return new TypeUsageStatementsGenerator( node as ts.TypeAliasDeclaration, - this.packageName + this.packageName, + this.excludedTypes ).generate(); case ts.SyntaxKind.EnumDeclaration: return new EnumUsageStatementsGenerator( diff --git a/scripts/components/api-changes-validator/api_usage_statements_generators.ts b/scripts/components/api-changes-validator/api_usage_statements_generators.ts index ad6088f098..d4e94526a9 100644 --- a/scripts/components/api-changes-validator/api_usage_statements_generators.ts +++ b/scripts/components/api-changes-validator/api_usage_statements_generators.ts @@ -122,10 +122,14 @@ export class TypeUsageStatementsGenerator implements UsageStatementsGenerator { */ constructor( private readonly typeAliasDeclaration: ts.TypeAliasDeclaration, - private readonly packageName: string + private readonly packageName: string, + private readonly excludedTypes: Array ) {} generate = (): UsageStatementsGeneratorOutput => { const typeName = this.typeAliasDeclaration.name.getText(); + if (this.excludedTypes.includes(typeName)) { + return {}; + } const constName = toLowerCamelCase(typeName); const genericTypeParametersDeclaration = new GenericTypeParameterDeclarationUsageStatementsGenerator( @@ -562,7 +566,18 @@ export class CallableUsageStatementsGenerator ).generate().usageStatement ?? ''; let returnValueAssignmentTarget = ''; if (this.functionType.type.kind !== ts.SyntaxKind.VoidKeyword) { - returnValueAssignmentTarget = `const returnValue: ${this.functionType.type.getText()} = `; + let returnType; + if (this.functionType.type.kind === ts.SyntaxKind.TypePredicate) { + // Example type predicate looks like this + // '(input: unknown) => input is SampleType;' + // It's a special syntax that tells compiler that it's safe to assume + // type after invoking the check. + // But when it comes to value assignment this is treated as boolean. + returnType = 'boolean'; + } else { + returnType = this.functionType.type.getText(); + } + returnValueAssignmentTarget = `const returnValue: ${returnType} = `; } const minParameterUsage = new CallableParameterUsageStatementsGenerator( diff --git a/scripts/components/api-changes-validator/test-resources/test-projects/without-breaks/project-with-namespace/API.md b/scripts/components/api-changes-validator/test-resources/test-projects/without-breaks/project-with-namespace/API.md index 6d8f581f00..f01fbae9ac 100644 --- a/scripts/components/api-changes-validator/test-resources/test-projects/without-breaks/project-with-namespace/API.md +++ b/scripts/components/api-changes-validator/test-resources/test-projects/without-breaks/project-with-namespace/API.md @@ -16,6 +16,14 @@ type SomeOtherTypeUnderSubNamespace = { someProperty: SomeTypeUnderSubNamespace; }; +class SomeClassUnderNamespace { + static readonly someStaticProperty: string; +} + +type SomeTypeUnderNamespaceWithGenerics = { + someGenericProperty: TPropertyType; +}; + declare namespace someSubNamespace { export { SomeTypeUnderSubNamespace, @@ -28,7 +36,9 @@ export const functionUsingTypes2: (props: SomeTypeUnderNamespace, extraArg: stri declare namespace someNamespace { export { + SomeClassUnderNamespace, SomeTypeUnderNamespace, + SomeTypeUnderNamespaceWithGenerics, someSubNamespace, functionUsingTypes1, functionUsingTypes2 diff --git a/scripts/components/api-changes-validator/test-resources/test-projects/without-breaks/project-with-namespace/src/some_namespace.ts b/scripts/components/api-changes-validator/test-resources/test-projects/without-breaks/project-with-namespace/src/some_namespace.ts index 24be0c1e89..0b65dbb60d 100644 --- a/scripts/components/api-changes-validator/test-resources/test-projects/without-breaks/project-with-namespace/src/some_namespace.ts +++ b/scripts/components/api-changes-validator/test-resources/test-projects/without-breaks/project-with-namespace/src/some_namespace.ts @@ -14,3 +14,11 @@ export const functionUsingTypes2 = ( ): Array => { throw new Error(); }; + +export class SomeClassUnderNamespace { + static readonly someStaticProperty: string; +} + +export type SomeTypeUnderNamespaceWithGenerics = { + someGenericProperty: TPropertyType; +}; diff --git a/scripts/components/api-changes-validator/test-resources/test-projects/without-breaks/project-without-breaks/API.md b/scripts/components/api-changes-validator/test-resources/test-projects/without-breaks/project-without-breaks/API.md index b84fb63b5a..aec702b4f8 100644 --- a/scripts/components/api-changes-validator/test-resources/test-projects/without-breaks/project-without-breaks/API.md +++ b/scripts/components/api-changes-validator/test-resources/test-projects/without-breaks/project-without-breaks/API.md @@ -79,4 +79,15 @@ export type SampleTypeUsingClass = { export type SampleTypeThatReferencesFunction = { sampleProperty: T; }; + +// This type is intentionally different from what's in sources +export type SampleIgnoredType = { + someProperty: string; +}; + +export const sampleTypePredicate: (input: unknown) => input is SampleType; + +export class SampleClassWithTypePredicate { + static sampleTypePredicate: (input: unknown) => input is SampleType; +} ``` diff --git a/scripts/components/api-changes-validator/test-resources/test-projects/without-breaks/project-without-breaks/src/index.ts b/scripts/components/api-changes-validator/test-resources/test-projects/without-breaks/project-without-breaks/src/index.ts index 4de03c28f0..1243ed5af2 100644 --- a/scripts/components/api-changes-validator/test-resources/test-projects/without-breaks/project-without-breaks/src/index.ts +++ b/scripts/components/api-changes-validator/test-resources/test-projects/without-breaks/project-without-breaks/src/index.ts @@ -110,3 +110,18 @@ export type SampleTypeUsingClass = { export type SampleTypeThatReferencesFunction = { sampleProperty: T; }; + +// This type is intentionally different from what's in api report +export type SampleIgnoredType = { + someProperty: number; +}; + +export const sampleTypePredicate = (input: unknown): input is SampleType => { + throw new Error(); +}; + +export class SampleClassWithTypePredicate { + static sampleTypePredicate = (input: unknown): input is SampleType => { + throw new Error(); + }; +} diff --git a/scripts/components/api-changes-validator/usage_statemets_renderer.ts b/scripts/components/api-changes-validator/usage_statemets_renderer.ts index a232c4ba6f..ba36d058be 100644 --- a/scripts/components/api-changes-validator/usage_statemets_renderer.ts +++ b/scripts/components/api-changes-validator/usage_statemets_renderer.ts @@ -69,9 +69,10 @@ export class UsageStatementsRenderer { // characters that can be found before or after symbol // this is to prevent partial matches in case one symbol's characters are subset of longer one - const symbolTerminators = '[\\s\\,\\(\\)<>;]'; + const possibleSymbolPrefix = '[\\s\\,\\(<;]'; + const possibleSymbolSuffix = '[\\s\\,\\(\\)<>;\\.]'; const regex = new RegExp( - `(${symbolTerminators})(${symbolName})(${symbolTerminators})`, + `(${possibleSymbolPrefix})(${symbolName})(${possibleSymbolSuffix})`, 'g' ); usageStatements = usageStatements.replaceAll( diff --git a/scripts/components/sparse_test_matrix_generator.test.ts b/scripts/components/sparse_test_matrix_generator.test.ts new file mode 100644 index 0000000000..73a0a5af60 --- /dev/null +++ b/scripts/components/sparse_test_matrix_generator.test.ts @@ -0,0 +1,61 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { SparseTestMatrixGenerator } from './sparse_test_matrix_generator.js'; +import { fileURLToPath } from 'url'; + +void describe('Sparse matrix generator', () => { + void it('generates sparse matrix', async () => { + const testDirectory = fileURLToPath( + new URL('./test-resources/sparse-generator-test-stubs', import.meta.url) + ); + const matrix = await new SparseTestMatrixGenerator({ + testGlobPattern: `${testDirectory}/*.test.ts`, + dimensions: { + dimension1: ['dim1val1', 'dim1val2', 'dim1,val3'], + dimension2: ['dim2val1', 'dim2val2'], + }, + maxTestsPerJob: 2, + }).generate(); + + assert.deepStrictEqual(matrix, { + include: [ + { + displayNames: 'test3.test.ts test2.test.ts', + dimension1: 'dim1val1', + dimension2: 'dim2val1', + testPaths: `${testDirectory}/test3.test.ts ${testDirectory}/test2.test.ts`, + }, + { + displayNames: 'test3.test.ts test2.test.ts', + dimension1: 'dim1val2', + dimension2: 'dim2val2', + testPaths: `${testDirectory}/test3.test.ts ${testDirectory}/test2.test.ts`, + }, + { + displayNames: 'test3.test.ts test2.test.ts', + dimension1: 'dim1,val3', + dimension2: 'dim2val1', + testPaths: `${testDirectory}/test3.test.ts ${testDirectory}/test2.test.ts`, + }, + { + displayNames: 'test1.test.ts', + dimension1: 'dim1val1', + dimension2: 'dim2val1', + testPaths: `${testDirectory}/test1.test.ts`, + }, + { + displayNames: 'test1.test.ts', + dimension1: 'dim1val2', + dimension2: 'dim2val2', + testPaths: `${testDirectory}/test1.test.ts`, + }, + { + displayNames: 'test1.test.ts', + dimension1: 'dim1,val3', + dimension2: 'dim2val1', + testPaths: `${testDirectory}/test1.test.ts`, + }, + ], + }); + }); +}); diff --git a/scripts/components/sparse_test_matrix_generator.ts b/scripts/components/sparse_test_matrix_generator.ts new file mode 100644 index 0000000000..4daf7ce0f0 --- /dev/null +++ b/scripts/components/sparse_test_matrix_generator.ts @@ -0,0 +1,93 @@ +import { glob } from 'glob'; +import path from 'path'; + +// See https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow +type JobMatrix = { + include?: Array>; +} & Record; + +export type SparseTestMatrixGeneratorProps = { + testGlobPattern: string; + maxTestsPerJob: number; + dimensions: Record>; +}; + +/** + * Generates a sparse test matrix. + * + * Sparse matrix is created is such a way that: + * 1. Every test is included + * 2. Every dimension's value is included + * 3. Algorithm avoids cartesian product of dimensions, just minimal subset that uses all values. + */ +export class SparseTestMatrixGenerator { + /** + * Creates sparse test matrix generator. + */ + constructor(private readonly props: SparseTestMatrixGeneratorProps) { + if (Object.keys(props.dimensions).length === 0) { + throw new Error('At least one dimension is required'); + } + } + + generate = async (): Promise => { + const testPaths = await glob(this.props.testGlobPattern); + + const matrix: JobMatrix = {}; + matrix.include = []; + + for (const testPathsBatch of this.chunkArray( + testPaths, + this.props.maxTestsPerJob + )) { + const dimensionsIndexes: Record = {}; + const dimensionCoverageComplete: Record = {}; + + Object.keys(this.props.dimensions).forEach((key) => { + dimensionsIndexes[key] = 0; + dimensionCoverageComplete[key] = false; + }); + + let allDimensionsComplete = false; + + do { + const matrixEntry: Record = {}; + matrixEntry.displayNames = testPathsBatch + .map((testPath) => path.basename(testPath)) + .join(' '); + Object.keys(this.props.dimensions).forEach((key) => { + matrixEntry[key] = this.props.dimensions[key][dimensionsIndexes[key]]; + }); + matrixEntry.testPaths = testPathsBatch.join(' '); + matrix.include?.push(matrixEntry); + + Object.keys(this.props.dimensions).forEach((key) => { + dimensionsIndexes[key]++; + if (dimensionsIndexes[key] === this.props.dimensions[key].length) { + // mark dimension as complete and start the cycle from start until all dimensions are used. + dimensionCoverageComplete[key] = true; + dimensionsIndexes[key] = 0; + } + }); + + // check if all dimensions are processed. + allDimensionsComplete = Object.keys(this.props.dimensions).reduce( + (acc, key) => { + return acc && dimensionCoverageComplete[key]; + }, + true + ); + } while (!allDimensionsComplete); + } + + return matrix; + }; + + private chunkArray = (array: Array, chunkSize: number) => { + const result: Array> = []; + for (let i = 0; i < array.length; i += chunkSize) { + result.push(array.slice(i, i + chunkSize)); + } + return result; + }; +} diff --git a/scripts/components/test-resources/sparse-generator-test-stubs/test1.test.ts b/scripts/components/test-resources/sparse-generator-test-stubs/test1.test.ts new file mode 100644 index 0000000000..af6cf15235 --- /dev/null +++ b/scripts/components/test-resources/sparse-generator-test-stubs/test1.test.ts @@ -0,0 +1 @@ +// Empty, content doesn't matter. diff --git a/scripts/components/test-resources/sparse-generator-test-stubs/test2.test.ts b/scripts/components/test-resources/sparse-generator-test-stubs/test2.test.ts new file mode 100644 index 0000000000..af6cf15235 --- /dev/null +++ b/scripts/components/test-resources/sparse-generator-test-stubs/test2.test.ts @@ -0,0 +1 @@ +// Empty, content doesn't matter. diff --git a/scripts/components/test-resources/sparse-generator-test-stubs/test3.test.ts b/scripts/components/test-resources/sparse-generator-test-stubs/test3.test.ts new file mode 100644 index 0000000000..af6cf15235 --- /dev/null +++ b/scripts/components/test-resources/sparse-generator-test-stubs/test3.test.ts @@ -0,0 +1 @@ +// Empty, content doesn't matter. diff --git a/scripts/generate_sparse_test_matrix.ts b/scripts/generate_sparse_test_matrix.ts new file mode 100644 index 0000000000..c5a76eb965 --- /dev/null +++ b/scripts/generate_sparse_test_matrix.ts @@ -0,0 +1,31 @@ +import { SparseTestMatrixGenerator } from './components/sparse_test_matrix_generator.js'; + +// This script generates a sparse test matrix. +// Every test must run on each type of OS and each version of node. +// However, we don't have to run every combination. + +if (process.argv.length < 3) { + console.log( + "Usage: npx tsx scripts/generate_sparse_test_matrix.ts '' " + ); +} + +const testGlobPattern = process.argv[2]; +const maxTestsPerJob = process.argv[3] ? parseInt(process.argv[3]) : 2; + +if (!Number.isInteger(maxTestsPerJob)) { + throw new Error( + 'Invalid max tests per job. If you are using glob pattern with starts in bash put it in quotes' + ); +} + +const matrix = await new SparseTestMatrixGenerator({ + testGlobPattern, + maxTestsPerJob, + dimensions: { + 'node-version': ['18', '20'], + os: ['ubuntu-latest', 'macos-14-xlarge', 'windows-latest'], + }, +}).generate(); + +console.log(JSON.stringify(matrix)); diff --git a/scripts/stop_npm_proxy.ts b/scripts/stop_npm_proxy.ts index 070126177e..dd8555313d 100644 --- a/scripts/stop_npm_proxy.ts +++ b/scripts/stop_npm_proxy.ts @@ -13,10 +13,20 @@ await execa('npm', ['config', 'set', 'registry', NPM_REGISTRY]); // returns the process id of the process listening on the specified port let pid: number; try { - const lsofResult = await execaCommand( - `lsof -n -t -iTCP:${VERDACCIO_PORT} -sTCP:LISTEN` - ); - pid = Number.parseInt(lsofResult.stdout.toString()); + if (process.platform === 'win32') { + const netStatResult = await execaCommand( + `netstat -n -a -o | grep LISTENING | grep ${VERDACCIO_PORT}`, + { shell: 'bash' } + ); + pid = Number.parseInt( + netStatResult.stdout.toString().split(/(\s)/).slice(-1)[0] + ); + } else { + const lsofResult = await execaCommand( + `lsof -n -t -iTCP:${VERDACCIO_PORT} -sTCP:LISTEN` + ); + pid = Number.parseInt(lsofResult.stdout.toString()); + } } catch (err) { console.warn( 'Could not determine npm proxy process id. Most likely the process has already been stopped.' diff --git a/templates/construct/package.json b/templates/construct/package.json index 2a879e3768..3494b52996 100644 --- a/templates/construct/package.json +++ b/templates/construct/package.json @@ -18,7 +18,7 @@ }, "license": "Apache-2.0", "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0" } }