diff --git a/.changeset/forty-buckets-visit.md b/.changeset/forty-buckets-visit.md deleted file mode 100644 index 4f19c5fee76..00000000000 --- 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 4b78eee2c57..00000000000 --- 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/silver-tables-do.md b/.changeset/silver-tables-do.md new file mode 100644 index 00000000000..8029abf5cf9 --- /dev/null +++ b/.changeset/silver-tables-do.md @@ -0,0 +1,10 @@ +--- +'@aws-amplify/backend-deployer': patch +'create-amplify': patch +'@aws-amplify/backend-cli': patch +'@aws-amplify/cli-core': patch +'@aws-amplify/platform-core': minor +'@aws-amplify/plugin-types': minor +--- + +Report cdk versions diff --git a/.changeset/stale-worms-pretend.md b/.changeset/stale-worms-pretend.md deleted file mode 100644 index a845151cc84..00000000000 --- 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 e7ee1c9d472..00000000000 --- 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/yellow-jokes-kick.md b/.changeset/yellow-jokes-kick.md deleted file mode 100644 index 83fd0a01b42..00000000000 --- 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 ff69a3796a7..bac7d033068 100644 --- a/.eslint_dictionary.json +++ b/.eslint_dictionary.json @@ -14,14 +14,17 @@ "argv", "arn", "arns", + "aws", "backends", "birthdate", "bundler", "callee", + "cartesian", "cdk", "changelog", "changeset", "changesets", + "checksum", "chown", "claude", "cloudformation", @@ -38,6 +41,8 @@ "datasync", "debounce", "declarator", + "decrypt", + "dependabot", "deployer", "deprecations", "deprecator", @@ -59,6 +64,7 @@ "formatter", "frontend", "frontends", + "frontmatter", "fullname", "func", "geofence", @@ -72,12 +78,16 @@ "homedir", "hotfix", "hotswap", + "hotswappable", "hotswapped", + "hotswapping", + "href", "iamv2", "identitypool", "idps", "implementors", "inheritdoc", + "instanceof", "interop", "invokable", "invoker", @@ -90,10 +100,12 @@ "lang", "linux", "localhost", + "lockfile", "lsof", "lstat", "macos", "matchers", + "mebibytes", "mfas", "minify", "mkdtemp", @@ -102,6 +114,7 @@ "mysql", "namespace", "namespaces", + "netstat", "nodejs", "nodenext", "nodir", @@ -131,6 +144,7 @@ "renderer", "repo", "resolvers", + "retryable", "saml", "scala", "schema", @@ -143,6 +157,7 @@ "sigint", "signout", "signup", + "SKey", "sms", "stderr", "stdin", @@ -154,8 +169,10 @@ "subpath", "syncable", "synthing", + "testapp", "testname", "testnamebucket", + "testuser", "timestamps", "tmpdir", "todos", @@ -168,6 +185,7 @@ "tslint", "typename", "typeof", + "ubuntu", "unauth", "unix", "unlink", @@ -186,6 +204,7 @@ "wildcards", "workspace", "writev", + "xlarge", "yaml", "yargs", "zoneinfo" diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 05f9f0a155c..23e09de5175 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/CODEOWNERS b/.github/CODEOWNERS index 066b0d8ef52..d8a32d72030 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,3 +8,15 @@ # GitHub actions/checks approval /.github/ @aws-amplify/amplify-backend-admins + +# Packages used by console team. +/packages/backend-secret @aws-amplify/amplify-backend @aws-amplify/amplify-studio-uibuilder +# API.md change always has related code change. Both teams are notified, but API approval by backend team is mandatory this way. +/packages/backend-secret/API.md @aws-amplify/amplify-backend-api-approvers +/packages/backend-secret/package.json @aws-amplify/amplify-backend-api-approvers @aws-amplify/amplify-studio-uibuilder +/packages/client-config @aws-amplify/amplify-backend @aws-amplify/amplify-studio-uibuilder +/packages/client-config/API.md @aws-amplify/amplify-backend-api-approvers +/packages/client-config/package.json @aws-amplify/amplify-backend-api-approvers @aws-amplify/amplify-studio-uibuilder +/packages/deployed-backend-client @aws-amplify/amplify-backend @aws-amplify/amplify-studio-uibuilder +/packages/deployed-backend-client/API.md @aws-amplify/amplify-backend-api-approvers +/packages/deployed-backend-client/package.json @aws-amplify/amplify-backend-api-approvers @aws-amplify/amplify-studio-uibuilder diff --git a/.github/actions/build_with_cache/action.yml b/.github/actions/build_with_cache/action.yml index cbe1e5a5da8..6e93905d1e1 100644 --- a/.github/actions/build_with_cache/action.yml +++ b/.github/actions/build_with_cache/action.yml @@ -2,17 +2,36 @@ name: build_with_cache description: builds the source code if cache miss and caches the result inputs: node-version: - default: 18 + required: true + cdk-version: + required: true runs: using: composite steps: + # Validate that non-blank inputs are provided. + # This is to ensure that inputs are plumbed and not defaulted accidentally in action call chains. + # The 'required' input property does not assert this if value is provided at runtime. + - name: Validate input + shell: bash + run: | + if [ -z "${{ inputs.cdk-version }}" ]; then + echo "CDK version must be provided" + exit 1; + fi + if [ -z "${{ inputs.node-version }}" ]; then + echo "Node version must be provided" + exit 1; + fi - uses: ./.github/actions/install_with_cache + with: + node-version: ${{ inputs.node-version }} + cdk-version: ${{ inputs.cdk-version }} # cache build output based on commit sha - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # version 4.0.2 id: build-cache with: path: '**/lib' - key: ${{ github.sha }}-node${{ inputs.node-version }} + key: ${{ github.sha }}-node${{ inputs.node-version }}-cdk${{ inputs.cdk-version }} enableCrossOsArchive: true # only build if cache miss - if: steps.build-cache.outputs.cache-hit != 'true' diff --git a/.github/actions/install_with_cache/action.yml b/.github/actions/install_with_cache/action.yml index e8eedf39c1a..cf2549fbb06 100644 --- a/.github/actions/install_with_cache/action.yml +++ b/.github/actions/install_with_cache/action.yml @@ -2,10 +2,26 @@ name: install_with_cache description: installs node_modules if cache miss and stores in the cache inputs: node-version: - default: 18 + required: true + cdk-version: + required: true runs: using: composite steps: + # Validate that non-blank inputs are provided. + # This is to ensure that inputs are plumbed and not defaulted accidentally in action call chains. + # The 'required' input property does not assert this if value is provided at runtime. + - name: Validate input + shell: bash + run: | + if [ -z "${{ inputs.cdk-version }}" ]; then + echo "CDK version must be provided" + exit 1; + fi + if [ -z "${{ inputs.node-version }}" ]; then + echo "Node version must be provided" + exit 1; + fi # cache node_modules based on package-lock.json hash - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # version 4.0.2 id: npm-cache @@ -13,8 +29,14 @@ runs: path: | node_modules packages/**/node_modules - key: ${{ runner.os }}-${{ hashFiles('package-lock.json') }}-node${{ inputs.node-version }} + key: ${{ runner.os }}-${{ hashFiles('package-lock.json') }}-node${{ inputs.node-version }}-cdk${{ inputs.cdk-version }} # only install if cache miss - if: steps.npm-cache.outputs.cache-hit != 'true' shell: bash - run: npm ci + run: | + npm ci + if [[ ${{ inputs.cdk-version }} != 'FROM_PACKAGE_LOCK' ]]; then + echo "Installing CDK version ${{ inputs.cdk-version }}" + npm install --no-save aws-cdk@${{ inputs.cdk-version }} aws-cdk-lib@${{ inputs.cdk-version }} + npx cdk --version + fi diff --git a/.github/actions/restore_build_cache/action.yml b/.github/actions/restore_build_cache/action.yml index 83adef08346..27785b4789e 100644 --- a/.github/actions/restore_build_cache/action.yml +++ b/.github/actions/restore_build_cache/action.yml @@ -3,16 +3,35 @@ description: composes restoring node_modules and restoring build artifacts inputs: node-version: description: node version used to configure environment with - default: 18 + required: true + cdk-version: + required: true runs: using: composite steps: + # Validate that non-blank inputs are provided. + # This is to ensure that inputs are plumbed and not defaulted accidentally in action call chains. + # The 'required' input property does not assert this if value is provided at runtime. + - name: Validate input + shell: bash + run: | + if [ -z "${{ inputs.cdk-version }}" ]; then + echo "CDK version must be provided" + exit 1; + fi + if [ -z "${{ inputs.node-version }}" ]; then + echo "Node version must be provided" + exit 1; + fi - uses: ./.github/actions/restore_install_cache + with: + node-version: ${{ inputs.node-version }} + cdk-version: ${{ inputs.cdk-version }} # restore build output from cache - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # version 4.0.2 id: build-cache with: path: '**/lib' - key: ${{ github.sha }}-node${{ inputs.node-version }} + key: ${{ github.sha }}-node${{ inputs.node-version }}-cdk${{ inputs.cdk-version }} fail-on-cache-miss: true enableCrossOsArchive: true diff --git a/.github/actions/restore_install_cache/action.yml b/.github/actions/restore_install_cache/action.yml index a6141cf65c1..e070d63c808 100644 --- a/.github/actions/restore_install_cache/action.yml +++ b/.github/actions/restore_install_cache/action.yml @@ -3,10 +3,26 @@ description: restores node_modules from the cache and fails if no cache entry fo inputs: node-version: description: node version used to configure environment with - default: 18 + required: true + cdk-version: + required: true runs: using: composite steps: + # Validate that non-blank inputs are provided. + # This is to ensure that inputs are plumbed and not defaulted accidentally in action call chains. + # The 'required' input property does not assert this if value is provided at runtime. + - name: Validate input + shell: bash + run: | + if [ -z "${{ inputs.cdk-version }}" ]; then + echo "CDK version must be provided" + exit 1; + fi + if [ -z "${{ inputs.node-version }}" ]; then + echo "Node version must be provided" + exit 1; + fi # restore node_modules from cache - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # version 4.0.2 id: npm-cache @@ -14,5 +30,5 @@ runs: path: | node_modules packages/**/node_modules - key: ${{ runner.os }}-${{ hashFiles('package-lock.json') }}-node${{ inputs.node-version }} + key: ${{ runner.os }}-${{ hashFiles('package-lock.json') }}-node${{ inputs.node-version }}-cdk${{ inputs.cdk-version }} fail-on-cache-miss: true diff --git a/.github/actions/run_with_e2e_account/action.yml b/.github/actions/run_with_e2e_account/action.yml index 5af2303bdc1..0a170e433ef 100644 --- a/.github/actions/run_with_e2e_account/action.yml +++ b/.github/actions/run_with_e2e_account/action.yml @@ -9,7 +9,7 @@ inputs: required: false node_version: description: node version used to configure environment with - required: false + required: true e2e_test_accounts: description: Serialized JSON array of strings with account numbers required: true @@ -22,6 +22,8 @@ inputs: fresh_build: description: Whether should build from scratch default: false + cdk-version: + required: true runs: using: composite steps: @@ -32,9 +34,15 @@ runs: - name: Restore Build Cache if: inputs.fresh_build != 'true' uses: ./.github/actions/restore_build_cache + with: + cdk-version: ${{ inputs.cdk-version }} + node-version: ${{ inputs.node_version }} - name: Build With Cache if: inputs.fresh_build == 'true' uses: ./.github/actions/build_with_cache + with: + cdk-version: ${{ inputs.cdk-version }} + node-version: ${{ inputs.node_version }} - name: Link CLI if: inputs.link_cli == 'true' shell: bash diff --git a/.github/actions/setup_baseline_version/action.yml b/.github/actions/setup_baseline_version/action.yml new file mode 100644 index 00000000000..7091cbd5190 --- /dev/null +++ b/.github/actions/setup_baseline_version/action.yml @@ -0,0 +1,58 @@ +name: setup_baseline_version +description: Set up a baseline or "previous" version of the library for testing. Mostly useful for backwards compatibility +inputs: + node_version: + description: node version used to configure environment with + required: true +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 + with: + node-version: ${{ inputs.node_version }} + - 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 6bda314a870..d8a794f9b4c 100644 --- a/.github/actions/setup_node/action.yml +++ b/.github/actions/setup_node/action.yml @@ -4,11 +4,28 @@ name: setup_node inputs: node-version: description: node version used to configure environment with - default: 18 + required: true runs: using: composite steps: + # Validate that non-blank inputs are provided. + # This is to ensure that inputs are plumbed and not defaulted accidentally in action call chains. + # The 'required' input property does not assert this if value is provided at runtime. + - name: Validate input + shell: bash + run: | + if [ -z "${{ inputs.node-version }}" ]; then + echo "Node version must be provided" + exit 1; + fi - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # version 4.0.2 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 4c86490af33..e7ab8be9f7a 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/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..1013809baa7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +# Dependabot version updates raises a maximum of five pull requests each time it checks dependencies. +# Note that there is some overlap with Dependabot security updates so some options can effect security updates as well, +# see https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file. + +version: 2 +updates: + # Maintain dependencies for npm ecosystem + - package-ecosystem: 'npm' + # Checks all directories from the current layer and below recursively for package.json files + directories: + - '**/*' + schedule: + # Runs every Monday + interval: 'weekly' + # Update package.json files if new version is outside of version range specified there. Otherwise lock file only. + versioning-strategy: increase-if-necessary diff --git a/.github/workflows/canary_checks.yml b/.github/workflows/canary_checks.yml index e534fe29c1f..af08fd3592b 100644 --- a/.github/workflows/canary_checks.yml +++ b/.github/workflows/canary_checks.yml @@ -11,6 +11,8 @@ jobs: steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - uses: ./.github/actions/setup_node + with: + node-version: 18 - name: Install and build without lock file shell: bash run: | @@ -46,7 +48,10 @@ jobs: uses: ./.github/actions/run_with_e2e_account with: e2e_test_accounts: ${{ vars.E2E_TEST_ACCOUNTS }} - node_version: ${{ matrix.node-version }} + node_version: 18 + # Use version from package lock. Tests projects are created outside of repository root + # and are using latest CDK version. + cdk-version: FROM_PACKAGE_LOCK aws_region: ${{ matrix.region }} fresh_build: true shell: bash diff --git a/.github/workflows/deprecate_release.yml b/.github/workflows/deprecate_release.yml index 83bc4e69c73..f618695cad9 100644 --- a/.github/workflows/deprecate_release.yml +++ b/.github/workflows/deprecate_release.yml @@ -28,7 +28,12 @@ jobs: steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - uses: ./.github/actions/setup_node + with: + node-version: 18 - uses: ./.github/actions/install_with_cache + with: + node-version: 18 + cdk-version: FROM_PACKAGE_LOCK deprecate_release: needs: - install @@ -47,6 +52,11 @@ jobs: # fetch full history so that we can properly lookup past releases fetch-depth: 0 - uses: ./.github/actions/setup_node + with: + node-version: 18 - uses: ./.github/actions/restore_install_cache + with: + node-version: 18 + cdk-version: FROM_PACKAGE_LOCK - name: Deprecate release versions run: npx tsx scripts/deprecate_release.ts diff --git a/.github/workflows/e2e_resource_cleanup.yml b/.github/workflows/e2e_resource_cleanup.yml index ff3e7554b7e..406ac3981e7 100644 --- a/.github/workflows/e2e_resource_cleanup.yml +++ b/.github/workflows/e2e_resource_cleanup.yml @@ -24,7 +24,12 @@ jobs: steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - uses: ./.github/actions/setup_node + with: + node-version: 18 - uses: ./.github/actions/install_with_cache + with: + node-version: 18 + cdk-version: FROM_PACKAGE_LOCK - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # version 4.0.2 with: diff --git a/.github/workflows/health_checks.yml b/.github/workflows/health_checks.yml index c950c12f453..9438226e94f 100644 --- a/.github/workflows/health_checks.yml +++ b/.github/workflows/health_checks.yml @@ -11,15 +11,124 @@ on: - hotfix - feature/** schedule: - # Every day at At minute 0 past hour 0, 6, 12, and 18 UTC. + # Every day at minute 0 past hour 0, 6, 12, and 18 UTC. # This is to make sure that there is at least one workflow run every 24 hours # taking into account that # 1) scheduled runs may not fire at exact prescribed time; # 2) transient failures may happen and auto recover; - cron: '0 0,6,12,18 * * *' workflow_dispatch: + inputs: + desired-cdk-version: + description: 'AWS CDK version (exact or tag). Defaults to package-locked version.' + required: false + type: string + include-package-manager-e2e-tests: + description: 'Include package manager e2e tests?' + required: false + type: boolean + default: true + include-create-amplify-e2e-tests: + description: 'Include create-amplify e2e tests?' + required: false + type: boolean + default: true + include-macos: + description: 'Include MacOS?' + required: false + type: boolean + default: true + include-windows: + description: 'Include Windows?' + required: false + type: boolean + default: true + node: + description: 'Node versions list (as JSON array).' + required: false + type: string + default: '["18", "20", "22"]' + workflow_call: + inputs: + desired-cdk-version: + description: 'AWS CDK version (exact or tag). Defaults to package-locked version.' + required: false + type: string + include-package-manager-e2e-tests: + description: 'Include package manager e2e tests?' + required: false + type: boolean + default: true + include-create-amplify-e2e-tests: + description: 'Include create-amplify e2e tests?' + required: false + type: boolean + default: true + include-macos: + description: 'Include MacOS?' + required: false + type: boolean + default: true + include-windows: + description: 'Include Windows?' + required: false + type: boolean + default: true + node: + description: 'Node versions list (as JSON array).' + required: false + type: string + default: '["18", "20", "22"]' + +env: + # Health checks can run on un-released code. Often work in progress. + # Disable data from there. + AMPLIFY_DISABLE_TELEMETRY: true jobs: + # This workflow may be called by variety of events. + # This steps resolves and applies appropriate defaults depending on the trigger. + resolve_inputs: + runs-on: ubuntu-latest + outputs: + cdk_version: ${{ steps.resolve_inputs.outputs.cdk_version }} + os: ${{ steps.resolve_inputs.outputs.os }} + os_for_e2e: ${{ steps.resolve_inputs.outputs.os_for_e2e }} + node: ${{ steps.resolve_inputs.outputs.node }} + steps: + - name: Resolve Inputs + id: resolve_inputs + # This is intentionally in pure bash to make this job independent of repo checkout and fast. + run: | + if [ -z "${{ inputs.desired-cdk-version }}" ]; then + echo "cdk_version=FROM_PACKAGE_LOCK" >> "$GITHUB_OUTPUT" + else + echo "cdk_version=$(npm view aws-cdk@${{ inputs.desired-cdk-version }} version)" >> "$GITHUB_OUTPUT" + fi + # Build JSON array in readable way in bash... + os='["ubuntu-latest"' + os_for_e2e='["ubuntu-latest"' + if [ "${{ inputs.include-macos }}" != "false" ]; then + os+=', "macos-14"' + os_for_e2e+=', "macos-14-xlarge"' + fi + if [ "${{ inputs.include-windows }}" != "false" ]; then + os+=', "windows-latest"' + os_for_e2e+=', "windows-latest"' + fi + os+=']' + os_for_e2e+=']' + echo "os=$os" >> "$GITHUB_OUTPUT" + echo "os_for_e2e=$os_for_e2e" >> "$GITHUB_OUTPUT" + if [ -z "${{ inputs.node }}" ]; then + echo 'node=["18", "20", "22"]' >> "$GITHUB_OUTPUT" + else + echo 'node=${{ inputs.node }}' >> "$GITHUB_OUTPUT" + fi + - run: echo cdk_version set to ${{ steps.resolve_inputs.outputs.cdk_version }} + - run: echo os set to ${{ steps.resolve_inputs.outputs.os }} + - run: echo os_for_e2e set to ${{ steps.resolve_inputs.outputs.os_for_e2e }} + - run: echo node set to ${{ steps.resolve_inputs.outputs.node }} install: strategy: matrix: @@ -27,9 +136,15 @@ jobs: # Larger workers use different drive (C: instead of D:) to check out project and NPM installation # creates file system links that include drive letter. # Changing between standard and custom workers requires full install cache invalidation - os: [ubuntu-latest, macos-14, windows-latest] - node: [18, 20] + os: ${{ fromJSON(needs.resolve_inputs.outputs.os) }} + node: ${{ fromJSON(needs.resolve_inputs.outputs.node) }} + # Always include Node 18 and Ubuntu. Non-testing jobs depend on it. + include: + - os: ubuntu-latest + node: 18 runs-on: ${{ matrix.os }} + needs: + - resolve_inputs timeout-minutes: 10 steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 @@ -39,13 +154,18 @@ jobs: - uses: ./.github/actions/install_with_cache with: node-version: ${{ matrix.node }} + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_version }} build: strategy: matrix: - node: [18, 20] + node: ${{ fromJSON(needs.resolve_inputs.outputs.node) }} + # Always include Node 18. Non-testing jobs depend on it. + include: + - node: 18 runs-on: ubuntu-latest needs: - install + - resolve_inputs steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - uses: ./.github/actions/setup_node @@ -54,13 +174,15 @@ jobs: - uses: ./.github/actions/build_with_cache with: node-version: ${{ matrix.node }} + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_version }} test_with_coverage: needs: - build + - resolve_inputs strategy: matrix: - os: [ubuntu-latest, macos-14, windows-latest] - node: [18, 20] + os: ${{ fromJSON(needs.resolve_inputs.outputs.os) }} + node: ${{ fromJSON(needs.resolve_inputs.outputs.node) }} runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 @@ -70,27 +192,40 @@ jobs: - uses: ./.github/actions/restore_build_cache with: node-version: ${{ matrix.node }} + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_version }} - run: npm run set-script-shell - run: npm run test:coverage:threshold test_scripts: needs: - build + - resolve_inputs runs-on: ubuntu-latest steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - uses: ./.github/actions/setup_node + with: + node-version: 18 - uses: ./.github/actions/restore_build_cache + with: + node-version: 18 + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_version }} - run: | npm run set-script-shell npm run test:scripts test_with_baseline_dependencies: needs: - install + - resolve_inputs runs-on: ubuntu-latest steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - uses: ./.github/actions/setup_node + with: + node-version: 18 - uses: ./.github/actions/restore_install_cache + with: + node-version: 18 + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_version }} - name: Pin some dependencies to nearest patch and rebuild run: | npx tsx scripts/set_baseline_dependency_versions.ts @@ -106,13 +241,19 @@ jobs: if: github.event_name == 'pull_request' needs: - build + - resolve_inputs runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout pull request ref uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - uses: ./.github/actions/setup_node + with: + node-version: 18 - uses: ./.github/actions/restore_build_cache + with: + node-version: 18 + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_version }} - name: Publish packages locally timeout-minutes: 2 run: | @@ -128,9 +269,37 @@ jobs: run: | mkdir api-validation-projects npx tsx scripts/check_api_changes.ts base-branch-content api-validation-projects + handle_dependabot_version_update: + if: github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' + runs-on: ubuntu-latest + needs: + - install + - resolve_inputs + permissions: + # This is required so that this job can add the 'run-e2e' label and push to the pull request + pull-requests: write + contents: write + steps: + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 + with: + fetch-depth: 0 + - uses: ./.github/actions/setup_node + with: + node-version: 18 + - uses: ./.github/actions/restore_install_cache + with: + node-version: 18 + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_version }} + - name: Handle Dependabot version update pull request + run: npx tsx scripts/dependabot_handle_version_update.ts "$BASE_SHA" + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + # The dependabot_handler_version_update script needs to add the 'run-e2e' pull request label + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} do_include_e2e: needs: - install + - resolve_inputs runs-on: ubuntu-latest permissions: # This is required so that the step can read the labels on the pull request @@ -140,14 +309,39 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} outputs: run_e2e: ${{ steps.check.outputs.run_e2e }} + include_package_manager_e2e: ${{ steps.check_package_manager.outputs.include_package_manager_e2e }} + include_create_amplify_e2e: ${{ steps.check_create_amplify.outputs.include_create_amplify_e2e }} steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - uses: ./.github/actions/setup_node + with: + node-version: 18 - uses: ./.github/actions/restore_install_cache - - name: Check if E2E tests should run + with: + node-version: 18 + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_version }} + - name: Check if any E2E tests should run id: check run: echo "run_e2e=$(npx tsx scripts/do_include_e2e.ts)" >> "$GITHUB_OUTPUT" - run: echo run_e2e set to ${{ steps.check.outputs.run_e2e }} + - name: Check if Package Manager E2E tests should be included + id: check_package_manager + run: | + if [ -z "${{ inputs.include-package-manager-e2e-tests }}" ]; then + echo "include_package_manager_e2e=true" >> "$GITHUB_OUTPUT" + else + echo "include_package_manager_e2e=${{ inputs.include-package-manager-e2e-tests }}" >> "$GITHUB_OUTPUT" + fi + - run: echo include_package_manager_e2e set to ${{ steps.check_package_manager.outputs.include_package_manager_e2e }} + - name: Check if Create Amplify E2E tests should be included + id: check_create_amplify + run: | + if [ -z "${{ inputs.include-create-amplify-e2e-tests }}" ]; then + echo "include_create_amplify_e2e=true" >> "$GITHUB_OUTPUT" + else + echo "include_create_amplify_e2e=${{ inputs.include-create-amplify-e2e-tests }}" >> "$GITHUB_OUTPUT" + fi + - run: echo include_create_amplify_e2e set to ${{ steps.check_create_amplify.outputs.include_create_amplify_e2e }} e2e_iam_access_drift: if: needs.do_include_e2e.outputs.run_e2e == 'true' runs-on: ubuntu-latest @@ -155,80 +349,98 @@ jobs: needs: - do_include_e2e - build + - resolve_inputs permissions: # these permissions are required for the configure-aws-credentials action to get a JWT from GitHub 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 + - name: Setup baseline version + uses: ./.github/actions/setup_baseline_version + id: setup_baseline_version 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"; + node_version: 18 - name: Checkout current version uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - name: Run e2e iam access drift test uses: ./.github/actions/run_with_e2e_account with: e2e_test_accounts: ${{ vars.E2E_TEST_ACCOUNTS }} - node_version: ${{ matrix.node-version }} + node_version: 18 + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_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 + - resolve_inputs + 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 + with: + node_version: 18 + - 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: 18 + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_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 + - resolve_inputs + steps: + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 + - uses: ./.github/actions/restore_build_cache + with: + node-version: 18 + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_version }} + - run: echo "$(npx tsx scripts/generate_sparse_test_matrix.ts 'packages/integration-tests/lib/test-e2e/deployment/*.deployment.test.js' '${{ needs.resolve_inputs.outputs.node }}' '${{ needs.resolve_inputs.outputs.os_for_e2e }}')" + - id: generateMatrix + run: echo "matrix=$(npx tsx scripts/generate_sparse_test_matrix.ts 'packages/integration-tests/lib/test-e2e/deployment/*.deployment.test.js' '${{ needs.resolve_inputs.outputs.node }}' '${{ needs.resolve_inputs.outputs.os_for_e2e }}')" >> "$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 + - resolve_inputs permissions: # these permissions are required for the configure-aws-credentials action to get a JWT from GitHub id-token: write @@ -240,28 +452,43 @@ jobs: with: e2e_test_accounts: ${{ vars.E2E_TEST_ACCOUNTS }} node_version: ${{ matrix.node-version }} + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_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 + - resolve_inputs + steps: + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 + - uses: ./.github/actions/restore_build_cache + with: + node-version: 18 + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_version }} + - run: echo "$(npx tsx scripts/generate_sparse_test_matrix.ts 'packages/integration-tests/lib/test-e2e/sandbox/*.sandbox.test.js' '${{ needs.resolve_inputs.outputs.node }}' '${{ needs.resolve_inputs.outputs.os_for_e2e }}')" + - id: generateMatrix + run: echo "matrix=$(npx tsx scripts/generate_sparse_test_matrix.ts 'packages/integration-tests/lib/test-e2e/sandbox/*.sandbox.test.js' '${{ needs.resolve_inputs.outputs.node }}' '${{ needs.resolve_inputs.outputs.os_for_e2e }}')" >> "$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 + - resolve_inputs permissions: # these permissions are required for the configure-aws-credentials action to get a JWT from GitHub id-token: write @@ -273,8 +500,9 @@ jobs: with: e2e_test_accounts: ${{ vars.E2E_TEST_ACCOUNTS }} node_version: ${{ matrix.node-version }} + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_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 @@ -282,6 +510,7 @@ jobs: needs: - do_include_e2e - build + - resolve_inputs permissions: # these permissions are required for the configure-aws-credentials action to get a JWT from GitHub id-token: write @@ -292,46 +521,55 @@ jobs: uses: ./.github/actions/run_with_e2e_account with: e2e_test_accounts: ${{ vars.E2E_TEST_ACCOUNTS }} - node_version: ${{ matrix.node-version }} + node_version: 18 + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_version }} link_cli: true run: npm run test:dir packages/integration-tests/lib/test-e2e/backend_output.test.js e2e_create_amplify: - if: needs.do_include_e2e.outputs.run_e2e == 'true' + if: needs.do_include_e2e.outputs.run_e2e == 'true' && needs.do_include_e2e.outputs.include_create_amplify_e2e == 'true' strategy: # will finish running other test matrices even if one fails fail-fast: false matrix: - os: [ubuntu-latest, macos-14, windows-latest] - node-version: [18, 20] + os: ${{ fromJSON(needs.resolve_inputs.outputs.os) }} + node-version: ${{ fromJSON(needs.resolve_inputs.outputs.node) }} # skip multiple node version test on other os exclude: - os: macos-14 node-version: 20 - os: windows-latest node-version: 20 + - os: macos-14 + node-version: 22 + - os: windows-latest + node-version: 22 runs-on: ${{ matrix.os }} timeout-minutes: ${{ matrix.os == 'windows-latest' && 35 || 25 }} needs: - do_include_e2e - build + - resolve_inputs steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - uses: ./.github/actions/setup_node with: node-version: ${{ matrix.node-version }} - uses: ./.github/actions/restore_build_cache + with: + node-version: ${{ matrix.node-version }} + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_version }} - run: cd packages/cli && npm link - name: Run e2e create-amplify tests run: npm run test:dir packages/integration-tests/lib/test-e2e/create_amplify.test.js e2e_package_manager: - if: needs.do_include_e2e.outputs.run_e2e == 'true' + if: needs.do_include_e2e.outputs.run_e2e == 'true' && needs.do_include_e2e.outputs.include_package_manager_e2e == 'true' strategy: # will finish running other test matrices even if one fails fail-fast: false matrix: - os: [ubuntu-latest, macos-14, windows-latest] + os: ${{ fromJSON(needs.resolve_inputs.outputs.os) }} pkg-manager: [npm, yarn-classic, yarn-modern, pnpm] - node-version: ['20'] + node-version: ['22'] env: PACKAGE_MANAGER: ${{ matrix.pkg-manager }} runs-on: ${{ matrix.os }} @@ -339,6 +577,7 @@ jobs: needs: - build - do_include_e2e + - resolve_inputs permissions: # these permissions are required for the configure-aws-credentials action to get a JWT from GitHub id-token: write @@ -351,6 +590,7 @@ jobs: with: e2e_test_accounts: ${{ vars.E2E_TEST_ACCOUNTS }} node_version: ${{ matrix.node-version }} + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_version }} shell: bash run: | PACKAGE_MANAGER=${{matrix.pkg-manager}} npm run test:dir packages/integration-tests/src/package_manager_sanity_checks.test.ts @@ -358,46 +598,76 @@ jobs: runs-on: ubuntu-latest needs: - build + - resolve_inputs steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - uses: ./.github/actions/setup_node + with: + node-version: 18 - uses: ./.github/actions/restore_build_cache + with: + node-version: 18 + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_version }} - run: npm run lint check_dependencies: runs-on: ubuntu-latest needs: - install + - resolve_inputs steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - uses: ./.github/actions/setup_node + with: + node-version: 18 - uses: ./.github/actions/restore_install_cache + with: + node-version: 18 + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_version }} - run: npm run check:dependencies check_tsconfig_refs: runs-on: ubuntu-latest needs: - install + - resolve_inputs steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - uses: ./.github/actions/setup_node + with: + node-version: 18 - uses: ./.github/actions/restore_install_cache + with: + node-version: 18 + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_version }} - run: npm run check:tsconfig-refs check_api_extract: runs-on: ubuntu-latest needs: - build + - resolve_inputs steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - uses: ./.github/actions/setup_node + with: + node-version: 18 - uses: ./.github/actions/restore_build_cache + with: + node-version: 18 + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_version }} - run: npm run check:api docs_build_and_publish: runs-on: ubuntu-latest needs: - build + - resolve_inputs steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - uses: ./.github/actions/setup_node + with: + node-version: 18 - uses: ./.github/actions/restore_build_cache + with: + node-version: 18 + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_version }} - run: npm run docs - if: ${{ github.event_name == 'push' && github.ref_name == 'main' }} uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # version 4.0.0 @@ -410,28 +680,46 @@ jobs: runs-on: ubuntu-latest needs: - install + - resolve_inputs steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - uses: ./.github/actions/setup_node + with: + node-version: 18 - uses: ./.github/actions/restore_install_cache + with: + node-version: 18 + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_version }} - 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 needs: - install + - resolve_inputs steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 with: # fetch full history so that changeset can properly compute divergence point fetch-depth: 0 - uses: ./.github/actions/setup_node + with: + node-version: 18 - uses: ./.github/actions/restore_install_cache + with: + node-version: 18 + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_version }} - 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,12 +728,19 @@ jobs: check_package_versions: if: github.event_name == 'pull_request' runs-on: ubuntu-latest + timeout-minutes: 10 needs: - install + - resolve_inputs steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - uses: ./.github/actions/setup_node + with: + node-version: 18 - uses: ./.github/actions/restore_install_cache + with: + node-version: 18 + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_version }} - run: npx changeset version - run: npm run check:package-versions @@ -453,11 +748,17 @@ jobs: if: ${{ github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'hotfix') }} needs: - install + - resolve_inputs runs-on: ubuntu-latest steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - uses: ./.github/actions/setup_node + with: + node-version: 18 - uses: ./.github/actions/restore_install_cache + with: + node-version: 18 + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_version }} - id: is_version_packages_commit run: echo "is_version_packages_commit=$(npx tsx scripts/is_version_packages_commit.ts)" >> "$GITHUB_OUTPUT" - name: Create or update Version Packages PR @@ -480,11 +781,17 @@ jobs: - e2e_deployment - e2e_sandbox - e2e_create_amplify + - resolve_inputs runs-on: ubuntu-latest steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - uses: ./.github/actions/setup_node + with: + node-version: 18 - uses: ./.github/actions/restore_build_cache + with: + node-version: 18 + cdk-version: ${{ needs.resolve_inputs.outputs.cdk_version }} - id: is_version_packages_commit run: echo "is_version_packages_commit=$(npx tsx scripts/is_version_packages_commit.ts)" >> "$GITHUB_OUTPUT" - name: Publish packages diff --git a/.github/workflows/restore_release.yml b/.github/workflows/restore_release.yml index 403c9d84a61..219b33692df 100644 --- a/.github/workflows/restore_release.yml +++ b/.github/workflows/restore_release.yml @@ -24,7 +24,12 @@ jobs: steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - uses: ./.github/actions/setup_node + with: + node-version: 18 - uses: ./.github/actions/install_with_cache + with: + node-version: 18 + cdk-version: FROM_PACKAGE_LOCK restore_release: needs: - install @@ -42,6 +47,11 @@ jobs: # fetch full history so that we can properly lookup past releases fetch-depth: 0 - uses: ./.github/actions/setup_node + with: + node-version: 18 - uses: ./.github/actions/restore_install_cache + with: + node-version: 18 + cdk-version: FROM_PACKAGE_LOCK - name: Restore release versions run: npx tsx scripts/restore_release.ts diff --git a/.github/workflows/snapshot_release.yml b/.github/workflows/snapshot_release.yml index 048c06de062..4f5b5a22e8a 100644 --- a/.github/workflows/snapshot_release.yml +++ b/.github/workflows/snapshot_release.yml @@ -9,7 +9,12 @@ jobs: steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - uses: ./.github/actions/setup_node + with: + node-version: 18 - uses: ./.github/actions/install_with_cache + with: + node-version: 18 + cdk-version: FROM_PACKAGE_LOCK build: runs-on: ubuntu-latest needs: @@ -17,7 +22,12 @@ jobs: steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - uses: ./.github/actions/setup_node + with: + node-version: 18 - uses: ./.github/actions/build_with_cache + with: + node-version: 18 + cdk-version: FROM_PACKAGE_LOCK test: needs: - build @@ -25,7 +35,12 @@ jobs: steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - uses: ./.github/actions/setup_node + with: + node-version: 18 - uses: ./.github/actions/restore_build_cache + with: + node-version: 18 + cdk-version: FROM_PACKAGE_LOCK - run: npm run set-script-shell - run: npm run test publish_snapshot: @@ -35,7 +50,12 @@ jobs: steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # version 4.1.4 - uses: ./.github/actions/setup_node + with: + node-version: 18 - uses: ./.github/actions/restore_build_cache + with: + node-version: 18 + cdk-version: FROM_PACKAGE_LOCK - name: Authenticate run: | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc diff --git a/.github/workflows/validate_cdk_release.yml b/.github/workflows/validate_cdk_release.yml new file mode 100644 index 00000000000..d54707ef0b2 --- /dev/null +++ b/.github/workflows/validate_cdk_release.yml @@ -0,0 +1,30 @@ +name: validate_cdk_release + +on: + schedule: + # Every day at 16:00 UTC (8:00 PST) and 18:00 UTC (10:00 PST) + # So that it produces at least 2 data points daily. + - cron: '0 16,18 * * *' + workflow_dispatch: + inputs: + desired-cdk-version: + description: 'AWS CDK version (exact or tag). Defaults to latest version.' + required: false + type: string + +jobs: + health_checks_with_cdk_version: + uses: ./.github/workflows/health_checks.yml + secrets: inherit + with: + # This runs all deployment and sandbox tests. + desired-cdk-version: ${{ inputs.desired-cdk-version || 'latest' }} + # Exclude additional runtimes. + # They don't bring much value, but may bring instability and extra latency. + include-macos: false + include-windows: false + node: '["18"]' + # Exclude package manager and create-amplify tests. + # They don't bring functional coverage for CDK usage patterns. + include-package-manager-e2e-tests: false + include-create-amplify-e2e-tests: false diff --git a/.prettierignore b/.prettierignore index c4daa53d7c2..d7e58b0dd3d 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 424876cb2fb..b7cffcc92a7 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 @@ -144,6 +146,13 @@ At a minimum, each package needs: 6. An `.npmignore` file 7. A `README.md` file that gives a brief description of the intent of the package +## Debugging + +For debugging purposes you can use the following knobs: + +1. Most of `npx ampx` commands take `--debug` parameter. It enables verbose console output. +2. We are using `execa` for spawning child processes. You can set `export NODE_DEBUG=execa` to reveal exact command lines. + ## Licensing See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. diff --git a/README.md b/README.md index f02f9af1778..782788f8362 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ + +Amplify logo + + # AWS Amplify Gen 2 Backend This next generation of Amplify’s backend building experience lets you author your frontend and backend definition completely with TypeScript, a file convention, and Git branch-based environments. To learn more, visit [AWS Amplify Gen 2](https://docs.amplify.aws). diff --git a/package-lock.json b/package-lock.json index 73015bf27cb..3f7db738980 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,21 +756,19 @@ } }, "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.14.5", + "resolved": "https://registry.npmjs.org/@aws-amplify/data-construct/-/data-construct-1.14.5.tgz", + "integrity": "sha512-QKwnf/6Cksts8tpDiHw2gl9aapN3ii4/4Cl2a2RELoTrWfXvaih9WDJmFSsgM2a2VdhJ1bjS6GGzb8GrN1vdSg==", "bundleDependencies": [ + "@aws-amplify/ai-constructs", "@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", "@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", @@ -723,398 +777,559 @@ "@aws-amplify/graphql-relational-transformer", "@aws-amplify/graphql-searchable-transformer", "@aws-amplify/graphql-sql-transformer", - "@aws-amplify/graphql-generation-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", - "fs-extra", - "graphql", - "graphql-transformer-common", - "hjson", - "lodash", - "md5", - "object-hash", - "ts-dedent", + "@aws-crypto/crc32", + "@aws-crypto/sha256-browser", + "@aws-crypto/sha256-js", + "@aws-crypto/supports-web-crypto", + "@aws-crypto/util", + "@aws-sdk/client-bedrock-runtime", + "@aws-sdk/client-sso", + "@aws-sdk/client-sso-oidc", + "@aws-sdk/client-sts", + "@aws-sdk/core", + "@aws-sdk/credential-provider-env", + "@aws-sdk/credential-provider-http", + "@aws-sdk/credential-provider-ini", + "@aws-sdk/credential-provider-node", + "@aws-sdk/credential-provider-process", + "@aws-sdk/credential-provider-sso", + "@aws-sdk/credential-provider-web-identity", + "@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/token-providers", + "@aws-sdk/types", + "@aws-sdk/util-endpoints", + "@aws-sdk/util-locate-window", + "@aws-sdk/util-user-agent-browser", + "@aws-sdk/util-user-agent-node", + "@smithy/abort-controller", + "@smithy/config-resolver", + "@smithy/core", + "@smithy/credential-provider-imds", + "@smithy/eventstream-codec", + "@smithy/eventstream-serde-browser", + "@smithy/eventstream-serde-config-resolver", + "@smithy/eventstream-serde-node", + "@smithy/eventstream-serde-universal", + "@smithy/fetch-http-handler", + "@smithy/hash-node", + "@smithy/invalid-dependency", + "@smithy/is-array-buffer", + "@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/property-provider", + "@smithy/protocol-http", + "@smithy/querystring-builder", + "@smithy/querystring-parser", + "@smithy/service-error-classification", + "@smithy/shared-ini-file-loader", + "@smithy/signature-v4", + "@smithy/smithy-client", + "@smithy/types", + "@smithy/url-parser", + "@smithy/util-base64", + "@smithy/util-body-length-browser", + "@smithy/util-body-length-node", + "@smithy/util-buffer-from", + "@smithy/util-config-provider", + "@smithy/util-defaults-mode-browser", + "@smithy/util-defaults-mode-node", + "@smithy/util-endpoints", + "@smithy/util-hex-encoding", + "@smithy/util-middleware", + "@smithy/util-retry", + "@smithy/util-stream", + "@smithy/util-uri-escape", + "@smithy/util-utf8", + "bowser", "charenc", + "ci-info", "crypt", + "fast-xml-parser", + "fs-extra", "graceful-fs", + "graphql", "graphql-mapping-template", + "graphql-transformer-common", + "hjson", "immer", "is-buffer", + "is-ci", "jsonfile", "libphonenumber-js", + "lodash", + "lodash.mergewith", + "md5", + "object-hash", "pluralize", - "universalify" + "semver", + "strnum", + "ts-dedent", + "tslib", + "universalify", + "uuid", + "zod" ], "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/graphql-transformer-interfaces": "4.1.0", - "@aws-amplify/platform-core": "^0.2.0", - "@aws-amplify/plugin-types": "^0.4.1", + "@aws-amplify/ai-constructs": "^1.0.0", + "@aws-amplify/backend-output-schemas": "^1.0.0", + "@aws-amplify/backend-output-storage": "^1.0.0", + "@aws-amplify/graphql-api-construct": "1.18.5", + "@aws-amplify/graphql-auth-transformer": "4.1.9", + "@aws-amplify/graphql-conversation-transformer": "1.1.4", + "@aws-amplify/graphql-default-value-transformer": "3.1.6", + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-function-transformer": "3.1.8", + "@aws-amplify/graphql-generation-transformer": "1.1.2", + "@aws-amplify/graphql-http-transformer": "3.0.11", + "@aws-amplify/graphql-index-transformer": "3.0.11", + "@aws-amplify/graphql-maps-to-transformer": "4.0.11", + "@aws-amplify/graphql-model-transformer": "3.1.3", + "@aws-amplify/graphql-predictions-transformer": "3.0.11", + "@aws-amplify/graphql-relational-transformer": "3.1.3", + "@aws-amplify/graphql-searchable-transformer": "3.0.11", + "@aws-amplify/graphql-sql-transformer": "0.4.11", + "@aws-amplify/graphql-transformer": "2.2.4", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.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", + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2", "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", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.3.0" } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/ai-constructs": { - "version": "0.1.2", + "version": "1.0.0", "inBundle": true, "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" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", - "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", - "inBundle": true, - "license": "Apache-2.0", - "peerDependencies": { - "@aws-sdk/types": "^3.609.0", - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0" } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/backend-output-schemas": { - "version": "0.4.0", + "version": "1.4.0", "inBundle": true, "license": "Apache-2.0", "peerDependencies": { - "zod": "^3.21.4" + "zod": "^3.22.2" } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/backend-output-storage": { - "version": "0.2.2", + "version": "1.1.3", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/backend-output-schemas": "^0.4.0", - "@aws-amplify/platform-core": "^0.2.0" + "@aws-amplify/backend-output-schemas": "^1.2.0", + "@aws-amplify/platform-core": "^1.0.6", + "@aws-amplify/plugin-types": "^1.3.1" }, "peerDependencies": { - "aws-cdk-lib": "^2.103.0" + "aws-cdk-lib": "^2.158.0" } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-auth-transformer": { - "version": "4.1.0", + "version": "4.1.9", "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", + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-model-transformer": "3.1.3", + "@aws-amplify/graphql-relational-transformer": "3.1.3", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1", + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2", "lodash": "^4.17.21", "md5": "^2.3.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.3.0" } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-conversation-transformer": { - "version": "0.2.0", + "version": "1.1.4", "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", + "@aws-amplify/ai-constructs": "^1.0.0", + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-index-transformer": "3.0.11", + "@aws-amplify/graphql-model-transformer": "3.1.3", + "@aws-amplify/graphql-relational-transformer": "3.1.3", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", + "@aws-amplify/plugin-types": "^1.0.0", "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1", - "immer": "^9.0.12" + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2", + "immer": "^9.0.12", + "semver": "^7.6.3" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.3.0" } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-default-value-transformer": { - "version": "3.0.2", + "version": "3.1.6", "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", + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1", + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2", "libphonenumber-js": "1.9.47" } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-directives": { - "version": "2.1.0", + "version": "2.6.1", "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.8", "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", + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1" + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.3.0" } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-generation-transformer": { - "version": "0.2.0", + "version": "1.1.2", "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", + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1", + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2", "immer": "^9.0.12" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.3.0" } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-http-transformer": { - "version": "3.0.2", + "version": "3.0.11", "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", + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1" + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.3.0" } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-index-transformer": { - "version": "3.0.2", + "version": "3.0.11", "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", + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-model-transformer": "3.1.3", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1" + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.3.0" } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-maps-to-transformer": { - "version": "4.0.2", + "version": "4.0.11", "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" + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.3.0" } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-model-transformer": { - "version": "3.0.2", + "version": "3.1.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-transformer-interfaces": "4.1.0", + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1" + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.3.0" } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-predictions-transformer": { - "version": "3.0.2", + "version": "3.0.11", "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", + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1" + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.3.0" } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-relational-transformer": { - "version": "3.0.2", + "version": "3.1.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-transformer-interfaces": "4.1.0", + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-index-transformer": "3.0.11", + "@aws-amplify/graphql-model-transformer": "3.1.3", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1", + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2", "immer": "^9.0.12" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.3.0" } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-searchable-transformer": { - "version": "3.0.2", + "version": "3.0.11", "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", + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-model-transformer": "3.1.3", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1" + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.3.0" } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-sql-transformer": { - "version": "0.4.2", + "version": "0.4.11", "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", + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-model-transformer": "3.1.3", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1" + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.3.0" } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-transformer": { - "version": "2.1.0", + "version": "2.2.4", "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" + "@aws-amplify/graphql-auth-transformer": "4.1.9", + "@aws-amplify/graphql-conversation-transformer": "1.1.4", + "@aws-amplify/graphql-default-value-transformer": "3.1.6", + "@aws-amplify/graphql-function-transformer": "3.1.8", + "@aws-amplify/graphql-generation-transformer": "1.1.2", + "@aws-amplify/graphql-http-transformer": "3.0.11", + "@aws-amplify/graphql-index-transformer": "3.0.11", + "@aws-amplify/graphql-maps-to-transformer": "4.0.11", + "@aws-amplify/graphql-model-transformer": "3.1.3", + "@aws-amplify/graphql-predictions-transformer": "3.0.11", + "@aws-amplify/graphql-relational-transformer": "3.1.3", + "@aws-amplify/graphql-searchable-transformer": "3.0.11", + "@aws-amplify/graphql-sql-transformer": "0.4.11", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", + "@aws-amplify/plugin-types": "^1.0.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.3.0" } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-transformer-core": { - "version": "3.1.0", + "version": "3.3.3", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-directives": "2.1.0", - "@aws-amplify/graphql-transformer-interfaces": "4.1.0", + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", "fs-extra": "^8.1.0", "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", - "graphql-transformer-common": "5.0.1", + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2", "hjson": "^3.2.2", "lodash": "^4.17.21", "md5": "^2.3.0", @@ -1122,740 +1337,5195 @@ "ts-dedent": "^2.0.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.3.0" } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/graphql-transformer-interfaces": { - "version": "4.1.0", + "version": "4.2.1", "inBundle": true, "license": "Apache-2.0", "dependencies": { "graphql": "^15.5.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.3.0" } }, "node_modules/@aws-amplify/data-construct/node_modules/@aws-amplify/platform-core": { - "version": "0.2.0", + "version": "1.2.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.4.0", "inBundle": true, "license": "Apache-2.0", "peerDependencies": { - "aws-cdk-lib": "^2.103.0", + "@aws-sdk/types": "^3.609.0", + "aws-cdk-lib": "^2.158.0", "constructs": "^10.0.0" } }, - "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/crypt": { - "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/fs-extra": { - "version": "8.1.0", + "node_modules/@aws-amplify/data-construct/node_modules/@aws-crypto/crc32/node_modules/@aws-sdk/types": { + "version": "3.692.0", "inBundle": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "@smithy/types": "^3.7.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/data-construct/node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", "inBundle": true, - "license": "MIT", + "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/@aws-crypto/sha256-browser/node_modules/@aws-sdk/types": { + "version": "3.692.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.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": "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/sha256-js/node_modules/@aws-sdk/types": { + "version": "3.692.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.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/@aws-sdk/types": { + "version": "3.692.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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.693.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.693.0", + "@aws-sdk/client-sts": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/eventstream-serde-browser": "^3.0.12", + "@smithy/eventstream-serde-config-resolver": "^3.0.9", + "@smithy/eventstream-serde-node": "^3.0.11", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-stream": "^3.3.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/client-sso": { + "version": "3.693.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.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.693.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.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/core": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/core": "^2.5.2", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/signature-v4": "^4.2.2", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-middleware": "^3.0.9", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-stream": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-ini": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/token-providers": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/middleware-logger": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@smithy/core": "^2.5.2", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.9", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/token-providers": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.693.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/types": { + "version": "3.692.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/util-endpoints": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "@smithy/util-endpoints": "^2.1.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/fetch-http-handler": { + "version": "4.1.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.7", + "@smithy/querystring-builder": "^3.0.10", + "@smithy/types": "^3.7.1", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "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.693.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.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/client-sso": { + "version": "3.693.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.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.693.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.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/core": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/core": "^2.5.2", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/signature-v4": "^4.2.2", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-middleware": "^3.0.9", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-stream": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-ini": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/token-providers": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/middleware-logger": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@smithy/core": "^2.5.2", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.9", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/token-providers": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.693.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/types": { + "version": "3.692.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/util-endpoints": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "@smithy/util-endpoints": "^2.1.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@aws-sdk/client-sts/node_modules/@smithy/fetch-http-handler": { + "version": "4.1.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.7", + "@smithy/querystring-builder": "^3.0.10", + "@smithy/types": "^3.7.1", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "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.693.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.8", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/config-resolver": { + "version": "3.0.12", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.11", + "@smithy/types": "^3.7.1", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.10", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/core": { + "version": "2.5.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^3.0.10", + "@smithy/protocol-http": "^4.1.7", + "@smithy/types": "^3.7.1", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-middleware": "^3.0.10", + "@smithy/util-stream": "^3.3.1", + "@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.7", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.11", + "@smithy/property-provider": "^3.1.10", + "@smithy/types": "^3.7.1", + "@smithy/url-parser": "^3.0.10", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/eventstream-codec": { + "version": "3.1.9", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^3.7.1", + "@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.13", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^3.0.12", + "@smithy/types": "^3.7.1", + "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.10", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/eventstream-serde-node": { + "version": "3.0.12", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^3.0.12", + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/eventstream-serde-universal": { + "version": "3.0.12", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^3.1.9", + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.9", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.4", + "@smithy/querystring-builder": "^3.0.7", + "@smithy/types": "^3.5.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/hash-node": { + "version": "3.0.10", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.1", + "@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.10", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.1", + "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.12", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.7", + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/middleware-endpoint": { + "version": "3.2.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^2.5.3", + "@smithy/middleware-serde": "^3.0.10", + "@smithy/node-config-provider": "^3.1.11", + "@smithy/shared-ini-file-loader": "^3.1.11", + "@smithy/types": "^3.7.1", + "@smithy/url-parser": "^3.0.10", + "@smithy/util-middleware": "^3.0.10", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/middleware-retry": { + "version": "3.0.27", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.11", + "@smithy/protocol-http": "^4.1.7", + "@smithy/service-error-classification": "^3.0.10", + "@smithy/smithy-client": "^3.4.4", + "@smithy/types": "^3.7.1", + "@smithy/util-middleware": "^3.0.10", + "@smithy/util-retry": "^3.0.10", + "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.10", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/middleware-stack": { + "version": "3.0.10", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/node-config-provider": { + "version": "3.1.11", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^3.1.10", + "@smithy/shared-ini-file-loader": "^3.1.11", + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/node-http-handler": { + "version": "3.3.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^3.1.8", + "@smithy/protocol-http": "^4.1.7", + "@smithy/querystring-builder": "^3.0.10", + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/property-provider": { + "version": "3.1.10", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/protocol-http": { + "version": "4.1.7", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/querystring-builder": { + "version": "3.0.10", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.1", + "@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.10", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/service-error-classification": { + "version": "3.0.10", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/shared-ini-file-loader": { + "version": "3.1.11", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/signature-v4": { + "version": "4.2.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/protocol-http": "^4.1.7", + "@smithy/types": "^3.7.1", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-middleware": "^3.0.10", + "@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.4.4", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^2.5.3", + "@smithy/middleware-endpoint": "^3.2.3", + "@smithy/middleware-stack": "^3.0.10", + "@smithy/protocol-http": "^4.1.7", + "@smithy/types": "^3.7.1", + "@smithy/util-stream": "^3.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/types": { + "version": "3.7.1", + "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.10", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^3.0.10", + "@smithy/types": "^3.7.1", + "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.27", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^3.1.10", + "@smithy/smithy-client": "^3.4.4", + "@smithy/types": "^3.7.1", + "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.27", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^3.0.12", + "@smithy/credential-provider-imds": "^3.2.7", + "@smithy/node-config-provider": "^3.1.11", + "@smithy/property-provider": "^3.1.10", + "@smithy/smithy-client": "^3.4.4", + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/util-endpoints": { + "version": "2.1.6", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.11", + "@smithy/types": "^3.7.1", + "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.10", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/util-retry": { + "version": "3.0.10", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^3.0.10", + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/data-construct/node_modules/@smithy/util-stream": { + "version": "3.3.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^4.1.1", + "@smithy/node-http-handler": "^3.3.1", + "@smithy/types": "^3.7.1", + "@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-stream/node_modules/@smithy/fetch-http-handler": { + "version": "4.1.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.7", + "@smithy/querystring-builder": "^3.0.10", + "@smithy/types": "^3.7.1", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "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.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/@aws-amplify/data-construct/node_modules/graphql-transformer-common": { + "version": "5.1.2", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.2", + "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/fs-extra/node_modules/universalify": { - "version": "0.1.2", + "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.8.1", + "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.16.1", + "resolved": "https://registry.npmjs.org/@aws-amplify/data-schema/-/data-schema-1.16.1.tgz", + "integrity": "sha512-ThEiEoDbGfU03a2wVpdW4VORLrUkrlWMb9Xc6kI6I296+Gk0DHKNmQUFov4nlqxUIBe3lntJUcZSCMWJZTq4ZQ==", + "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.18.5", + "resolved": "https://registry.npmjs.org/@aws-amplify/graphql-api-construct/-/graphql-api-construct-1.18.5.tgz", + "integrity": "sha512-QZXZJ7R15eFfILjwbawcLDtuOX9J7JQvDhVv3/AJqAWWsLCk2nAXcL/rBPdZ27P6OO53LPGWGBzPccsWMj7S2w==", + "bundleDependencies": [ + "@aws-amplify/ai-constructs", + "@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-crypto/crc32", + "@aws-crypto/sha256-browser", + "@aws-crypto/sha256-js", + "@aws-crypto/supports-web-crypto", + "@aws-crypto/util", + "@aws-sdk/client-bedrock-runtime", + "@aws-sdk/client-sso", + "@aws-sdk/client-sso-oidc", + "@aws-sdk/client-sts", + "@aws-sdk/core", + "@aws-sdk/credential-provider-env", + "@aws-sdk/credential-provider-http", + "@aws-sdk/credential-provider-ini", + "@aws-sdk/credential-provider-node", + "@aws-sdk/credential-provider-process", + "@aws-sdk/credential-provider-sso", + "@aws-sdk/credential-provider-web-identity", + "@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/token-providers", + "@aws-sdk/types", + "@aws-sdk/util-endpoints", + "@aws-sdk/util-locate-window", + "@aws-sdk/util-user-agent-browser", + "@aws-sdk/util-user-agent-node", + "@smithy/abort-controller", + "@smithy/config-resolver", + "@smithy/core", + "@smithy/credential-provider-imds", + "@smithy/eventstream-codec", + "@smithy/eventstream-serde-browser", + "@smithy/eventstream-serde-config-resolver", + "@smithy/eventstream-serde-node", + "@smithy/eventstream-serde-universal", + "@smithy/fetch-http-handler", + "@smithy/hash-node", + "@smithy/invalid-dependency", + "@smithy/is-array-buffer", + "@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/property-provider", + "@smithy/protocol-http", + "@smithy/querystring-builder", + "@smithy/querystring-parser", + "@smithy/service-error-classification", + "@smithy/shared-ini-file-loader", + "@smithy/signature-v4", + "@smithy/smithy-client", + "@smithy/types", + "@smithy/url-parser", + "@smithy/util-base64", + "@smithy/util-body-length-browser", + "@smithy/util-body-length-node", + "@smithy/util-buffer-from", + "@smithy/util-config-provider", + "@smithy/util-defaults-mode-browser", + "@smithy/util-defaults-mode-node", + "@smithy/util-endpoints", + "@smithy/util-hex-encoding", + "@smithy/util-middleware", + "@smithy/util-retry", + "@smithy/util-stream", + "@smithy/util-uri-escape", + "@smithy/util-utf8", + "bowser", + "charenc", + "ci-info", + "crypt", + "fast-xml-parser", + "fs-extra", + "graceful-fs", + "graphql", + "graphql-mapping-template", + "graphql-transformer-common", + "hjson", + "immer", + "is-buffer", + "is-ci", + "jsonfile", + "libphonenumber-js", + "lodash", + "lodash.mergewith", + "md5", + "object-hash", + "pluralize", + "semver", + "strnum", + "ts-dedent", + "tslib", + "universalify", + "uuid", + "zod" + ], + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/ai-constructs": "^1.0.0", + "@aws-amplify/backend-output-schemas": "^1.0.0", + "@aws-amplify/backend-output-storage": "^1.0.0", + "@aws-amplify/graphql-auth-transformer": "4.1.9", + "@aws-amplify/graphql-conversation-transformer": "1.1.4", + "@aws-amplify/graphql-default-value-transformer": "3.1.6", + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-function-transformer": "3.1.8", + "@aws-amplify/graphql-generation-transformer": "1.1.2", + "@aws-amplify/graphql-http-transformer": "3.0.11", + "@aws-amplify/graphql-index-transformer": "3.0.11", + "@aws-amplify/graphql-maps-to-transformer": "4.0.11", + "@aws-amplify/graphql-model-transformer": "3.1.3", + "@aws-amplify/graphql-predictions-transformer": "3.0.11", + "@aws-amplify/graphql-relational-transformer": "3.1.3", + "@aws-amplify/graphql-searchable-transformer": "3.0.11", + "@aws-amplify/graphql-sql-transformer": "0.4.11", + "@aws-amplify/graphql-transformer": "2.2.4", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.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.0", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2", + "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.168.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/ai-constructs": { + "version": "1.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@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", + "json-schema-to-ts": "^3.1.1" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.158.0", + "constructs": "^10.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/backend-output-schemas": { + "version": "1.4.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.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/backend-output-schemas": "^1.2.0", + "@aws-amplify/platform-core": "^1.0.6", + "@aws-amplify/plugin-types": "^1.3.1" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.158.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-auth-transformer": { + "version": "4.1.9", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-model-transformer": "3.1.3", + "@aws-amplify/graphql-relational-transformer": "3.1.3", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2", + "lodash": "^4.17.21", + "md5": "^2.3.0" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.168.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-conversation-transformer": { + "version": "1.1.4", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/ai-constructs": "^1.0.0", + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-index-transformer": "3.0.11", + "@aws-amplify/graphql-model-transformer": "3.1.3", + "@aws-amplify/graphql-relational-transformer": "3.1.3", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", + "@aws-amplify/plugin-types": "^1.0.0", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2", + "immer": "^9.0.12", + "semver": "^7.6.3" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.168.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-default-value-transformer": { + "version": "3.1.6", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2", + "libphonenumber-js": "1.9.47" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-directives": { + "version": "2.6.1", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-function-transformer": { + "version": "3.1.8", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.168.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-generation-transformer": { + "version": "1.1.2", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2", + "immer": "^9.0.12" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.168.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-http-transformer": { + "version": "3.0.11", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.168.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-index-transformer": { + "version": "3.0.11", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-model-transformer": "3.1.3", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.168.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-maps-to-transformer": { + "version": "4.0.11", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.168.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-model-transformer": { + "version": "3.1.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.168.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-predictions-transformer": { + "version": "3.0.11", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.168.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-relational-transformer": { + "version": "3.1.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-index-transformer": "3.0.11", + "@aws-amplify/graphql-model-transformer": "3.1.3", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2", + "immer": "^9.0.12" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.168.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-searchable-transformer": { + "version": "3.0.11", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-model-transformer": "3.1.3", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.168.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-sql-transformer": { + "version": "0.4.11", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-model-transformer": "3.1.3", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.168.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-transformer": { + "version": "2.2.4", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-auth-transformer": "4.1.9", + "@aws-amplify/graphql-conversation-transformer": "1.1.4", + "@aws-amplify/graphql-default-value-transformer": "3.1.6", + "@aws-amplify/graphql-function-transformer": "3.1.8", + "@aws-amplify/graphql-generation-transformer": "1.1.2", + "@aws-amplify/graphql-http-transformer": "3.0.11", + "@aws-amplify/graphql-index-transformer": "3.0.11", + "@aws-amplify/graphql-maps-to-transformer": "4.0.11", + "@aws-amplify/graphql-model-transformer": "3.1.3", + "@aws-amplify/graphql-predictions-transformer": "3.0.11", + "@aws-amplify/graphql-relational-transformer": "3.1.3", + "@aws-amplify/graphql-searchable-transformer": "3.0.11", + "@aws-amplify/graphql-sql-transformer": "0.4.11", + "@aws-amplify/graphql-transformer-core": "3.3.3", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", + "@aws-amplify/plugin-types": "^1.0.0" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.168.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-transformer-core": { + "version": "3.3.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/graphql-directives": "2.6.1", + "@aws-amplify/graphql-transformer-interfaces": "4.2.1", + "fs-extra": "^8.1.0", + "graphql": "^15.5.0", + "graphql-mapping-template": "5.0.2", + "graphql-transformer-common": "5.1.2", + "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.168.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/graphql-transformer-interfaces": { + "version": "4.2.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "graphql": "^15.5.0" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.168.0", + "constructs": "^10.3.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-amplify/platform-core": { + "version": "1.2.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.4.0", + "inBundle": true, + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/types": "^3.609.0", + "aws-cdk-lib": "^2.158.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/crc32/node_modules/@aws-sdk/types": { + "version": "3.692.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.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/@aws-sdk/types": { + "version": "3.692.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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/sha256-js/node_modules/@aws-sdk/types": { + "version": "3.692.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.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/@aws-sdk/types": { + "version": "3.692.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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.693.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.693.0", + "@aws-sdk/client-sts": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/eventstream-serde-browser": "^3.0.12", + "@smithy/eventstream-serde-config-resolver": "^3.0.9", + "@smithy/eventstream-serde-node": "^3.0.11", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-stream": "^3.3.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/client-sso": { + "version": "3.693.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.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.693.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.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/core": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/core": "^2.5.2", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/signature-v4": "^4.2.2", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-middleware": "^3.0.9", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-stream": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-ini": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/token-providers": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/middleware-logger": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@smithy/core": "^2.5.2", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.9", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/token-providers": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.693.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/types": { + "version": "3.692.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/util-endpoints": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "@smithy/util-endpoints": "^2.1.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@smithy/fetch-http-handler": { + "version": "4.1.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.7", + "@smithy/querystring-builder": "^3.0.10", + "@smithy/types": "^3.7.1", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "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.693.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.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/client-sso": { + "version": "3.693.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.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.693.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.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/core": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/core": "^2.5.2", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/signature-v4": "^4.2.2", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-middleware": "^3.0.9", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-stream": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-ini": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/token-providers": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.693.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/middleware-logger": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@smithy/core": "^2.5.2", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.9", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/token-providers": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.693.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/types": { + "version": "3.692.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/util-endpoints": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "@smithy/util-endpoints": "^2.1.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.693.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/client-sts/node_modules/@smithy/fetch-http-handler": { + "version": "4.1.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.7", + "@smithy/querystring-builder": "^3.0.10", + "@smithy/types": "^3.7.1", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "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": { + "@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/graphql-api-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/graphql-api-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/graphql-api-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/graphql-api-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/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": "MIT", + "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": ">= 4.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.614.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/types": { + "version": "3.609.0", "inBundle": true, - "license": "ISC" + "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/graphql": { - "version": "15.8.0", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/util-endpoints": { + "version": "3.637.0", "inBundle": true, - "license": "MIT", + "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": ">= 10.x" + "node": ">=16.0.0" } }, - "node_modules/@aws-amplify/data-construct/node_modules/graphql-mapping-template": { - "version": "5.0.1", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/util-locate-window": { + "version": "3.693.0", "inBundle": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.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/@aws-sdk/util-user-agent-browser": { + "version": "3.609.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" + "@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/hjson": { - "version": "3.2.2", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.614.0", "inBundle": true, - "license": "MIT", - "bin": { - "hjson": "bin/hjson" + "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/immer": { - "version": "9.0.21", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/abort-controller": { + "version": "3.1.8", "inBundle": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.1", + "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/config-resolver": { + "version": "3.0.12", "inBundle": true, - "license": "MIT", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.11", + "@smithy/types": "^3.7.1", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.10", + "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/core": { + "version": "2.5.3", "inBundle": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "universalify": "^2.0.0" + "@smithy/middleware-serde": "^3.0.10", + "@smithy/protocol-http": "^4.1.7", + "@smithy/types": "^3.7.1", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-middleware": "^3.0.10", + "@smithy/util-stream": "^3.3.1", + "@smithy/util-utf8": "^3.0.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/credential-provider-imds": { + "version": "3.2.7", "inBundle": true, - "license": "MIT" + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.11", + "@smithy/property-provider": "^3.1.10", + "@smithy/types": "^3.7.1", + "@smithy/url-parser": "^3.0.10", + "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/eventstream-codec": { + "version": "3.1.9", "inBundle": true, - "license": "MIT" + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^3.7.1", + "@smithy/util-hex-encoding": "^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/eventstream-serde-browser": { + "version": "3.0.13", "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/eventstream-serde-universal": "^3.0.12", + "@smithy/types": "^3.7.1", + "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/eventstream-serde-config-resolver": { + "version": "3.0.10", "inBundle": true, - "license": "MIT" + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } }, - "node_modules/@aws-amplify/data-construct/node_modules/object-hash": { + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/eventstream-serde-node": { + "version": "3.0.12", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^3.0.12", + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/eventstream-serde-universal": { + "version": "3.0.12", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^3.1.9", + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/fetch-http-handler": { + "version": "3.2.9", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.4", + "@smithy/querystring-builder": "^3.0.7", + "@smithy/types": "^3.5.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/hash-node": { + "version": "3.0.10", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.1", + "@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/graphql-api-construct/node_modules/@smithy/invalid-dependency": { + "version": "3.0.10", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + } + }, + "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.12", "inBundle": true, - "license": "MIT", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.7", + "@smithy/types": "^3.7.1", + "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.2.3", "inBundle": true, - "license": "MIT", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^2.5.3", + "@smithy/middleware-serde": "^3.0.10", + "@smithy/node-config-provider": "^3.1.11", + "@smithy/shared-ini-file-loader": "^3.1.11", + "@smithy/types": "^3.7.1", + "@smithy/url-parser": "^3.0.10", + "@smithy/util-middleware": "^3.0.10", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/middleware-retry": { + "version": "3.0.27", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.11", + "@smithy/protocol-http": "^4.1.7", + "@smithy/service-error-classification": "^3.0.10", + "@smithy/smithy-client": "^3.4.4", + "@smithy/types": "^3.7.1", + "@smithy/util-middleware": "^3.0.10", + "@smithy/util-retry": "^3.0.10", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, "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-serde": { + "version": "3.0.10", "inBundle": true, - "license": "MIT", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + }, "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-stack": { + "version": "3.0.10", "inBundle": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.1", + "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/node-config-provider": { + "version": "3.1.11", + "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/property-provider": "^3.1.10", + "@smithy/shared-ini-file-loader": "^3.1.11", + "@smithy/types": "^3.7.1", + "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-http-handler": { + "version": "3.3.1", + "inBundle": true, "license": "Apache-2.0", "dependencies": { - "graphql": "15.8.0", - "rxjs": "^7.8.1" + "@smithy/abort-controller": "^3.1.8", + "@smithy/protocol-http": "^4.1.7", + "@smithy/querystring-builder": "^3.0.10", + "@smithy/types": "^3.7.1", + "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/property-provider": { + "version": "3.1.10", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.1", + "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/protocol-http": { + "version": "4.1.7", + "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.7.1", + "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/querystring-builder": { + "version": "3.0.10", + "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.7.1", + "@smithy/util-uri-escape": "^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/ai-constructs": { - "version": "0.1.2", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/querystring-parser": { + "version": "3.0.10", "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.7.1", + "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/service-error-classification": { + "version": "3.0.10", "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.7.1" + }, + "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/shared-ini-file-loader": { + "version": "3.1.11", "inBundle": true, "license": "Apache-2.0", - "peerDependencies": { - "zod": "^3.21.4" + "dependencies": { + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" + }, + "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/signature-v4": { + "version": "4.2.3", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/backend-output-schemas": "^0.4.0", - "@aws-amplify/platform-core": "^0.2.0" + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/protocol-http": "^4.1.7", + "@smithy/types": "^3.7.1", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-middleware": "^3.0.10", + "@smithy/util-uri-escape": "^3.0.0", + "@smithy/util-utf8": "^3.0.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": { - "version": "4.1.0", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/smithy-client": { + "version": "3.4.4", "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/core": "^2.5.3", + "@smithy/middleware-endpoint": "^3.2.3", + "@smithy/middleware-stack": "^3.0.10", + "@smithy/protocol-http": "^4.1.7", + "@smithy/types": "^3.7.1", + "@smithy/util-stream": "^3.3.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-conversation-transformer": { - "version": "0.2.0", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/types": { + "version": "3.7.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/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" + "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/url-parser": { + "version": "3.0.10", "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" + "@smithy/querystring-parser": "^3.0.10", + "@smithy/types": "^3.7.1", + "tslib": "^2.6.2" } }, - "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/util-base64": { + "version": "3.0.0", "inBundle": true, - "license": "Apache-2.0" + "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/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-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" - }, - "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-generation-transformer": { - "version": "0.2.0", + "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", - "immer": "^9.0.12" + "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-http-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-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-index-transformer": { - "version": "3.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-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-maps-to-transformer": { - "version": "4.0.2", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/util-defaults-mode-browser": { + "version": "3.0.27", "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" + "@smithy/property-provider": "^3.1.10", + "@smithy/smithy-client": "^3.4.4", + "@smithy/types": "^3.7.1", + "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-model-transformer": { - "version": "3.0.2", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/util-defaults-mode-node": { + "version": "3.0.27", "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.12", + "@smithy/credential-provider-imds": "^3.2.7", + "@smithy/node-config-provider": "^3.1.11", + "@smithy/property-provider": "^3.1.10", + "@smithy/smithy-client": "^3.4.4", + "@smithy/types": "^3.7.1", + "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-endpoints": { + "version": "2.1.6", "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/node-config-provider": "^3.1.11", + "@smithy/types": "^3.7.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-relational-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-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" + "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-middleware": { + "version": "3.0.10", "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.7.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-retry": { + "version": "3.0.10", "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/service-error-classification": "^3.0.10", + "@smithy/types": "^3.7.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-transformer": { - "version": "2.1.0", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/util-stream": { + "version": "3.3.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-transformer-interfaces": "4.1.0" + "@smithy/fetch-http-handler": "^4.1.1", + "@smithy/node-http-handler": "^3.3.1", + "@smithy/types": "^3.7.1", + "@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-core": { - "version": "3.1.0", + "node_modules/@aws-amplify/graphql-api-construct/node_modules/@smithy/util-stream/node_modules/@smithy/fetch-http-handler": { + "version": "4.1.1", "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" - }, - "peerDependencies": { - "aws-cdk-lib": "^2.152.0", - "constructs": "^10.3.0" + "@smithy/protocol-http": "^4.1.7", + "@smithy/querystring-builder": "^3.0.10", + "@smithy/types": "^3.7.1", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" } }, - "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 +6535,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 +6557,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 +6591,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": { @@ -1916,17 +6605,17 @@ } }, "node_modules/@aws-amplify/graphql-api-construct/node_modules/graphql-mapping-template": { - "version": "5.0.1", + "version": "5.0.2", "inBundle": true, "license": "Apache-2.0" }, "node_modules/@aws-amplify/graphql-api-construct/node_modules/graphql-transformer-common": { - "version": "5.0.1", + "version": "5.1.2", "inBundle": true, "license": "Apache-2.0", "dependencies": { "graphql": "^15.5.0", - "graphql-mapping-template": "5.0.1", + "graphql-mapping-template": "5.0.2", "md5": "^2.2.1", "pluralize": "8.0.0" } @@ -1949,34 +6638,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 +6671,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 +6686,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 +6702,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 +6726,33 @@ "node": ">=6.10" } }, + "node_modules/@aws-amplify/graphql-api-construct/node_modules/tslib": { + "version": "2.8.1", + "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 +6783,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 +6816,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 +6833,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 +6848,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 +6861,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 +6911,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 +6925,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 +7553,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 +7585,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 +7603,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 +7627,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 +7673,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" } }, @@ -3185,15 +7943,15 @@ } }, "node_modules/@aws-cdk/asset-awscli-v1": { - "version": "2.2.202", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.202.tgz", - "integrity": "sha512-JqlF0D4+EVugnG5dAsNZMqhu3HW7ehOXm5SDMxMbXNDMdsF0pxtQKNHRl52z1U9igsHmaFpUgSGjbhAJ+0JONg==", + "version": "2.2.213", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.213.tgz", + "integrity": "sha512-crm1yDJmORJF2Y9gDvNUX4Q3iQXVhWrL7oaZfpx3QDqrvVz5UEgWGpJdysqDuWFZTmIgtrI5Svq3UfdwCNNpsg==", "license": "Apache-2.0" }, "node_modules/@aws-cdk/asset-kubectl-v20": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.2.tgz", - "integrity": "sha512-3M2tELJOxQv0apCIiuKQ4pAbncz9GuLwnKFqxifWfe77wuMxyTRPmxssYHs42ePqzap1LT6GDcPygGs+hHstLg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.3.tgz", + "integrity": "sha512-cDG1w3ieM6eOT9mTefRuTypk95+oyD7P5X/wRltwmYxU7nZc3+076YEVS6vrjDKr3ADYbfn0lDKpfB1FBtO9CQ==", "license": "Apache-2.0" }, "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { @@ -3203,9 +7961,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 +7972,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 +8686,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": { + "@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-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/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", + "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-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/@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/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": { "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 +15228,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 +15253,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 +15267,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 +15276,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 +15305,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 +15379,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 +15390,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 +15452,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 +15469,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 +15499,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 +15553,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 +15572,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 +15586,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 +15595,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 +15666,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.7", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.7.tgz", + "integrity": "sha512-LzxlLEMbBOPYB85uXrDqvD4MgcenjRBLIns3zyhx7vTPj/0u2eQhzXvPiGcaJrV38Q9dbkExWp6cOHPJ+EtFYg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -10392,16 +15678,16 @@ "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.1", "safe-buffer": "^5.1.2", - "tough-cookie": "^4.1.3", + "tough-cookie": "^5.0.0", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" }, @@ -13123,6 +18409,12 @@ "string-argv": "~0.3.1" } }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, "node_modules/@shopify/eslint-plugin": { "version": "43.0.0", "resolved": "https://registry.npmjs.org/@shopify/eslint-plugin/-/eslint-plugin-43.0.0.tgz", @@ -13206,6 +18498,18 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -13256,12 +18560,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 +18591,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 +18606,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 +18620,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 +18632,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 +18652,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 +18667,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 +18681,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 +18760,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 +18784,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 +18812,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 +18844,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 +18857,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 +18874,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 +18888,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 +18900,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 +18919,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 +18933,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 +18958,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 +18970,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 +19022,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 +19037,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 +19049,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 +19061,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 +19074,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 +19086,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 +19122,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,27 +19140,25 @@ } }, "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": { "node": ">=16.0.0" } }, - "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", + "node_modules/@smithy/types": { + "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 +19167,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 +19237,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 +19252,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 +19269,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 +19283,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 +19295,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 +19308,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 +19322,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 +19346,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 +19358,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 +19371,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 +20035,20 @@ "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.7", + "resolved": "https://registry.npmjs.org/@verdaccio/auth/-/auth-8.0.0-next-8.7.tgz", + "integrity": "sha512-CSLBAsCJT1oOpJ4OWnVGmN6o/ZilDNa7Aa5+AU1LI2lbRblqgr4BVRn07GFqimJ//6+tPzl8BHgyiCbBhh1ZiA==", "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", - "debug": "4.3.7", + "@verdaccio/config": "8.0.0-next-8.7", + "@verdaccio/core": "8.0.0-next-8.7", + "@verdaccio/loaders": "8.0.0-next-8.4", + "@verdaccio/signature": "8.0.0-next-8.1", + "@verdaccio/utils": "8.1.0-next-8.7", + "debug": "4.4.0", "lodash": "4.17.21", - "verdaccio-htpasswd": "13.0.0-next-8.1" + "verdaccio-htpasswd": "13.0.0-next-8.7" }, "engines": { "node": ">=18" @@ -14795,6 +20058,70 @@ "url": "https://opencollective.com/verdaccio" } }, + "node_modules/@verdaccio/auth/node_modules/@verdaccio/utils": { + "version": "8.1.0-next-8.7", + "resolved": "https://registry.npmjs.org/@verdaccio/utils/-/utils-8.1.0-next-8.7.tgz", + "integrity": "sha512-4eqPCnPAJsL6gdVs0/oqZNgs2PnQW3HHBMgBHyEbb5A/ESI10TvRp+B7MRl9glUmy/aR5B6YSI68rgXvAFjdxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@verdaccio/core": "8.0.0-next-8.7", + "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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@verdaccio/auth/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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, + "license": "ISC", + "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 +20148,41 @@ "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.7", + "resolved": "https://registry.npmjs.org/@verdaccio/config/-/config-8.0.0-next-8.7.tgz", + "integrity": "sha512-pA0WCWvvWY6vPRav+X0EuFmuK6M08zIpRzTKkqSriCWk6JUCZ07TDnN054AS8TSSOy6EaWgHxnUw3nTd34Z4Sg==", "dev": true, "license": "MIT", "dependencies": { - "@verdaccio/core": "8.0.0-next-8.1", - "@verdaccio/utils": "7.0.1-next-8.1", - "debug": "4.3.7", + "@verdaccio/core": "8.0.0-next-8.7", + "@verdaccio/utils": "8.1.0-next-8.7", + "debug": "4.4.0", "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.7", + "resolved": "https://registry.npmjs.org/@verdaccio/utils/-/utils-8.1.0-next-8.7.tgz", + "integrity": "sha512-4eqPCnPAJsL6gdVs0/oqZNgs2PnQW3HHBMgBHyEbb5A/ESI10TvRp+B7MRl9glUmy/aR5B6YSI68rgXvAFjdxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@verdaccio/core": "8.0.0-next-8.7", + "lodash": "4.17.21", + "minimatch": "7.4.6", + "semver": "7.6.3" + }, + "engines": { + "node": ">=12" }, "funding": { "type": "opencollective", @@ -14859,6 +20206,24 @@ "balanced-match": "^1.0.0" } }, + "node_modules/@verdaccio/config/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/@verdaccio/config/node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -14889,9 +20254,9 @@ } }, "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.7", + "resolved": "https://registry.npmjs.org/@verdaccio/core/-/core-8.0.0-next-8.7.tgz", + "integrity": "sha512-pf8M2Z5EI/5Zdhdcm3aadb9Q9jiDsIredPD3+cIoDum8x3di2AIYvQD7i5BEramfzZlLXVICmFAulU7nUY11qg==", "dev": true, "license": "MIT", "dependencies": { @@ -14903,7 +20268,7 @@ "semver": "7.6.3" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { "type": "opencollective", @@ -14964,13 +20329,12 @@ } }, "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.4", + "resolved": "https://registry.npmjs.org/@verdaccio/loaders/-/loaders-8.0.0-next-8.4.tgz", + "integrity": "sha512-Powlqb4SuMbe6RVgxyyOXaCjuHCcK7oZA+lygaKZDpV9NSHJtbkkV4L+rXyCfTX3b0tKsBh7FzaIdgWc1rDeGQ==", "dev": true, "license": "MIT", "dependencies": { - "@verdaccio/logger": "8.0.0-next-8.1", "debug": "4.3.7", "lodash": "4.17.21" }, @@ -15039,14 +20403,14 @@ "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.7", + "resolved": "https://registry.npmjs.org/@verdaccio/logger/-/logger-8.0.0-next-8.7.tgz", + "integrity": "sha512-5EMPdZhz2V08BP2rjhtN/Fz5KxCfPJBkYDitbk/eo+FCZ9nVdMCQE3WRbHEaXyJQcIso/LJ6RnL/zKN20E/rPg==", "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.7", + "pino": "9.5.0" }, "engines": { "node": ">=18" @@ -15056,162 +20420,102 @@ "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.7", + "resolved": "https://registry.npmjs.org/@verdaccio/logger-commons/-/logger-commons-8.0.0-next-8.7.tgz", + "integrity": "sha512-sXNx57G1LVp81xF4qHer3AOcMEZ90W4FjxtYF0vmULcVg3ybdtStKAT/9ocZtVMvLWTPAauhqylfnXoRZYf32A==", "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.7", + "@verdaccio/logger-prettify": "8.0.0-next-8.1", + "colorette": "2.0.20", + "debug": "4.4.0" }, "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==", + "node_modules/@verdaccio/logger-commons/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "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" + "ms": "^2.1.3" }, - "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" + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "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==", + "node_modules/@verdaccio/logger-prettify": { + "version": "8.0.0-next-8.1", + "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": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "colorette": "2.0.20", + "dayjs": "1.11.13", + "lodash": "4.17.21", + "pino-abstract-transport": "1.2.0", + "sonic-boom": "3.8.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": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" } }, - "node_modules/@verdaccio/logger-commons": { - "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==", + "node_modules/@verdaccio/middleware": { + "version": "8.0.0-next-8.7", + "resolved": "https://registry.npmjs.org/@verdaccio/middleware/-/middleware-8.0.0-next-8.7.tgz", + "integrity": "sha512-Zad7KcdOsI1DUBt1TjQb08rIi/IFFaJKdPhj7M6oy5BX9l/4OM0TtbBueHFNS1+aU+t5eo8ue7ZHbqmjDY/6VQ==", "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" + "@verdaccio/config": "8.0.0-next-8.7", + "@verdaccio/core": "8.0.0-next-8.7", + "@verdaccio/url": "13.0.0-next-8.7", + "@verdaccio/utils": "8.1.0-next-8.7", + "debug": "4.4.0", + "express": "4.21.2", + "express-rate-limit": "5.5.1", + "lodash": "4.17.21", + "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/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/node_modules/@verdaccio/utils": { + "version": "8.1.0-next-8.7", + "resolved": "https://registry.npmjs.org/@verdaccio/utils/-/utils-8.1.0-next-8.7.tgz", + "integrity": "sha512-4eqPCnPAJsL6gdVs0/oqZNgs2PnQW3HHBMgBHyEbb5A/ESI10TvRp+B7MRl9glUmy/aR5B6YSI68rgXvAFjdxA==", "dev": true, "license": "MIT", "dependencies": { - "colorette": "2.0.20", - "dayjs": "1.11.13", + "@verdaccio/core": "8.0.0-next-8.7", "lodash": "4.17.21", - "pino-abstract-transport": "1.1.0", - "sonic-boom": "3.8.0" + "minimatch": "7.4.6", + "semver": "7.6.3" }, "engines": { "node": ">=12" @@ -15221,30 +20525,32 @@ "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/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" + } + }, + "node_modules/@verdaccio/middleware/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "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", - "lodash": "4.17.21", - "lru-cache": "7.18.3", - "mime": "2.6.0" + "ms": "^2.1.3" }, "engines": { - "node": ">=12" + "node": ">=6.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/verdaccio" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/@verdaccio/middleware/node_modules/lru-cache": { @@ -15270,14 +20576,29 @@ "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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "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,9 +20606,9 @@ } }, "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": { @@ -15295,7 +20616,7 @@ "jsonwebtoken": "9.0.2" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { "type": "opencollective", @@ -15318,55 +20639,137 @@ } }, "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.7", + "resolved": "https://registry.npmjs.org/@verdaccio/tarball/-/tarball-13.0.0-next-8.7.tgz", + "integrity": "sha512-EWRuEOLgb3UETxUsYg6+Mml6DDRiwQqKIEsE4Ys6y6rcH2vgW6XMnTt+s/v5pFI+zlbi6fxjOgQB1e6IJAwxVA==", "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", - "debug": "4.3.7", + "@verdaccio/core": "8.0.0-next-8.7", + "@verdaccio/url": "13.0.0-next-8.7", + "@verdaccio/utils": "8.1.0-next-8.7", + "debug": "4.4.0", "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/tarball/node_modules/@verdaccio/utils": { + "version": "8.1.0-next-8.7", + "resolved": "https://registry.npmjs.org/@verdaccio/utils/-/utils-8.1.0-next-8.7.tgz", + "integrity": "sha512-4eqPCnPAJsL6gdVs0/oqZNgs2PnQW3HHBMgBHyEbb5A/ESI10TvRp+B7MRl9glUmy/aR5B6YSI68rgXvAFjdxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@verdaccio/core": "8.0.0-next-8.7", + "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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@verdaccio/tarball/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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, + "license": "ISC", + "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.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==", + "version": "8.0.0-next-8.7", + "resolved": "https://registry.npmjs.org/@verdaccio/ui-theme/-/ui-theme-8.0.0-next-8.7.tgz", + "integrity": "sha512-+7f7XqqIU+TVCHjsP6lWzCdsD4sM7MEhn4cu3mLW1kJZ7eenWKEltoqixQnoXJzaBjCiz+yXW1WkjMyEFLNbpg==", "dev": true, "license": "MIT" }, "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.7", + "resolved": "https://registry.npmjs.org/@verdaccio/url/-/url-13.0.0-next-8.7.tgz", + "integrity": "sha512-biFvwH3zIXYicA+SXNGvjMAe8oIQ5VddsfbO0ZXWlFs0lIz8cgI7QYPeSiCkU2VKpGzZ8pEKgqkxFsfFkU5kGA==", "dev": true, "license": "MIT", "dependencies": { - "@verdaccio/core": "8.0.0-next-8.1", - "debug": "4.3.7", + "@verdaccio/core": "8.0.0-next-8.7", + "debug": "4.4.0", "lodash": "4.17.21", "validator": "13.12.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/verdaccio" } }, + "node_modules/@verdaccio/url/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/@verdaccio/utils": { "version": "7.0.1-next-8.1", "resolved": "https://registry.npmjs.org/@verdaccio/utils/-/utils-7.0.1-next-8.1.tgz", @@ -15387,6 +20790,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 +20839,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 +20954,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 +21099,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 +21390,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 +21522,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.171.1", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.171.1.tgz", + "integrity": "sha512-IWENyT4F5UcLr1szLsbipUdjIHn8FD3d/RvaIvhs2+qCamkfEV5mqv/ChMvRJ8H2jebhIZ2iz74or9O5Ismp+Q==", "license": "Apache-2.0", "peer": true, "bin": { @@ -16066,9 +21538,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.171.1", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.171.1.tgz", + "integrity": "sha512-BmXodHmeOWu7EZMwXFA+Mp+SnlZgIwhMxfOmqpdGa5dXF4BWOrs0cm4YgrzcJkg0XK713eXPj5IWGj8YeRIU3g==", "bundleDependencies": [ "@balena/dockerignore", "case", @@ -16084,10 +21556,10 @@ ], "license": "Apache-2.0", "dependencies": { - "@aws-cdk/asset-awscli-v1": "^2.2.202", - "@aws-cdk/asset-kubectl-v20": "^2.1.2", + "@aws-cdk/asset-awscli-v1": "^2.2.208", + "@aws-cdk/asset-kubectl-v20": "^2.1.3", "@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.1", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", "fs-extra": "^11.2.0", @@ -16211,9 +21683,9 @@ "license": "MIT" }, "node_modules/aws-cdk-lib/node_modules/fast-uri": { - "version": "3.0.1", + "version": "3.0.3", "inBundle": true, - "license": "MIT" + "license": "BSD-3-Clause" }, "node_modules/aws-cdk-lib/node_modules/fs-extra": { "version": "11.2.0", @@ -16527,9 +21999,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 +22147,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 +22204,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 +22245,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", @@ -16983,11 +22444,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 +22905,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,20 +22933,74 @@ } }, "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": "^7.0.0" + }, + "engines": { + "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": { - "slice-ansi": "^5.0.0", - "string-width": "^5.0.0" + "ansi-regex": "^6.0.1" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/cli-width": { @@ -17499,9 +23013,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 +23130,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 +23172,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 +23206,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 +23233,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 +23270,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 +23379,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 +23774,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 +23949,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", @@ -19842,6 +25367,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", @@ -19862,9 +25388,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.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dev": true, "license": "MIT", "dependencies": { @@ -19873,7 +25399,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", @@ -19887,7 +25413,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -19902,6 +25428,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-rate-limit": { @@ -20372,18 +25902,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 +25952,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 +26046,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", @@ -20547,6 +26091,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, "license": "MIT", "engines": { "node": ">=16" @@ -20980,15 +26525,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" @@ -21026,6 +26571,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=16.17.0" @@ -21096,6 +26642,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", @@ -21631,6 +27178,18 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-promise": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", @@ -21732,7 +27291,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" }, @@ -21789,6 +27347,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-upper-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-upper-case/-/is-upper-case-2.0.2.tgz", @@ -22073,6 +27643,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 +27958,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 +27978,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 +28018,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 +28247,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 +28333,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 +28365,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", @@ -22807,10 +28446,28 @@ "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=12" + "node": ">=12" + }, + "funding": { + "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/strip-ansi?sponsor=1" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/long": { @@ -23026,6 +28683,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 +28696,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 +28714,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 +28814,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 +28895,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 +29425,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 +29471,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" } @@ -23969,6 +29568,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -24076,9 +29687,9 @@ "license": "ISC" }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "dev": true, "license": "MIT" }, @@ -24247,32 +29858,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.5.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.5.0.tgz", + "integrity": "sha512-xSEmD4pLnV54t0NOUN16yCl7RIB1c5UUOse5HSyEXtBp+FgFQyPeDutc+Q2ZO7/22vImV7VfEjH/1zV2QuqvYw==", "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": "^2.0.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": { @@ -24337,9 +29948,9 @@ "license": "BSD-3-Clause" }, "node_modules/pino-abstract-transport/node_modules/readable-stream": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", - "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.6.0.tgz", + "integrity": "sha512-cbAdYt0VcnpN2Bekq7PU+k363ZRsPwJoEEJOEtSJQlJXzwaxt3FIo/uL+KeDSGIjJqtkwyge4KQgD2S2kd+CQw==", "dev": true, "license": "MIT", "dependencies": { @@ -24364,19 +29975,39 @@ } }, "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/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, "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", @@ -24559,6 +30190,21 @@ "node": ">=6.0.0" } }, + "node_modules/pretty-ms": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz", + "integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==", + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -24624,20 +30270,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", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true, - "license": "MIT" - }, "node_modules/pump": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", @@ -24671,13 +30303,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.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz", + "integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -24696,13 +30328,6 @@ "node": ">=0.4.x" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true, - "license": "MIT" - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -24763,16 +30388,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", @@ -25080,13 +30695,6 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "license": "ISC" }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true, - "license": "MIT" - }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -25133,55 +30741,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 +31386,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 +31424,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", @@ -26079,9 +31596,9 @@ } }, "node_modules/streamx": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.1.tgz", - "integrity": "sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==", + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.1.tgz", + "integrity": "sha512-PhP9wUnFLa+91CPy3N6tiQsK+gnYyUNuk15S3YG/zjYE7RuPeCjJngqnzpC31ow0lzBHQ+QGO4cNJnd0djYUsw==", "dev": true, "license": "MIT", "dependencies": { @@ -26518,9 +32035,9 @@ } }, "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.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -26534,9 +32051,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": { @@ -26591,6 +32108,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tldts": { + "version": "6.1.70", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.70.tgz", + "integrity": "sha512-/W1YVgYVJd9ZDjey5NXadNh0mJXkiUMUue9Zebd0vpdo1sU+H4zFFTaJ1RKD4N6KFoHfcXy6l+Vu7bh+bdWCzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.70" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.70", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.70.tgz", + "integrity": "sha512-RNnIXDB1FD4T9cpQRErEqw6ZpjLlGdMOitdV+0xtbsnwr4YFka1zpc7D4KD+aAn8oSG5JyFrdasZTE04qDE9Yg==", + "dev": true, + "license": "MIT" + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -26641,29 +32178,16 @@ "license": "MIT" }, "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" + "tldts": "^6.1.32" }, "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" + "node": ">=16" } }, "node_modules/tr46": { @@ -26681,6 +32205,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 +32380,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", @@ -27101,6 +32617,18 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/universal-user-agent": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", @@ -27211,17 +32739,6 @@ "querystring": "0.2.0" } }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "node_modules/url/node_modules/punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", @@ -27332,33 +32849,33 @@ } }, "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.5", + "resolved": "https://registry.npmjs.org/verdaccio/-/verdaccio-6.0.5.tgz", + "integrity": "sha512-hv+v4mtG/rcNidGUHXAtNuVySiPE3/PM+7dYye5jCDrhCUmRJYOtnvDe/Ym1ZE/twti39g6izVRxEkjnSp52gA==", "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.7", + "@verdaccio/auth": "8.0.0-next-8.7", + "@verdaccio/config": "8.0.0-next-8.7", + "@verdaccio/core": "8.0.0-next-8.7", "@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.7", + "@verdaccio/middleware": "8.0.0-next-8.7", + "@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.7", + "@verdaccio/ui-theme": "8.0.0-next-8.7", + "@verdaccio/url": "13.0.0-next-8.7", "@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.4.0", + "envinfo": "7.14.0", + "express": "4.21.2", "express-rate-limit": "5.5.1", "fast-safe-stringify": "2.1.1", "handlebars": "4.7.8", @@ -27370,18 +32887,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.7", + "verdaccio-htpasswd": "13.0.0-next-8.7" }, "bin": { "verdaccio": "bin/verdaccio" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { "type": "opencollective", @@ -27389,20 +32905,20 @@ } }, "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.7", + "resolved": "https://registry.npmjs.org/verdaccio-audit/-/verdaccio-audit-13.0.0-next-8.7.tgz", + "integrity": "sha512-kd6YdrDztkP1/GDZT7Ue2u41iGPvM9y+5aaUbIBUPvTY/YVv57K6MaCMfn9C/I+ZL4R7XOTSxTtWvz3JK4QrNg==", "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.7", + "@verdaccio/core": "8.0.0-next-8.7", + "express": "4.21.2", "https-proxy-agent": "5.0.1", "node-fetch": "cjs" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "type": "opencollective", @@ -27410,23 +32926,23 @@ } }, "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.7", + "resolved": "https://registry.npmjs.org/verdaccio-htpasswd/-/verdaccio-htpasswd-13.0.0-next-8.7.tgz", + "integrity": "sha512-znyFnwt59mLKTAu6eHJrfWP07iaHUlYiQN7QoBo8KMAOT1AecUYreBqs93oKHdIOzjTI8j6tQLg57DpeVS5vgg==", "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.7", + "@verdaccio/file-locking": "13.0.0-next-8.2", "apache-md5": "1.1.8", "bcryptjs": "2.4.3", "core-js": "3.37.1", - "debug": "4.3.7", + "debug": "4.4.0", "http-errors": "2.0.0", "unix-crypt-td-js": "1.1.4" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "type": "opencollective", @@ -27434,16 +32950,16 @@ } }, "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", @@ -27462,6 +32978,24 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/verdaccio-htpasswd/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/verdaccio/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -27469,17 +33003,22 @@ "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==", + "node_modules/verdaccio/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "license": "MIT", - "bin": { - "envinfo": "dist/cli.js" + "dependencies": { + "ms": "^2.1.3" }, "engines": { - "node": ">=4" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/verdaccio/node_modules/handlebars": { @@ -27900,10 +33439,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" } @@ -27994,6 +33537,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", + "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yup": { "version": "0.32.11", "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz", @@ -28039,53 +33594,62 @@ } }, "packages/ai-constructs": { - "version": "0.1.4", + "name": "@aws-amplify/ai-constructs", + "version": "1.2.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.3.0", + "@aws-amplify/plugin-types": "^1.6.0", "@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.4", + "typescript": "^5.0.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.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.1", "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.4", + "@aws-amplify/plugin-types": "^1.6.0", "@aws-sdk/util-arn-parser": "^3.568.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.0.0" } }, "packages/backend": { - "version": "1.2.1", + "name": "@aws-amplify/backend", + "version": "1.12.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/data-schema": "^1.0.0", - "@aws-amplify/platform-core": "^1.1.0", - "@aws-amplify/plugin-types": "^1.2.1", - "@aws-sdk/client-amplify": "^3.624.0", - "lodash.snakecase": "^4.1.1" + "@aws-amplify/backend-auth": "^1.4.2", + "@aws-amplify/backend-data": "^1.4.0", + "@aws-amplify/backend-function": "^1.11.0", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/backend-output-storage": "^1.1.4", + "@aws-amplify/backend-secret": "^1.1.4", + "@aws-amplify/backend-storage": "^1.2.4", + "@aws-amplify/client-config": "^1.5.5", + "@aws-amplify/data-schema": "^1.13.4", + "@aws-amplify/platform-core": "^1.5.0", + "@aws-amplify/plugin-types": "^1.7.0", + "@aws-sdk/client-amplify": "^3.624.0" }, "devDependencies": { "@types/aws-lambda": "^8.10.119", @@ -28093,96 +33657,346 @@ "aws-lambda": "^1.0.7" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.0.0" } }, "packages/backend-ai": { - "version": "0.1.1", + "name": "@aws-amplify/backend-ai", + "version": "1.3.0", "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.2.0", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/backend-output-storage": "^1.1.4", + "@aws-amplify/data-schema-types": "^1.2.0", + "@aws-amplify/platform-core": "^1.5.0", + "@aws-amplify/plugin-types": "^1.7.0" }, "peerDependencies": { - "@smithy/types": "^3.3.0", - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.0.0" } }, "packages/backend-auth": { - "version": "1.1.4", + "name": "@aws-amplify/backend-auth", + "version": "1.4.2", "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.1", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/backend-output-storage": "^1.1.4", + "@aws-amplify/plugin-types": "^1.6.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.7", + "@aws-amplify/platform-core": "^1.3.0", + "@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.168.0", "constructs": "^10.0.0" } }, "packages/backend-data": { - "version": "1.1.3", + "name": "@aws-amplify/backend-data", + "version": "1.4.0", "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.4", + "@aws-amplify/data-construct": "^1.14.5", + "@aws-amplify/data-schema-types": "^1.2.0", + "@aws-amplify/graphql-generator": "^0.5.1", + "@aws-amplify/plugin-types": "^1.7.0" }, "devDependencies": { - "@aws-amplify/backend-platform-test-stubs": "^0.3.4", - "@aws-amplify/data-schema": "^1.0.0", - "@aws-amplify/platform-core": "^1.0.7" + "@aws-amplify/backend-platform-test-stubs": "^0.3.7", + "@aws-amplify/data-schema": "^1.13.4", + "@aws-amplify/platform-core": "^1.5.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.0.0" } }, "packages/backend-deployer": { - "version": "1.1.2", + "name": "@aws-amplify/backend-deployer", + "version": "1.1.13", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/platform-core": "^1.0.6", - "@aws-amplify/plugin-types": "^1.2.1", - "execa": "^8.0.1", + "@aws-amplify/platform-core": "^1.5.0", + "@aws-amplify/plugin-types": "^1.7.0", + "execa": "^9.5.1", + "strip-ansi": "^6.0.1", "tsx": "^4.6.1" }, "peerDependencies": { - "aws-cdk": "^2.152.0", + "aws-cdk": "^2.168.0", "typescript": "^5.0.0" } }, + "packages/backend-deployer/node_modules/execa": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.1.tgz", + "integrity": "sha512-QY5PPtSonnGwhhHDNI7+3RvY285c7iuJFFB+lU+oEzMY/gEGJ808owqJsrr8Otd1E/x07po1LkUBmdAc5duPAg==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.3", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.0", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "packages/backend-deployer/node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/backend-deployer/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/backend-deployer/node_modules/human-signals": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.0.tgz", + "integrity": "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "packages/backend-deployer/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/backend-deployer/node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/backend-deployer/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/backend-deployer/node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/backend-function": { - "version": "1.4.0", + "name": "@aws-amplify/backend-function", + "version": "1.11.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", - "execa": "^8.0.1" + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/backend-output-storage": "^1.1.4", + "@aws-amplify/plugin-types": "^1.7.0", + "@aws-sdk/client-s3": "^3.624.0", + "execa": "^9.5.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.7", + "@aws-amplify/platform-core": "^1.5.0", + "@aws-sdk/client-s3": "^3.624.0", "@aws-sdk/client-ssm": "^3.624.0", "aws-sdk": "^2.1550.0", "uuid": "^9.0.1" }, - "peerDependencies": { - "aws-cdk-lib": "^2.152.0", - "constructs": "^10.0.0" + "peerDependencies": { + "aws-cdk-lib": "^2.168.0", + "constructs": "^10.0.0" + } + }, + "packages/backend-function/node_modules/execa": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.1.tgz", + "integrity": "sha512-QY5PPtSonnGwhhHDNI7+3RvY285c7iuJFFB+lU+oEzMY/gEGJ808owqJsrr8Otd1E/x07po1LkUBmdAc5duPAg==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.3", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.0", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "packages/backend-function/node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/backend-function/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/backend-function/node_modules/human-signals": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.0.tgz", + "integrity": "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "packages/backend-function/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/backend-function/node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/backend-function/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/backend-function/node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "packages/backend-function/node_modules/uuid": { @@ -28200,7 +34014,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 +34025,35 @@ } }, "packages/backend-output-storage": { - "version": "1.1.1", + "name": "@aws-amplify/backend-output-storage", + "version": "1.1.4", "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.3.0", + "@aws-amplify/plugin-types": "^1.6.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0" + "aws-cdk-lib": "^2.168.0" } }, "packages/backend-platform-test-stubs": { - "version": "0.3.4", + "name": "@aws-amplify/backend-platform-test-stubs", + "version": "0.3.7", "license": "Apache-2.0", "dependencies": { - "aws-cdk-lib": "^2.152.0", + "@aws-amplify/plugin-types": "^1.6.0", + "aws-cdk-lib": "^2.168.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 +34061,40 @@ } }, "packages/backend-storage": { - "version": "1.1.2", + "name": "@aws-amplify/backend-storage", + "version": "1.2.4", "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.4", + "@aws-amplify/plugin-types": "^1.6.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.7", + "@aws-amplify/platform-core": "^1.3.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.0.0" } }, "packages/cli": { - "version": "1.2.6", - "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", + "name": "@aws-amplify/backend-cli", + "version": "1.4.6", + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/backend-deployer": "^1.1.13", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/backend-secret": "^1.1.2", + "@aws-amplify/cli-core": "^1.2.1", + "@aws-amplify/client-config": "^1.5.5", + "@aws-amplify/deployed-backend-client": "^1.5.0", + "@aws-amplify/form-generator": "^1.0.3", + "@aws-amplify/model-generator": "^1.0.12", + "@aws-amplify/platform-core": "^1.5.0", + "@aws-amplify/plugin-types": "^1.7.0", + "@aws-amplify/sandbox": "^1.2.9", + "@aws-amplify/schema-generator": "^1.2.6", "@aws-sdk/client-amplify": "^3.624.0", "@aws-sdk/client-cloudformation": "^3.624.0", "@aws-sdk/client-s3": "^3.624.0", @@ -28281,7 +34104,7 @@ "@smithy/node-config-provider": "^2.1.3", "@smithy/shared-ini-file-loader": "^2.2.5", "envinfo": "^7.11.0", - "execa": "^8.0.1", + "execa": "^9.5.1", "is-ci": "^3.0.1", "open": "^9.1.0", "yargs": "^17.7.2", @@ -28303,15 +34126,134 @@ } }, "packages/cli-core": { - "version": "1.1.2", + "name": "@aws-amplify/cli-core", + "version": "1.2.1", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/platform-core": "^1.0.5", + "@aws-amplify/platform-core": "^1.3.0", "@inquirer/prompts": "^3.0.0", - "execa": "^8.0.1", + "execa": "^9.5.1", "kleur": "^4.1.5" } }, + "packages/cli-core/node_modules/execa": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.1.tgz", + "integrity": "sha512-QY5PPtSonnGwhhHDNI7+3RvY285c7iuJFFB+lU+oEzMY/gEGJ808owqJsrr8Otd1E/x07po1LkUBmdAc5duPAg==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.3", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.0", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "packages/cli-core/node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/cli-core/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/cli-core/node_modules/human-signals": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.0.tgz", + "integrity": "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "packages/cli-core/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/cli-core/node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/cli-core/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/cli-core/node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/cli/node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -28332,6 +34274,72 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, + "packages/cli/node_modules/execa": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.1.tgz", + "integrity": "sha512-QY5PPtSonnGwhhHDNI7+3RvY285c7iuJFFB+lU+oEzMY/gEGJ808owqJsrr8Otd1E/x07po1LkUBmdAc5duPAg==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.3", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.0", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "packages/cli/node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/cli/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/cli/node_modules/human-signals": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.0.tgz", + "integrity": "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "packages/cli/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -28341,6 +34349,46 @@ "node": ">=8" } }, + "packages/cli/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/cli/node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/cli/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/cli/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -28355,6 +34403,18 @@ "node": ">=8" } }, + "packages/cli/node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/cli/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -28409,13 +34469,15 @@ } }, "packages/client-config": { - "version": "1.3.0", + "name": "@aws-amplify/client-config", + "version": "1.5.5", "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/platform-core": "^1.0.7", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/deployed-backend-client": "^1.5.0", + "@aws-amplify/model-generator": "^1.0.12", + "@aws-amplify/platform-core": "^1.5.0", + "@aws-amplify/plugin-types": "^1.7.0", "zod": "^3.22.2" }, "devDependencies": { @@ -28430,13 +34492,13 @@ } }, "packages/create-amplify": { - "version": "1.0.5", + "version": "1.0.7", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/cli-core": "^1.1.1", - "@aws-amplify/platform-core": "^1.0.3", - "@aws-amplify/plugin-types": "^1.1.0", - "execa": "^8.0.1", + "@aws-amplify/cli-core": "^1.2.1", + "@aws-amplify/platform-core": "^1.3.0", + "@aws-amplify/plugin-types": "^1.6.0", + "execa": "^9.5.1", "kleur": "^4.1.5", "yargs": "^17.7.2" }, @@ -28453,27 +34515,133 @@ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "license": "ISC", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "packages/create-amplify/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "packages/create-amplify/node_modules/execa": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.1.tgz", + "integrity": "sha512-QY5PPtSonnGwhhHDNI7+3RvY285c7iuJFFB+lU+oEzMY/gEGJ808owqJsrr8Otd1E/x07po1LkUBmdAc5duPAg==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.3", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.0", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "packages/create-amplify/node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/create-amplify/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/create-amplify/node_modules/human-signals": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.0.tgz", + "integrity": "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "packages/create-amplify/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "packages/create-amplify/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/create-amplify/node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" }, "engines": { - "node": ">=12" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/create-amplify/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "packages/create-amplify/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "packages/create-amplify/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "packages/create-amplify/node_modules/string-width": { @@ -28490,6 +34658,18 @@ "node": ">=8" } }, + "packages/create-amplify/node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/create-amplify/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -28544,11 +34724,13 @@ } }, "packages/deployed-backend-client": { - "version": "1.4.0", + "name": "@aws-amplify/deployed-backend-client", + "version": "1.5.0", "license": "Apache-2.0", "dependencies": { "@aws-amplify/backend-output-schemas": "^1.2.0", - "@aws-amplify/platform-core": "^1.0.5", + "@aws-amplify/platform-core": "^1.4.0", + "@aws-amplify/plugin-types": "^1.2.2", "zod": "^3.22.2" }, "peerDependencies": { @@ -28559,6 +34741,7 @@ } }, "packages/eslint-rules": { + "name": "eslint-plugin-amplify-backend-rules", "version": "0.0.1", "license": "Apache-2.0", "dependencies": { @@ -28569,7 +34752,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 +34772,26 @@ } }, "packages/integration-tests": { - "version": "0.5.8", + "name": "@aws-amplify/integration-tests", + "version": "0.6.1", "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/data-schema": "^1.0.0", - "@aws-amplify/deployed-backend-client": "^1.3.0", - "@aws-amplify/platform-core": "^1.1.0", + "@aws-amplify/ai-constructs": "^1.1.0", + "@aws-amplify/auth-construct": "^1.5.1", + "@aws-amplify/backend": "^1.9.0", + "@aws-amplify/backend-ai": "^1.1.0", + "@aws-amplify/backend-secret": "^1.1.4", + "@aws-amplify/client-config": "^1.5.3", + "@aws-amplify/data-schema": "^1.13.4", + "@aws-amplify/deployed-backend-client": "^1.4.1", + "@aws-amplify/platform-core": "^1.3.0", + "@aws-amplify/plugin-types": "^1.6.0", "@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,11 +34802,12 @@ "@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.168.0", "constructs": "^10.0.0", - "execa": "^8.0.1", + "execa": "^9.5.1", "fs-extra": "^11.1.1", "glob": "^10.2.7", "graphql-tag": "^2.12.6", @@ -28627,9 +34815,136 @@ "node-fetch": "^3.3.2", "semver": "^7.6.3", "ssh2": "^1.15.0", + "strip-ansi": "^6.0.1", "uuid": "^9.0.1" } }, + "packages/integration-tests/node_modules/execa": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.1.tgz", + "integrity": "sha512-QY5PPtSonnGwhhHDNI7+3RvY285c7iuJFFB+lU+oEzMY/gEGJ808owqJsrr8Otd1E/x07po1LkUBmdAc5duPAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.3", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.0", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "packages/integration-tests/node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/integration-tests/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/integration-tests/node_modules/human-signals": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.0.tgz", + "integrity": "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "packages/integration-tests/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/integration-tests/node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/integration-tests/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/integration-tests/node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/integration-tests/node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -28645,14 +34960,16 @@ } }, "packages/model-generator": { - "version": "1.0.6", + "name": "@aws-amplify/model-generator", + "version": "1.0.12", "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.5.0", + "@aws-amplify/graphql-generator": "^0.5.1", "@aws-amplify/graphql-types-generator": "^3.6.0", - "@aws-amplify/platform-core": "^1.0.5", + "@aws-amplify/platform-core": "^1.5.0", + "@aws-amplify/plugin-types": "^1.7.0", "@aws-sdk/client-appsync": "^3.624.0", "@aws-sdk/client-s3": "^3.624.0", "@aws-sdk/credential-providers": "^3.624.0", @@ -28665,13 +34982,15 @@ } }, "packages/platform-core": { - "version": "1.1.0", + "name": "@aws-amplify/platform-core", + "version": "1.5.0", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/plugin-types": "^1.2.1", + "@aws-amplify/plugin-types": "^1.7.0", "@aws-sdk/client-sts": "^3.624.0", "is-ci": "^3.0.1", "lodash.mergewith": "^4.6.2", + "lodash.snakecase": "^4.1.1", "semver": "^7.6.3", "uuid": "^9.0.1", "zod": "^3.22.2" @@ -28680,6 +34999,10 @@ "@types/is-ci": "^3.0.4", "@types/lodash.mergewith": "^4.6.2", "@types/uuid": "9.0.7" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.168.0", + "constructs": "^10.0.0" } }, "packages/platform-core/node_modules/uuid": { @@ -28696,150 +35019,32 @@ } }, "packages/plugin-types": { - "version": "1.2.1", + "name": "@aws-amplify/plugin-types", + "version": "1.7.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.168.0", "constructs": "^10.0.0" } }, - "packages/plugin-types/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "packages/plugin-types/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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/plugin-types/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "packages/plugin-types/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/plugin-types/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" - } - }, - "packages/plugin-types/node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "packages/plugin-types/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/plugin-types/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" - }, - "packages/plugin-types/node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "packages/sandbox": { - "version": "1.2.1", + "name": "@aws-amplify/sandbox", + "version": "1.2.9", "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.13", + "@aws-amplify/backend-secret": "^1.1.2", + "@aws-amplify/cli-core": "^1.2.1", + "@aws-amplify/client-config": "^1.5.5", + "@aws-amplify/deployed-backend-client": "^1.5.0", + "@aws-amplify/platform-core": "^1.5.0", + "@aws-amplify/plugin-types": "^1.7.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,15 +35056,16 @@ "@types/parse-gitignore": "^1.0.0" }, "peerDependencies": { - "aws-cdk": "^2.152.0" + "aws-cdk": "^2.168.0" } }, "packages/schema-generator": { - "version": "1.2.2", + "name": "@aws-amplify/schema-generator", + "version": "1.2.6", "license": "Apache-2.0", "dependencies": { - "@aws-amplify/graphql-schema-generator": "^0.9.4", - "@aws-amplify/platform-core": "^1.0.5" + "@aws-amplify/graphql-schema-generator": "^0.11.0", + "@aws-amplify/platform-core": "^1.3.0" } } } diff --git a/package.json b/package.json index ff4526d10a9..064c496cf6d 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "test": "npm run test:dir $(tsx scripts/get_unit_test_dir_list.ts)", "test:coverage:generate": "NODE_V8_COVERAGE=coverage/ npm run test", "test:coverage:threshold": "c8 npm run test", - "test:dir": "tsx --test --test-reporter spec", + "test:dir": "tsx scripts/run_tests.ts", "test:scripts": "npm run test:dir $(glob --cwd=scripts --absolute **/*.test.ts)", "update:api": "tsx scripts/concurrent_workspace_script.ts update:api --if-present", "update:tsconfig-refs": "tsx scripts/update_tsconfig_refs.ts", @@ -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 95ab27f8e12..85b12404d02 100644 --- a/packages/ai-constructs/API.md +++ b/packages/ai-constructs/API.md @@ -6,29 +6,34 @@ /// +import { AIConversationOutput } from '@aws-amplify/backend-output-schemas'; +import { ApplicationLogLevel } from 'aws-cdk-lib/aws-lambda'; +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'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; 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 +44,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 +56,20 @@ 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; + timeoutSeconds?: number; + logging?: { + level?: ApplicationLogLevel; + retention?: RetentionDays; }; + outputStorageStrategy?: BackendOutputStorageStrategy; }; // @public (undocumented) type ConversationTurnEvent = { conversationId: string; currentMessageId: string; + streamResponse?: boolean; responseMutation: { name: string; inputTypeName: string; @@ -87,11 +87,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 fe7736ec0e9..3f125eb1905 100644 --- a/packages/ai-constructs/CHANGELOG.md +++ b/packages/ai-constructs/CHANGELOG.md @@ -1,5 +1,137 @@ # @aws-amplify/ai-constructs +## 1.2.1 + +### Patch Changes + +- d46024e: Log streaming progress +- Updated dependencies [a712983] + - @aws-amplify/platform-core@1.5.1 + +## 1.2.0 + +### Minor Changes + +- a66f5f2: Expose timeout property + +## 1.1.0 + +### Minor Changes + +- 65abf6a: Add options to control log settings + +### Patch Changes + +- 72b2fe0: update aws-cdk lib to ^2.168.0 +- Updated dependencies [cfdc854] +- Updated dependencies [72b2fe0] +- Updated dependencies [65abf6a] +- Updated dependencies [f6ba240] + - @aws-amplify/platform-core@1.3.0 + - @aws-amplify/plugin-types@1.6.0 + +## 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 693bcf24cde..55944d7ced9 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.2.1", "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.5.1", + "@aws-amplify/plugin-types": "^1.6.0", "@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.4", + "typescript": "^5.0.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.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 07cfa8404ad..fefa71aa53b 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,9 @@ 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'; +import { ApplicationLogLevel } from 'aws-cdk-lib/aws-lambda'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; void describe('Conversation Handler Function construct', () => { void it('creates handler with log group with JWT token redacting policy', () => { @@ -81,7 +84,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 +120,10 @@ void describe('Conversation Handler Function construct', () => { PolicyDocument: { Statement: [ { - Action: 'bedrock:InvokeModel', + Action: [ + 'bedrock:InvokeModel', + 'bedrock:InvokeModelWithResponseStream', + ], Effect: 'Allow', Resource: { 'Fn::Join': [ @@ -140,6 +149,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 +224,165 @@ 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')); + }); + }); + + void describe('timeout property', () => { + void it('sets valid timeout', () => { + const app = new App(); + const stack = new Stack(app); + new ConversationHandlerFunction(stack, 'conversationHandler', { + models: [], + timeoutSeconds: 124, + }); + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::Lambda::Function', { + Timeout: 124, + }); + }); + + void it('sets default timeout', () => { + const app = new App(); + const stack = new Stack(app); + new ConversationHandlerFunction(stack, 'conversationHandler', { + models: [], + }); + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::Lambda::Function', { + Timeout: 60, + }); + }); + + void it('throws on timeout below 1', () => { + assert.throws(() => { + const app = new App(); + const stack = new Stack(app); + new ConversationHandlerFunction(stack, 'conversationHandler', { + models: [], + timeoutSeconds: 0, + }); + }, new Error('timeoutSeconds must be a whole number between 1 and 900 inclusive')); + }); + + void it('throws on timeout above 15 minutes', () => { + assert.throws(() => { + const app = new App(); + const stack = new Stack(app); + new ConversationHandlerFunction(stack, 'conversationHandler', { + models: [], + timeoutSeconds: 60 * 15 + 1, + }); + }, new Error('timeoutSeconds must be a whole number between 1 and 900 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')); + }); + }); + + void describe('logging options', () => { + void it('sets log level', () => { + const app = new App(); + const stack = new Stack(app); + new ConversationHandlerFunction(stack, 'conversationHandler', { + models: [], + logging: { + level: ApplicationLogLevel.DEBUG, + }, + }); + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::Lambda::Function', { + LoggingConfig: { + ApplicationLogLevel: 'DEBUG', + LogFormat: 'JSON', + }, + }); + }); + + void it('sets log retention', () => { + const app = new App(); + const stack = new Stack(app); + new ConversationHandlerFunction(stack, 'conversationHandler', { + models: [], + logging: { + retention: RetentionDays.ONE_YEAR, + }, + }); + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::Logs::LogGroup', { + RetentionInDays: 365, + }); + }); + }); }); diff --git a/packages/ai-constructs/src/conversation/conversation_handler_construct.ts b/packages/ai-constructs/src/conversation/conversation_handler_construct.ts index 57b33f779f8..36c19c66d35 100644 --- a/packages/ai-constructs/src/conversation/conversation_handler_construct.ts +++ b/packages/ai-constructs/src/conversation/conversation_handler_construct.ts @@ -1,7 +1,16 @@ -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 { + ApplicationLogLevel, + CfnFunction, + Runtime as LambdaRuntime, + LoggingFormat, +} from 'aws-cdk-lib/aws-lambda'; import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; import { CustomDataIdentifier, @@ -11,6 +20,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 +35,36 @@ 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; + + /** + * An amount of time in seconds between 1 second and 15 minutes. + * Must be a whole number. + * Default is 60 seconds. + */ + timeoutSeconds?: number; + + logging?: { + level?: ApplicationLogLevel; + retention?: RetentionDays; + }; + + /** + * @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 +79,7 @@ export class ConversationHandlerFunction extends Construct implements ResourceProvider { + static readonly eventVersion: ConversationTurnEventVersion = '1.0'; resources: FunctionResources; /** @@ -53,22 +96,27 @@ 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`, { runtime: LambdaRuntime.NODEJS_18_X, - timeout: Duration.seconds(60), + timeout: Duration.seconds(this.resolveTimeout()), 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, + applicationLogLevelV2: this.props.logging?.level, logGroup: new LogGroup(this, 'conversationHandlerFunctionLogGroup', { - retention: RetentionDays.INFINITE, + retention: this.props.logging?.retention ?? RetentionDays.INFINITE, dataProtectionPolicy: new DataProtectionPolicy({ identifiers: [ new CustomDataIdentifier( @@ -91,7 +139,10 @@ export class ConversationHandlerFunction conversationHandler.addToRolePolicy( new PolicyStatement({ effect: Effect.ALLOW, - actions: ['bedrock:InvokeModel'], + actions: [ + 'bedrock:InvokeModel', + 'bedrock:InvokeModelWithResponseStream', + ], resources, }) ); @@ -105,5 +156,68 @@ 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; + }; + + private resolveTimeout = () => { + const timeoutMin = 1; + const timeoutMax = 60 * 15; // 15 minutes in seconds + const timeoutDefault = 60; + if (this.props.timeoutSeconds === undefined) { + return timeoutDefault; + } + + if ( + !isWholeNumberBetweenInclusive( + this.props.timeoutSeconds, + timeoutMin, + timeoutMax + ) + ) { + throw new Error( + `timeoutSeconds must be a whole number between ${timeoutMin} and ${timeoutMax} inclusive` + ); + } + return this.props.timeoutSeconds; + }; } + +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 439dd4067cc..fa1330a494e 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 12c08d5d0df..26d261b292c 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,919 @@ 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, [ + { + 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, + }, { - text: 'block1', + accumulatedTurnContent: [ + { + text: 'block1', + }, + { + text: 'b', + }, + ], + conversationId: event.conversationId, + associatedUserMessageId: event.currentMessageId, + contentBlockText: 'b', + contentBlockIndex: 1, + contentBlockDeltaIndex: 0, }, { - text: 'block2', + 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: 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, + 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('logs streaming progress sparsely', async () => { + const event: ConversationTurnEvent = { + ...commonEvent, }; - assert.deepStrictEqual(bedrockRequest3.input, expectedBedrockInput3); + + const bedrockClient = new BedrockRuntimeClient(); + const bedrockResponseQueue: Array< + ConverseCommandOutput | ConverseStreamCommandOutput + > = []; + const numberOfBlocks = 512; + const content = Array.from({ length: numberOfBlocks }, (v, k) => { + return { + text: `block${k}`, + }; + }); + const bedrockResponse = mockBedrockResponse(content, true); + bedrockResponse.$metadata.requestId = 'testRequestId'; + bedrockResponseQueue.push(bedrockResponse); + + mock.method(bedrockClient, 'send', () => + Promise.resolve(bedrockResponseQueue.shift()) + ); + + const consoleInfoMock = mock.fn<(data: string) => void>(); + const consoleErrorMock = mock.fn<(data: string) => void>(); + const consoleLogMock = mock.fn<(data: string) => void>(); + const consoleDebugMock = mock.fn<(data: string) => void>(); + const consoleMock = { + info: consoleInfoMock, + error: consoleErrorMock, + log: consoleLogMock, + debug: consoleDebugMock, + } as unknown as Console; + const adapter = new BedrockConverseAdapter( + event, + [], + bedrockClient, + undefined, + messageHistoryRetriever, + undefined, + consoleMock + ); + + await askBedrockWithStreaming(adapter); + + const progressCalls = consoleInfoMock.mock.calls.filter((call) => + call.arguments[0].includes('chunks from Bedrock Converse Stream response') + ); + assert.strictEqual(progressCalls.length, 3); + assert.strictEqual( + progressCalls[0].arguments[0], + 'Processed 1000 chunks from Bedrock Converse Stream response, requestId=testRequestId' + ); + assert.strictEqual( + progressCalls[1].arguments[0], + 'Processed 2000 chunks from Bedrock Converse Stream response, requestId=testRequestId' + ); + // each block is decomposed into 4 chunks + start and stop of whole message. + const expectedNumberOfAllChunks = numberOfBlocks * 4 + 2; + assert.strictEqual( + progressCalls[2].arguments[0], + `Completed processing ${expectedNumberOfAllChunks.toString()} chunks from Bedrock Converse Stream response, requestId=testRequestId` + ); }); void it('throws if tool is duplicated', () => { @@ -385,375 +1040,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: {}, + 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: {}, }, - // 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', - }, - }, - ], - }, - }, - 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 responseContent = await new BedrockConverseAdapter( - event, - [tool], - bedrockClient - ).askBedrock(); - - assert.deepStrictEqual( - responseContent, - finalBedrockResponse.output?.message?.content + // @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' ); - - 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 7c7a572387b..ae9b01118eb 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,230 @@ 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: [], + }; + + let processedBedrockChunks = 0; + try { + 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 ?? ''; + } + processedBedrockChunks++; + if (processedBedrockChunks % 1000 === 0) { + this.logger.info( + `Processed ${processedBedrockChunks} chunks from Bedrock Converse Stream response, requestId=${bedrockResponse.$metadata.requestId}` + ); + } + } + } finally { + this.logger.info( + `Completed processing ${processedBedrockChunks} chunks from Bedrock Converse Stream response, requestId=${bedrockResponse.$metadata.requestId}` + ); + } + 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 +401,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 +412,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 +423,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 00000000000..9215fc80da1 --- /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 00000000000..c98889522a7 --- /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 e9a06647500..8c42431b632 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 79f87a05cf4..9c5389f6109 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 c7201ca1b50..32c579b2374 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 9fe979aac87..5892b6747ca 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 00000000000..1d3063dd49b --- /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 12bea0403e3..df63abd33d7 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 c008e7905d3..19ff56dc561 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 9989429ef5d..d764dca5088 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 a6f9cce9492..dcd37368a31 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 00000000000..b5ae5f701e7 --- /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 00000000000..8b5186ba4d2 --- /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 00000000000..fe605b21711 --- /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 00000000000..60f1af44bb3 --- /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 72f1300087e..187d962b031 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 00000000000..7f5b2032ca1 --- /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 3cab0c9925e..3d95030cabb 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 00000000000..6309e8de953 --- /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 00000000000..a958b4eb94e --- /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 c07fe67565c..fd5619c21a0 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 4491409e236..038f571e2fa 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 d029196189e..be64fde07a9 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 6afb8647cbd..f3c895c6ce2 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 d41c584235c..57296dcb7aa 100644 --- a/packages/auth-construct/CHANGELOG.md +++ b/packages/auth-construct/CHANGELOG.md @@ -1,5 +1,56 @@ # @aws-amplify/auth-construct +## 1.5.1 + +### Patch Changes + +- 72b2fe0: update aws-cdk lib to ^2.168.0 +- Updated dependencies [72b2fe0] +- Updated dependencies [f6ba240] + - @aws-amplify/backend-output-storage@1.1.4 + - @aws-amplify/plugin-types@1.6.0 + +## 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 cbd26233a5a..340a0311956 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.1", "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.4", + "@aws-amplify/plugin-types": "^1.6.0", "@aws-sdk/util-arn-parser": "^3.568.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.0.0" } } diff --git a/packages/auth-construct/src/construct.test.ts b/packages/auth-construct/src/construct.test.ts index c2a268f7d27..d597c0222e9 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 6c033aaf5b8..7c94f9ad004 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 13af450f20c..85e3aa6c6c1 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 5083ffb73c4..c3d4ddbbeaa 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 623d8380f5e..9232acb5010 100644 --- a/packages/backend-ai/API.md +++ b/packages/backend-ai/API.md @@ -1,64 +1,93 @@ -## API Report File for "@aws-amplify/backend-ai" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts - -import { ConstructFactory } from '@aws-amplify/plugin-types'; -import { DocumentType } from '@smithy/types'; -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 { - DefineConversationHandlerFunctionProps, - defineConversationHandlerFunction - } -} -export { __export__conversation } - -declare namespace __export__conversation__runtime { - export { - ToolResultContentBlock, - ExecutableTool, - ConversationTurnEvent, - handleConversationTurnEvent - } -} -export { __export__conversation__runtime } - -// @public (undocumented) -type ConversationTurnEvent = runtime.ConversationTurnEvent; - -// @public -const defineConversationHandlerFunction: (props: DefineConversationHandlerFunctionProps) => ConstructFactory>; - -// @public (undocumented) -type DefineConversationHandlerFunctionProps = { - name: string; - entry?: string; - models: Array<{ - modelId: string | { - resourcePath: string; - }; - region?: string; - }>; -}; - -// @public (undocumented) -type ExecutableTool = runtime.ToolDefinition & { - execute: (input: DocumentType | undefined) => Promise; -}; - -// @public (undocumented) -const handleConversationTurnEvent: (event: ConversationTurnEvent, props?: { - tools?: Array; -}) => Promise; - -// @public (undocumented) -type ToolResultContentBlock = runtime.ToolResultContentBlock; - -// (No @packageDocumentation comment for this package) - -``` +## API Report File for "@aws-amplify/backend-ai" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { AiModel } from '@aws-amplify/data-schema-types'; +import { ConstructFactory } from '@aws-amplify/plugin-types'; +import { ConversationTurnEventVersion } from '@aws-amplify/ai-constructs/conversation'; +import { FunctionResources } from '@aws-amplify/plugin-types'; +import { LogLevel } from '@aws-amplify/plugin-types'; +import { LogRetention } 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, + ConversationHandlerFunctionLogLevel, + ConversationHandlerFunctionLogRetention, + ConversationHandlerFunctionLoggingOptions, + DefineConversationHandlerFunctionProps, + defineConversationHandlerFunction + } +} +export { __export__conversation } + +declare namespace __export__conversation__runtime { + export { + ToolResultContentBlock, + ExecutableTool, + ConversationTurnEvent, + handleConversationTurnEvent, + createExecutableTool + } +} +export { __export__conversation__runtime } + +// @public (undocumented) +type ConversationHandlerFunctionFactory = ConstructFactory> & { + readonly eventVersion: ConversationTurnEventVersion; +}; + +// @public (undocumented) +type ConversationHandlerFunctionLoggingOptions = { + retention?: ConversationHandlerFunctionLogRetention; + level?: ConversationHandlerFunctionLogLevel; +}; + +// @public (undocumented) +type ConversationHandlerFunctionLogLevel = Extract; + +// @public (undocumented) +type ConversationHandlerFunctionLogRetention = LogRetention; + +// @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) => ConversationHandlerFunctionFactory; + +// @public (undocumented) +type DefineConversationHandlerFunctionProps = { + name: string; + entry?: string; + models: Array<{ + modelId: string | AiModel; + region?: string; + }>; + memoryMB?: number; + timeoutSeconds?: number; + logging?: ConversationHandlerFunctionLoggingOptions; +}; + +// @public (undocumented) +type ExecutableTool> = runtime.ToolDefinition & { + execute: (input: TToolInput) => Promise; +}; + +// @public (undocumented) +const handleConversationTurnEvent: (event: ConversationTurnEvent, props?: { + tools?: Array>; +}) => Promise; + +// @public (undocumented) +type ToolResultContentBlock = runtime.ToolResultContentBlock; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/backend-ai/CHANGELOG.md b/packages/backend-ai/CHANGELOG.md index 6ea84e7fdad..80955f5eb1a 100644 --- a/packages/backend-ai/CHANGELOG.md +++ b/packages/backend-ai/CHANGELOG.md @@ -1,5 +1,150 @@ # @aws-amplify/backend-ai +## 1.3.0 + +### Minor Changes + +- a7506f9: added data logging api to defineData + +### Patch Changes + +- Updated dependencies [a7506f9] +- Updated dependencies [a7506f9] + - @aws-amplify/platform-core@1.5.0 + - @aws-amplify/plugin-types@1.7.0 + +## 1.2.0 + +### Minor Changes + +- a66f5f2: Expose timeout property + +### Patch Changes + +- Updated dependencies [a66f5f2] + - @aws-amplify/ai-constructs@1.2.0 + +## 1.1.0 + +### Minor Changes + +- 65abf6a: Add options to control log settings + +### Patch Changes + +- 72b2fe0: update aws-cdk lib to ^2.168.0 +- Updated dependencies [cfdc854] +- Updated dependencies [72b2fe0] +- Updated dependencies [65abf6a] +- Updated dependencies [f6ba240] + - @aws-amplify/platform-core@1.3.0 + - @aws-amplify/backend-output-storage@1.1.4 + - @aws-amplify/ai-constructs@1.1.0 + - @aws-amplify/plugin-types@1.6.0 + +## 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 d758fedfa3d..281755a8112 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.3.0", "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.2.0", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/backend-output-storage": "^1.1.4", + "@aws-amplify/data-schema-types": "^1.2.0", + "@aws-amplify/platform-core": "^1.5.0", + "@aws-amplify/plugin-types": "^1.7.0" }, "peerDependencies": { - "@smithy/types": "^3.3.0", - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.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 aa6f76fc7de..94fc6fefe48 100644 --- a/packages/backend-ai/src/conversation/factory.test.ts +++ b/packages/backend-ai/src/conversation/factory.test.ts @@ -15,6 +15,8 @@ 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'; +import { AmplifyError } from '@aws-amplify/platform-core'; const createStackAndSetContext = (): Stack => { const app = new App(); @@ -57,6 +59,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 +168,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 +189,105 @@ 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, + }); + }); + + void it('maps invalid memory error', () => { + const factory = defineConversationHandlerFunction({ + entry: './test-assets/with-default-entry/handler.ts', + name: 'testHandlerName', + models: [], + memoryMB: -1, + }); + assert.throws( + () => factory.getInstance(getInstanceProps), + (error: Error) => { + assert.ok(AmplifyError.isAmplifyError(error)); + assert.strictEqual(error.name, 'InvalidMemoryMBError'); + return true; + } + ); + }); + + void it('passes timeout setting to construct', () => { + const factory = defineConversationHandlerFunction({ + entry: './test-assets/with-default-entry/handler.ts', + name: 'testHandlerName', + models: [], + timeoutSeconds: 124, + }); + 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', { + Timeout: 124, + }); + }); + + void it('maps invalid timeout error', () => { + const factory = defineConversationHandlerFunction({ + entry: './test-assets/with-default-entry/handler.ts', + name: 'testHandlerName', + models: [], + timeoutSeconds: -1, + }); + assert.throws( + () => factory.getInstance(getInstanceProps), + (error: Error) => { + assert.ok(AmplifyError.isAmplifyError(error)); + assert.strictEqual(error.name, 'InvalidTimeoutError'); + return true; + } + ); + }); + + void it('passes log level to construct', () => { + const factory = defineConversationHandlerFunction({ + entry: './test-assets/with-default-entry/handler.ts', + name: 'testHandlerName', + models: [], + logging: { + level: 'debug', + }, + }); + 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', { + LoggingConfig: { + ApplicationLogLevel: 'DEBUG', + LogFormat: 'JSON', + }, + }); + }); + + void it('passes log retention to construct', () => { + const factory = defineConversationHandlerFunction({ + entry: './test-assets/with-default-entry/handler.ts', + name: 'testHandlerName', + models: [], + logging: { + retention: '1 day', + }, + }); + const lambda = factory.getInstance(getInstanceProps); + const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + template.resourceCountIs('AWS::Lambda::Function', 1); + template.hasResourceProperties('AWS::Logs::LogGroup', { + RetentionInDays: 1, + }); + }); }); diff --git a/packages/backend-ai/src/conversation/factory.ts b/packages/backend-ai/src/conversation/factory.ts index 0a095204dec..04d3a5dc179 100644 --- a/packages/backend-ai/src/conversation/factory.ts +++ b/packages/backend-ai/src/conversation/factory.ts @@ -1,31 +1,41 @@ +import { AIConversationOutput } from '@aws-amplify/backend-output-schemas'; import { - FunctionOutput, - functionOutputKey, -} from '@aws-amplify/backend-output-schemas'; -import { + AmplifyResourceGroupName, BackendOutputStorageStrategy, ConstructContainerEntryGenerator, ConstructFactory, ConstructFactoryGetInstanceProps, FunctionResources, GenerateContainerEntryProps, + LogLevel, + LogRetention, ResourceProvider, } from '@aws-amplify/plugin-types'; import { ConversationHandlerFunction, ConversationHandlerFunctionProps, + ConversationTurnEventVersion, } from '@aws-amplify/ai-constructs/conversation'; import path from 'path'; -import { CallerDirectoryExtractor } from '@aws-amplify/platform-core'; +import { + AmplifyUserError, + CallerDirectoryExtractor, +} from '@aws-amplify/platform-core'; +import { AiModel } from '@aws-amplify/data-schema-types'; +import { + LogLevelConverter, + LogRetentionConverter, +} from '@aws-amplify/platform-core/cdk'; 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 +53,64 @@ class ConversationHandlerFunctionGenerator region: model.region, }; }), + outputStorageStrategy: this.outputStorageStrategy, + memoryMB: this.props.memoryMB, + timeoutSeconds: this.props.timeoutSeconds, }; - const conversationHandlerFunction = new ConversationHandlerFunction( - scope, - this.props.name, - constructProps - ); - this.storeOutput(this.outputStorageStrategy, conversationHandlerFunction); - return conversationHandlerFunction; + const logging: typeof constructProps.logging = {}; + if (this.props.logging?.level) { + logging.level = new LogLevelConverter().toCDKLambdaApplicationLogLevel( + this.props.logging.level + ); + } + if (this.props.logging?.retention) { + logging.retention = new LogRetentionConverter().toCDKRetentionDays( + this.props.logging.retention + ); + } + constructProps.logging = logging; + try { + return new ConversationHandlerFunction( + scope, + this.props.name, + constructProps + ); + } catch (e) { + throw this.mapConstructErrors(e); + } }; - /** - * 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, - }, - }); + private mapConstructErrors = (e: unknown) => { + if (!(e instanceof Error)) { + return e; + } + if (e.message.startsWith('memoryMB must be')) { + return new AmplifyUserError('InvalidMemoryMBError', { + message: `Invalid memoryMB of ${this.props.memoryMB}`, + resolution: e.message, + }); + } + if (e.message.startsWith('timeoutSeconds must be')) { + return new AmplifyUserError('InvalidTimeoutError', { + message: `Invalid timeout of ${this.props.timeoutSeconds} seconds`, + resolution: e.message, + }); + } + return e; }; } -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( @@ -123,18 +159,38 @@ class ConversationHandlerFunctionFactory }; } +export type ConversationHandlerFunctionLogLevel = Extract< + LogLevel, + 'info' | 'debug' | 'warn' | 'error' | 'fatal' | 'trace' +>; + +export type ConversationHandlerFunctionLogRetention = LogRetention; + +export type ConversationHandlerFunctionLoggingOptions = { + retention?: ConversationHandlerFunctionLogRetention; + level?: ConversationHandlerFunctionLogLevel; +}; + 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; + /** + * An amount of time in seconds between 1 second and 15 minutes. + * Must be a whole number. + * Default is 60 seconds. + */ + timeoutSeconds?: number; + logging?: ConversationHandlerFunctionLoggingOptions; }; /** @@ -142,6 +198,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 dcf2f7c4419..69eb8d3c5cb 100644 --- a/packages/backend-ai/src/conversation/index.ts +++ b/packages/backend-ai/src/conversation/index.ts @@ -1,9 +1,17 @@ import { + ConversationHandlerFunctionFactory, + ConversationHandlerFunctionLogLevel, + ConversationHandlerFunctionLogRetention, + ConversationHandlerFunctionLoggingOptions, DefineConversationHandlerFunctionProps, defineConversationHandlerFunction, } from './factory.js'; export { + ConversationHandlerFunctionFactory, + ConversationHandlerFunctionLogLevel, + ConversationHandlerFunctionLogRetention, + ConversationHandlerFunctionLoggingOptions, DefineConversationHandlerFunctionProps, defineConversationHandlerFunction, }; diff --git a/packages/backend-ai/src/conversation/runtime/index.ts b/packages/backend-ai/src/conversation/runtime/index.ts index e758247893f..1b26bb981d6 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 dbde1fb5dbc..78143c71132 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 b3cf7a91cd2..6663c93e786 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 eac5836eccd..1566c93dfc1 100644 --- a/packages/backend-auth/CHANGELOG.md +++ b/packages/backend-auth/CHANGELOG.md @@ -1,5 +1,73 @@ # @aws-amplify/backend-auth +## 1.4.2 + +### Patch Changes + +- 72b2fe0: update aws-cdk lib to ^2.168.0 +- Updated dependencies [72b2fe0] +- Updated dependencies [f6ba240] + - @aws-amplify/backend-output-storage@1.1.4 + - @aws-amplify/auth-construct@1.5.1 + - @aws-amplify/plugin-types@1.6.0 + +## 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 bee451734b5..5976e3d6688 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.2", "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.1", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/backend-output-storage": "^1.1.4", + "@aws-amplify/plugin-types": "^1.6.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.7", + "@aws-amplify/platform-core": "^1.3.0", + "@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.168.0", "constructs": "^10.0.0" } } diff --git a/packages/backend-auth/src/factory.test.ts b/packages/backend-auth/src/factory.test.ts index 3865fdcec40..eb7ed5336e3 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 c5184cb7439..c0a4c63445f 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 90ce00ddc31..8a53f0225dd 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 00000000000..fa0db4e4223 --- /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 00000000000..81c50484aaf --- /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 00000000000..9f31a7302b8 --- /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 00000000000..8c852add5e7 --- /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 00000000000..939319a26f4 --- /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 00000000000..ee16e7317e0 --- /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 00000000000..81f4ff51b6c --- /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 00000000000..1ac099f65b8 --- /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 f4b20fff374..fad144ef6b1 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 8b7c018febb..46c199f1a11 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 b98614a8127..42a487d8e77 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/API.md b/packages/backend-data/API.md index ae6e71d8c66..acdda8ebd3f 100644 --- a/packages/backend-data/API.md +++ b/packages/backend-data/API.md @@ -9,6 +9,8 @@ import { AmplifyFunction } from '@aws-amplify/plugin-types'; import { ConstructFactory } from '@aws-amplify/plugin-types'; import { DerivedCombinedSchema } from '@aws-amplify/data-schema-types'; import { DerivedModelSchema } from '@aws-amplify/data-schema-types'; +import { LogLevel } from '@aws-amplify/plugin-types'; +import { LogRetention } from '@aws-amplify/plugin-types'; // @public export type ApiKeyAuthorizationModeProps = { @@ -24,6 +26,19 @@ export type AuthorizationModes = { oidcAuthorizationMode?: OIDCAuthorizationModeProps; }; +// @public +export type DataLogConfig = { + retention?: LogRetention; + excludeVerboseContent?: boolean; + fieldLogLevel?: DataLogLevel; +}; + +// @public +export type DataLoggingOptions = true | DataLogConfig; + +// @public (undocumented) +export type DataLogLevel = Extract; + // @public export type DataProps = { schema: DataSchemaInput; @@ -31,6 +46,7 @@ export type DataProps = { authorizationModes?: AuthorizationModes; functions?: Record>; stackMapping?: Record; + logging?: DataLoggingOptions; }; // @public diff --git a/packages/backend-data/CHANGELOG.md b/packages/backend-data/CHANGELOG.md index 85ba09de848..2201316d3ca 100644 --- a/packages/backend-data/CHANGELOG.md +++ b/packages/backend-data/CHANGELOG.md @@ -1,5 +1,95 @@ # @aws-amplify/backend-data +## 1.4.0 + +### Minor Changes + +- a7506f9: added data logging api to defineData + +### Patch Changes + +- Updated dependencies [a7506f9] + - @aws-amplify/plugin-types@1.7.0 + +## 1.3.0 + +### Minor Changes + +- fbf209e: Add GraphQL API ID and Amplify environment name to custom JS resolver stash + +### Patch Changes + +- 07fe7d4: Allow apiKeyAuthorizationMode to be undefined if defaultAuthorizationMode is apiKey + +## 1.2.3 + +### Patch Changes + +- f193105: Update getAmplifyDataClientConfig to work with named data backend + +## 1.2.2 + +### Patch Changes + +- 5cbe318: Add lambda data client +- 72b2fe0: update aws-cdk lib to ^2.168.0 +- Updated dependencies [72b2fe0] +- Updated dependencies [f6ba240] + - @aws-amplify/backend-output-storage@1.1.4 + - @aws-amplify/plugin-types@1.6.0 + +## 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 8d993abce73..1e83462ba51 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.4.0", "type": "module", "publishConfig": { "access": "public" @@ -19,19 +19,20 @@ }, "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/data-schema": "^1.13.4", + "@aws-amplify/backend-platform-test-stubs": "^0.3.7", + "@aws-amplify/platform-core": "^1.5.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.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.4", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/data-construct": "^1.14.5", + "@aws-amplify/data-schema-types": "^1.2.0", + "@aws-amplify/graphql-generator": "^0.5.1", + "@aws-amplify/plugin-types": "^1.7.0" } } diff --git a/packages/backend-data/src/app_sync_policy_generator.ts b/packages/backend-data/src/app_sync_policy_generator.ts index 9962d1922ea..5db2eaea3d7 100644 --- a/packages/backend-data/src/app_sync_policy_generator.ts +++ b/packages/backend-data/src/app_sync_policy_generator.ts @@ -14,7 +14,10 @@ export class AppSyncPolicyGenerator { /** * Initialize with the GraphqlAPI that the policies will be scoped to */ - constructor(private readonly graphqlApi: IGraphqlApi) { + constructor( + private readonly graphqlApi: IGraphqlApi, + private readonly modelIntrospectionSchemaArn?: string + ) { this.stack = Stack.of(graphqlApi); } /** @@ -29,13 +32,25 @@ export class AppSyncPolicyGenerator { .map((action) => actionToTypeMap[action]) // convert Type to resourceName .map((type) => [this.graphqlApi.arn, 'types', type, '*'].join('/')); - return new Policy(this.stack, `${this.policyPrefix}${this.policyCount++}`, { - statements: [ + + const statements = [ + new PolicyStatement({ + actions: ['appsync:GraphQL'], + resources, + }), + ]; + + if (this.modelIntrospectionSchemaArn) { + statements.push( new PolicyStatement({ - actions: ['appsync:GraphQL'], - resources, - }), - ], + actions: ['s3:GetObject'], + resources: [this.modelIntrospectionSchemaArn], + }) + ); + } + + return new Policy(this.stack, `${this.policyPrefix}${this.policyCount++}`, { + statements, }); } } diff --git a/packages/backend-data/src/assets/js_resolver_handler.ts b/packages/backend-data/src/assets/js_resolver_handler.ts index 282dfb725d6..81b26c4e1b4 100644 --- a/packages/backend-data/src/assets/js_resolver_handler.ts +++ b/packages/backend-data/src/assets/js_resolver_handler.ts @@ -1,7 +1,9 @@ /** * Pipeline resolver request handler */ -export const request = () => { +export const request = (ctx: Record>) => { + ctx.stash.awsAppsyncApiId = '${amplifyApiId}'; + ctx.stash.amplifyApiEnvironmentName = '${amplifyApiEnvironmentName}'; return {}; }; /** diff --git a/packages/backend-data/src/convert_authorization_modes.test.ts b/packages/backend-data/src/convert_authorization_modes.test.ts index f6854144491..1e361795743 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', @@ -86,6 +87,7 @@ void describe('convertAuthorizationModesToCDK', () => { defaultAuthorizationMode: 'API_KEY', apiKeyConfig: { expires: Duration.days(7), + description: undefined, }, iamConfig: { enableIamAuthorizationMode: true, @@ -102,6 +104,26 @@ void describe('convertAuthorizationModesToCDK', () => { ); }); + void it('defaults api key expiry if default auth mode is api key and apiKeyConfig is undefined', () => { + const expectedOutput: CDKAuthorizationModes = { + defaultAuthorizationMode: 'API_KEY', + apiKeyConfig: { + expires: Duration.days(7), + description: undefined, + }, + iamConfig: { + enableIamAuthorizationMode: true, + }, + }; + + assert.deepStrictEqual( + convertAuthorizationModesToCDK(getInstancePropsStub, undefined, { + defaultAuthorizationMode: 'apiKey', + }), + expectedOutput + ); + }); + void it('defaults to user pool auth if a user pool is present in provided auth resources', () => { const expectedOutput: CDKAuthorizationModes = { defaultAuthorizationMode: 'AMAZON_COGNITO_USER_POOLS', diff --git a/packages/backend-data/src/convert_authorization_modes.ts b/packages/backend-data/src/convert_authorization_modes.ts index fadfec4fbb5..fd03862cc54 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: @@ -59,7 +60,7 @@ export const buildConstructFactoryProvidedAuthConfig = ( const convertApiKeyAuthConfigToCDK = ({ description, expiresInDays = DEFAULT_API_KEY_EXPIRATION_DAYS, -}: ApiKeyAuthorizationModeProps): CDKApiKeyAuthorizationConfig => ({ +}: ApiKeyAuthorizationModeProps = {}): CDKApiKeyAuthorizationConfig => ({ description, expires: Duration.days(expiresInDays), }); @@ -206,9 +207,12 @@ export const convertAuthorizationModesToCDK = ( const cdkAuthorizationMode = convertAuthorizationModeToCDK( defaultAuthorizationMode ); - const apiKeyConfig = authModes?.apiKeyAuthorizationMode - ? convertApiKeyAuthConfigToCDK(authModes.apiKeyAuthorizationMode) - : computeApiKeyAuthFromResource(authResources, authModes); + const apiKeyConfig = + authModes?.apiKeyAuthorizationMode || + // If default auth mode is apiKey, don't require apiKeyAuthorizationMode to be defined + defaultAuthorizationMode === 'apiKey' + ? convertApiKeyAuthConfigToCDK(authModes?.apiKeyAuthorizationMode) + : computeApiKeyAuthFromResource(authResources, authModes); const userPoolConfig = computeUserPoolAuthFromResource(authResources); const identityPoolConfig = computeIdentityPoolAuthFromResource(authResources); const lambdaConfig = authModes?.lambdaAuthorizationMode diff --git a/packages/backend-data/src/convert_js_resolvers.test.ts b/packages/backend-data/src/convert_js_resolvers.test.ts index 51a646de6ff..19508de86ad 100644 --- a/packages/backend-data/src/convert_js_resolvers.test.ts +++ b/packages/backend-data/src/convert_js_resolvers.test.ts @@ -1,4 +1,4 @@ -import { Template } from 'aws-cdk-lib/assertions'; +import { Match, Template } from 'aws-cdk-lib/assertions'; import assert from 'node:assert'; import { beforeEach, describe, it } from 'node:test'; import { App, Duration, Stack } from 'aws-cdk-lib'; @@ -6,10 +6,15 @@ import { AmplifyData, AmplifyDataDefinition, } from '@aws-amplify/data-construct'; -import { resolve } from 'path'; -import { fileURLToPath } from 'url'; -import { convertJsResolverDefinition } from './convert_js_resolvers.js'; +import { join, resolve } from 'path'; +import { tmpdir } from 'os'; +import { fileURLToPath, pathToFileURL } from 'url'; +import { + convertJsResolverDefinition, + defaultJsResolverCode, +} from './convert_js_resolvers.js'; import { a } from '@aws-amplify/data-schema'; +import { writeFileSync } from 'node:fs'; // stub schema for the AmplifyApi construct // not relevant to this test suite @@ -28,6 +33,33 @@ const createStackAndSetContext = (): Stack => { return stack; }; +void describe('defaultJsResolverCode', () => { + void it('returns the default JS resolver code with api id and env name in valid JS', async () => { + const code = defaultJsResolverCode('testApiId', 'testEnvName'); + assert(code.includes("ctx.stash.awsAppsyncApiId = 'testApiId';")); + assert( + code.includes("ctx.stash.amplifyApiEnvironmentName = 'testEnvName';") + ); + + const tempDir = tmpdir(); + const filename = join(tempDir, 'js_resolver_handler.js'); + writeFileSync(filename, code); + + // windows requires dynamic imports to use file urls + const fileUrl = pathToFileURL(filename).href; + const resolver = await import(fileUrl); + const context = { stash: {}, prev: { result: 'result' } }; + assert.deepEqual(resolver.request(context), {}); + + // assert api id and env name are added to the context stash + assert.deepEqual(context.stash, { + awsAppsyncApiId: 'testApiId', + amplifyApiEnvironmentName: 'testEnvName', + }); + assert.equal(resolver.response(context), 'result'); + }); +}); + void describe('convertJsResolverDefinition', () => { let stack: Stack; let amplifyApi: AmplifyData; @@ -158,4 +190,52 @@ void describe('convertJsResolverDefinition', () => { template.resourceCountIs('AWS::AppSync::Resolver', 1); }); + + void it('adds api id and environment name to stash', () => { + const absolutePath = resolve( + fileURLToPath(import.meta.url), + '../../lib/assets', + 'js_resolver_handler.js' + ); + + const schema = a.schema({ + customQuery: a + .query() + .authorization((allow) => allow.publicApiKey()) + .returns(a.string()) + .handler( + a.handler.custom({ + entry: absolutePath, + }) + ), + }); + const { jsFunctions } = schema.transform(); + convertJsResolverDefinition(stack, amplifyApi, jsFunctions); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::AppSync::Resolver', { + Runtime: { + Name: 'APPSYNC_JS', + RuntimeVersion: '1.0.0', + }, + Kind: 'PIPELINE', + TypeName: 'Query', + FieldName: 'customQuery', + Code: { + 'Fn::Join': [ + '', + [ + "/**\n * Pipeline resolver request handler\n */\nexport const request = (ctx) => {\n ctx.stash.awsAppsyncApiId = '", + { + 'Fn::GetAtt': [ + Match.stringLikeRegexp('amplifyDataGraphQLAPI.*'), + 'ApiId', + ], + }, + "';\n ctx.stash.amplifyApiEnvironmentName = 'NONE';\n return {};\n};\n/**\n * Pipeline resolver response handler\n */\nexport const response = (ctx) => {\n return ctx.prev.result;\n};\n", + ], + ], + }, + }); + }); }); diff --git a/packages/backend-data/src/convert_js_resolvers.ts b/packages/backend-data/src/convert_js_resolvers.ts index e4d23eec33e..5117a1135e6 100644 --- a/packages/backend-data/src/convert_js_resolvers.ts +++ b/packages/backend-data/src/convert_js_resolvers.ts @@ -4,6 +4,7 @@ import { CfnFunctionConfiguration, CfnResolver } from 'aws-cdk-lib/aws-appsync'; import { JsResolver } from '@aws-amplify/data-schema-types'; import { resolve } from 'path'; import { fileURLToPath } from 'node:url'; +import { readFileSync } from 'fs'; import { Asset } from 'aws-cdk-lib/aws-s3-assets'; import { resolveEntryPath } from './resolve_entry_path.js'; @@ -18,17 +19,25 @@ const JS_PIPELINE_RESOLVER_HANDLER = './assets/js_resolver_handler.js'; * It's required for defining a pipeline resolver. The only purpose it serves is returning the output of the last function in the pipeline back to the client. * * Customer-provided handlers are added as a Functions list in `pipelineConfig.functions` + * + * Add Amplify API ID and environment name to the context stash for use in the customer-provided handlers. */ -const defaultJsResolverAsset = (scope: Construct): Asset => { +export const defaultJsResolverCode = ( + amplifyApiId: string, + amplifyApiEnvironmentName: string +): string => { const resolvedTemplatePath = resolve( fileURLToPath(import.meta.url), '../../lib', JS_PIPELINE_RESOLVER_HANDLER ); - return new Asset(scope, 'default_js_resolver_handler_asset', { - path: resolveEntryPath(resolvedTemplatePath), - }); + return readFileSync(resolvedTemplatePath, 'utf-8') + .replace(new RegExp(/\$\{amplifyApiId\}/, 'g'), amplifyApiId) + .replace( + new RegExp(/\$\{amplifyApiEnvironmentName\}/, 'g'), + amplifyApiEnvironmentName + ); }; /** @@ -44,8 +53,6 @@ export const convertJsResolverDefinition = ( return; } - const jsResolverTemplateAsset = defaultJsResolverAsset(scope); - for (const resolver of jsResolvers) { const functions: string[] = resolver.handlers.map((handler, idx) => { const fnName = `Fn_${resolver.typeName}_${resolver.fieldName}_${idx + 1}`; @@ -71,12 +78,15 @@ export const convertJsResolverDefinition = ( const resolverName = `Resolver_${resolver.typeName}_${resolver.fieldName}`; + const amplifyApiEnvironmentName = + scope.node.tryGetContext('amplifyEnvironmentName') ?? 'NONE'; new CfnResolver(scope, resolverName, { apiId: amplifyApi.apiId, fieldName: resolver.fieldName, typeName: resolver.typeName, kind: APPSYNC_PIPELINE_RESOLVER, - codeS3Location: jsResolverTemplateAsset.s3ObjectUrl, + // Uses synth-time inline code to avoid circular dependency when adding the API ID as an environment variable. + code: defaultJsResolverCode(amplifyApi.apiId, amplifyApiEnvironmentName), runtime: { name: APPSYNC_JS_RUNTIME_NAME, runtimeVersion: APPSYNC_JS_RUNTIME_VERSION, diff --git a/packages/backend-data/src/factory.test.ts b/packages/backend-data/src/factory.test.ts index 526409bc898..55031b1ed88 100644 --- a/packages/backend-data/src/factory.test.ts +++ b/packages/backend-data/src/factory.test.ts @@ -38,7 +38,10 @@ import { import { AmplifyDataResources } from '@aws-amplify/data-construct'; import { AmplifyUserError } from '@aws-amplify/platform-core'; import { a } from '@aws-amplify/data-schema'; -import { AmplifyDataError } from './types.js'; +import { AmplifyDataError, DataLoggingOptions } from './types.js'; +import { CDKLoggingOptions } from './logging_options_parser.js'; +import { CfnGraphQLApi, FieldLogLevel } from 'aws-cdk-lib/aws-appsync'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; const CUSTOM_DDB_CFN_TYPE = 'Custom::AmplifyDynamoDBTable'; @@ -100,6 +103,7 @@ const createConstructContainerWithUserPoolAuthRegistered = ( ), }, groups: {}, + identityPoolId: 'identityPool', }, }), }); @@ -535,25 +539,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 +550,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 +561,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/*', ], @@ -617,6 +570,23 @@ void describe('DataFactory', () => { }, ], }, + { + Action: 's3:GetObject', + Resource: { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + 'modelIntrospectionSchemaBucketF566B665', + 'Arn', + ], + }, + '/modelIntrospectionSchema.json', + ], + ], + }, + }, ], }, Roles: [ @@ -717,24 +687,27 @@ void describe('DataFactory', () => { 'Fn::Join': [ '', [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':appsync:', { - Ref: 'AWS::Region', + 'Fn::GetAtt': ['amplifyDataGraphQLAPI42A6FA33', 'Arn'], }, - ':', - { - Ref: 'AWS::AccountId', - }, - // eslint-disable-next-line spellcheck/spell-checker - ':apis/', + '/types/Mutation/*', + ], + ], + }, + }, + { + Action: 's3:GetObject', + Resource: { + 'Fn::Join': [ + '', + [ { - 'Fn::GetAtt': ['amplifyDataGraphQLAPI42A6FA33', 'ApiId'], + 'Fn::GetAtt': [ + 'modelIntrospectionSchemaBucketF566B665', + 'Arn', + ], }, - '/types/Mutation/*', + '/modelIntrospectionSchema.json', ], ], }, @@ -757,24 +730,27 @@ void describe('DataFactory', () => { 'Fn::Join': [ '', [ - 'arn:', { - Ref: 'AWS::Partition', + 'Fn::GetAtt': ['amplifyDataGraphQLAPI42A6FA33', 'Arn'], }, - ':appsync:', - { - Ref: 'AWS::Region', - }, - ':', - { - Ref: 'AWS::AccountId', - }, - // eslint-disable-next-line spellcheck/spell-checker - ':apis/', + '/types/Query/*', + ], + ], + }, + }, + { + Action: 's3:GetObject', + Resource: { + 'Fn::Join': [ + '', + [ { - 'Fn::GetAtt': ['amplifyDataGraphQLAPI42A6FA33', 'ApiId'], + 'Fn::GetAtt': [ + 'modelIntrospectionSchemaBucketF566B665', + 'Arn', + ], }, - '/types/Query/*', + '/modelIntrospectionSchema.json', ], ], }, @@ -829,6 +805,144 @@ void describe('Destructive Schema Updates & Replace tables upon GSI updates', () }); }); +void describe('Logging Options', () => { + let stack: Stack; + let constructContainer: ConstructContainer; + let outputStorageStrategy: BackendOutputStorageStrategy; + let importPathVerifier: ImportPathVerifierStub; + let resourceNameValidator: ResourceNameValidatorStub; + let getInstanceProps: ConstructFactoryGetInstanceProps; + + const DEFAULT_LOGGING_OPTIONS: CDKLoggingOptions = { + excludeVerboseContent: true, + fieldLogLevel: FieldLogLevel.NONE, + retention: RetentionDays.ONE_WEEK, + }; + + const testCases: { + description: string; + input: DataLoggingOptions | undefined; + expectedOutput: CDKLoggingOptions | undefined; + }[] = [ + { + description: 'no logging options provided', + input: undefined, + expectedOutput: undefined, + }, + { + description: 'default - logging: true', + input: true, + expectedOutput: DEFAULT_LOGGING_OPTIONS, + }, + { + description: 'default - logging: {}', + input: {}, + expectedOutput: DEFAULT_LOGGING_OPTIONS, + }, + { + description: 'custom - excludeVerboseContent: false', + input: { excludeVerboseContent: false }, + expectedOutput: { + ...DEFAULT_LOGGING_OPTIONS, + excludeVerboseContent: false, + }, + }, + { + description: 'custom - fieldLogLevel: error', + input: { fieldLogLevel: 'error' }, + expectedOutput: { + ...DEFAULT_LOGGING_OPTIONS, + fieldLogLevel: FieldLogLevel.ERROR, + }, + }, + { + description: 'custom - fieldLogLevel: info, retention: 1 month', + input: { fieldLogLevel: 'info', retention: '1 month' }, + expectedOutput: { + ...DEFAULT_LOGGING_OPTIONS, + fieldLogLevel: FieldLogLevel.INFO, + retention: RetentionDays.ONE_MONTH, + }, + }, + { + description: + 'custom - excludeVerboseContent: false, level: debug, retention: 13 months', + input: { + excludeVerboseContent: false, + fieldLogLevel: 'debug', + retention: '13 months', + }, + expectedOutput: { + excludeVerboseContent: false, + fieldLogLevel: FieldLogLevel.DEBUG, + retention: RetentionDays.THIRTEEN_MONTHS, + }, + }, + ]; + + beforeEach(() => { + resetFactoryCount(); + stack = createStackAndSetContext({ isSandboxMode: true }); + + constructContainer = + createConstructContainerWithUserPoolAuthRegistered(stack); + outputStorageStrategy = new StackMetadataBackendOutputStorageStrategy( + stack + ); + importPathVerifier = new ImportPathVerifierStub(); + resourceNameValidator = new ResourceNameValidatorStub(); + + getInstanceProps = { + constructContainer, + outputStorageStrategy, + importPathVerifier, + resourceNameValidator, + }; + }); + + testCases.forEach((testCase) => { + void it(`${testCase.description}`, () => { + const dataFactory = defineData({ + schema: testSchema, + name: 'testLoggingOptions', + logging: testCase.input, + }); + const dataConstruct = dataFactory.getInstance(getInstanceProps); + const template = Template.fromStack( + Stack.of(dataConstruct.resources.graphqlApi) + ); + + if (testCase.expectedOutput) { + const createdLogConfig = dataConstruct.resources.cfnResources + .cfnGraphqlApi.logConfig as CfnGraphQLApi.LogConfigProperty; + assert.ok(createdLogConfig, 'logConfig should be defined'); + assert.strictEqual( + createdLogConfig.fieldLogLevel, + testCase.expectedOutput.fieldLogLevel + ); + assert.strictEqual( + createdLogConfig.excludeVerboseContent, + testCase.expectedOutput.excludeVerboseContent + ); + + template.hasResourceProperties('Custom::LogRetention', { + RetentionInDays: testCase.expectedOutput.retention, + }); + } else { + const createdLogConfig = dataConstruct.resources.cfnResources + .cfnGraphqlApi.logConfig as CfnGraphQLApi.LogConfigProperty; + assert.strictEqual(createdLogConfig, undefined); + + template.resourcePropertiesCountIs( + 'Custom::LogRetention', + 'LogRetention', + 0 + ); + } + }); + }); +}); + const resetFactoryCount = () => { DataFactory.factoryCount = 0; }; diff --git a/packages/backend-data/src/factory.ts b/packages/backend-data/src/factory.ts index 5eac5ffd016..6ed36ace85c 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 { @@ -16,6 +18,7 @@ import { TranslationBehavior, } from '@aws-amplify/data-construct'; import { GraphqlOutput } from '@aws-amplify/backend-output-schemas'; +import { generateModelsSync } from '@aws-amplify/graphql-generator'; import * as path from 'path'; import { AmplifyDataError, DataProps } from './types.js'; import { @@ -38,13 +41,18 @@ import { CDKContextKey, TagName, } from '@aws-amplify/platform-core'; -import { Aspects, IAspect, Tags } from 'aws-cdk-lib'; +import { Aspects, IAspect, RemovalPolicy, Tags } from 'aws-cdk-lib'; import { convertJsResolverDefinition } from './convert_js_resolvers.js'; import { AppSyncPolicyGenerator } from './app_sync_policy_generator.js'; import { FunctionSchemaAccess, JsResolver, } from '@aws-amplify/data-schema-types'; +import { Bucket } from 'aws-cdk-lib/aws-s3'; +import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment'; +import { convertLoggingOptionsToCDK } from './logging_options_parser.js'; +const modelIntrospectionSchemaKey = 'modelIntrospectionSchema.json'; +const defaultName = 'amplifyData'; /** * Singleton factory for AmplifyGraphqlApi constructs that can be used in Amplify project files. @@ -97,9 +105,9 @@ export class DataFactory implements ConstructFactory { this.props, buildConstructFactoryProvidedAuthConfig( props.constructContainer - .getConstructFactory>( - 'AuthResources' - ) + .getConstructFactory< + ResourceProvider + >('AuthResources') ?.getInstance(props) ), props, @@ -111,7 +119,7 @@ export class DataFactory implements ConstructFactory { } class DataGenerator implements ConstructContainerEntryGenerator { - readonly resourceGroupName = 'data'; + readonly resourceGroupName: AmplifyResourceGroupName = 'data'; private readonly name: string; constructor( @@ -120,7 +128,7 @@ class DataGenerator implements ConstructContainerEntryGenerator { private readonly getInstanceProps: ConstructFactoryGetInstanceProps, private readonly outputStorageStrategy: BackendOutputStorageStrategy ) { - this.name = props.name ?? 'amplifyData'; + this.name = props.name ?? defaultName; } generateContainerEntry = ({ @@ -184,7 +192,7 @@ class DataGenerator implements ConstructContainerEntryGenerator { this.props.authorizationModes ); } catch (error) { - if (error instanceof AmplifyError) { + if (AmplifyError.isAmplifyError(error)) { throw error; } throw new AmplifyUserError( @@ -231,14 +239,25 @@ class DataGenerator implements ConstructContainerEntryGenerator { ...schemasLambdaFunctions, }); let amplifyApi = undefined; + let modelIntrospectionSchema: string | undefined = undefined; const isSandboxDeployment = scope.node.tryGetContext(CDKContextKey.DEPLOYMENT_TYPE) === 'sandbox'; + const cdkLoggingOptions = convertLoggingOptionsToCDK( + this.props.logging ?? undefined + ); + try { + const combinedSchema = combineCDKSchemas(amplifyGraphqlDefinitions); + modelIntrospectionSchema = generateModelsSync({ + schema: combinedSchema.schema, + target: 'introspection', + })['model-introspection.json']; + amplifyApi = new AmplifyData(scope, this.name, { apiName: this.name, - definition: combineCDKSchemas(amplifyGraphqlDefinitions), + definition: combinedSchema, authorizationModes, outputStorageStrategy: this.outputStorageStrategy, functionNameMap, @@ -252,6 +271,7 @@ class DataGenerator implements ConstructContainerEntryGenerator { allowDestructiveGraphqlSchemaUpdates: true, _provisionHotswapFriendlyResources: isSandboxDeployment, }, + logging: cdkLoggingOptions, }); } catch (error) { throw new AmplifyUserError( @@ -264,6 +284,24 @@ class DataGenerator implements ConstructContainerEntryGenerator { ); } + const modelIntrospectionSchemaBucket = new Bucket( + scope, + 'modelIntrospectionSchemaBucket', + { + enforceSSL: true, + autoDeleteObjects: true, + removalPolicy: RemovalPolicy.DESTROY, + } + ); + new BucketDeployment(scope, 'modelIntrospectionSchemaBucketDeployment', { + // See https://github.com/aws-amplify/amplify-category-api/pull/1939 + memoryLimit: 1536, + destinationBucket: modelIntrospectionSchemaBucket, + sources: [ + Source.data(modelIntrospectionSchemaKey, modelIntrospectionSchema), + ], + }); + Tags.of(amplifyApi).add(TagName.FRIENDLY_NAME, this.name); /**; @@ -276,14 +314,37 @@ class DataGenerator implements ConstructContainerEntryGenerator { convertJsResolverDefinition(scope, amplifyApi, schemasJsFunctions); + const namePrefix = this.name === defaultName ? '' : defaultName; + + const ssmEnvironmentScopeContext = { + [`${namePrefix}${this.name}_GRAPHQL_ENDPOINT`]: + amplifyApi.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl, + [`${namePrefix}${this.name}_MODEL_INTROSPECTION_SCHEMA_BUCKET_NAME`]: + modelIntrospectionSchemaBucket.bucketName, + [`${namePrefix}${this.name}_MODEL_INTROSPECTION_SCHEMA_KEY`]: + modelIntrospectionSchemaKey, + ['AMPLIFY_DATA_DEFAULT_NAME']: `${namePrefix}${this.name}`, + }; + + const backwardsCompatibleScopeContext = + `${this.name}_GRAPHQL_ENDPOINT` !== + `${namePrefix}${this.name}_GRAPHQL_ENDPOINT` + ? { + // @deprecated + [`${this.name}_GRAPHQL_ENDPOINT`]: + amplifyApi.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl, + } + : {}; + const ssmEnvironmentEntries = ssmEnvironmentEntriesGenerator.generateSsmEnvironmentEntries({ - [`${this.name}_GRAPHQL_ENDPOINT`]: - amplifyApi.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl, + ...ssmEnvironmentScopeContext, + ...backwardsCompatibleScopeContext, }); const policyGenerator = new AppSyncPolicyGenerator( - amplifyApi.resources.graphqlApi + amplifyApi.resources.graphqlApi, + `${modelIntrospectionSchemaBucket.bucketArn}/${modelIntrospectionSchemaKey}` ); schemasFunctionSchemaAccess.forEach((accessDefinition) => { diff --git a/packages/backend-data/src/index.ts b/packages/backend-data/src/index.ts index b882c0c7126..7929c6a3da6 100644 --- a/packages/backend-data/src/index.ts +++ b/packages/backend-data/src/index.ts @@ -7,4 +7,7 @@ export { AuthorizationModes, DataSchemaInput, DataProps, + DataLogConfig, + DataLoggingOptions, + DataLogLevel, } from './types.js'; diff --git a/packages/backend-data/src/logging_options_parser.test.ts b/packages/backend-data/src/logging_options_parser.test.ts new file mode 100644 index 00000000000..11290965257 --- /dev/null +++ b/packages/backend-data/src/logging_options_parser.test.ts @@ -0,0 +1,87 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { DataLoggingOptions } from './types.js'; +import { + CDKLoggingOptions, + convertLoggingOptionsToCDK, +} from './logging_options_parser.js'; +import { FieldLogLevel } from 'aws-cdk-lib/aws-appsync'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; + +type TestCase = { + description: string; + input: DataLoggingOptions | undefined; + expectedOutput: CDKLoggingOptions | undefined; +}; + +const DEFAULT_LOGGING_OPTIONS = { + excludeVerboseContent: true, + fieldLogLevel: FieldLogLevel.NONE, + retention: RetentionDays.ONE_WEEK, +}; + +const testCases: Array = [ + { + description: 'no logging options provided', + input: undefined, + expectedOutput: undefined, + }, + { + description: 'default - logging: true', + input: true, + expectedOutput: DEFAULT_LOGGING_OPTIONS, + }, + { + description: 'default - logging: {}', + input: {}, + expectedOutput: DEFAULT_LOGGING_OPTIONS, + }, + { + description: 'custom - excludeVerboseContent: false', + input: { excludeVerboseContent: false }, + expectedOutput: { + ...DEFAULT_LOGGING_OPTIONS, + excludeVerboseContent: false, + }, + }, + { + description: 'custom - fieldLogLevel: error', + input: { fieldLogLevel: 'error' }, + expectedOutput: { + ...DEFAULT_LOGGING_OPTIONS, + fieldLogLevel: FieldLogLevel.ERROR, + }, + }, + { + description: 'custom - fieldLogLevel: info, retention: 1 month', + input: { fieldLogLevel: 'info', retention: '1 month' }, + expectedOutput: { + ...DEFAULT_LOGGING_OPTIONS, + fieldLogLevel: FieldLogLevel.INFO, + retention: RetentionDays.ONE_MONTH, + }, + }, + { + description: + 'custom - excludeVerboseContent: false, level: debug, retention: 13 months', + input: { + excludeVerboseContent: false, + fieldLogLevel: 'debug', + retention: '13 months', + }, + expectedOutput: { + excludeVerboseContent: false, + fieldLogLevel: FieldLogLevel.DEBUG, + retention: RetentionDays.THIRTEEN_MONTHS, + }, + }, +]; + +void describe('LoggingOptions converter', () => { + testCases.forEach((testCase) => { + void it(`${testCase.description}`, () => { + const convertedOptions = convertLoggingOptionsToCDK(testCase.input); + assert.deepStrictEqual(convertedOptions, testCase.expectedOutput); + }); + }); +}); diff --git a/packages/backend-data/src/logging_options_parser.ts b/packages/backend-data/src/logging_options_parser.ts new file mode 100644 index 00000000000..ce0cd995d69 --- /dev/null +++ b/packages/backend-data/src/logging_options_parser.ts @@ -0,0 +1,65 @@ +import { DataLogLevel, DataLoggingOptions } from './types.js'; +import { + LogLevelConverter, + LogRetentionConverter, +} from '@aws-amplify/platform-core/cdk'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { FieldLogLevel } from 'aws-cdk-lib/aws-appsync'; +import { LogRetention } from '@aws-amplify/plugin-types'; + +export type CDKLoggingOptions = { + excludeVerboseContent: boolean; + fieldLogLevel: FieldLogLevel; + retention: RetentionDays; +}; + +const DEFAULT_EXCLUDE_VERBOSE_CONTENT: boolean = true; +const DEFAULT_LEVEL: DataLogLevel = 'none'; +const DEFAULT_RETENTION: LogRetention = '1 week'; + +/** + * Converts logging options to CDK. + */ +export const convertLoggingOptionsToCDK = ( + loggingOptions: DataLoggingOptions | undefined +): CDKLoggingOptions | undefined => { + if (!loggingOptions) { + return undefined; + } + + // Determine if we should apply default configuration + const shouldApplyDefaultLogging = + loggingOptions === true || + (typeof loggingOptions === 'object' && + Object.keys(loggingOptions).length === 0); + + // Extract fields from the user's loggingOptions (if it's an object) + const config: DataLoggingOptions = + typeof loggingOptions === 'object' ? loggingOptions : {}; + + const excludeVerboseContent = shouldApplyDefaultLogging + ? DEFAULT_EXCLUDE_VERBOSE_CONTENT + : config.excludeVerboseContent ?? DEFAULT_EXCLUDE_VERBOSE_CONTENT; + + // For level and retention, we rely on converters. If config is empty or logging is true, use defaults. + const dataLogLevel = shouldApplyDefaultLogging + ? DEFAULT_LEVEL + : config.fieldLogLevel ?? DEFAULT_LEVEL; + + const logRetention = shouldApplyDefaultLogging + ? DEFAULT_RETENTION + : config.retention ?? DEFAULT_RETENTION; + + const fieldLogLevel = new LogLevelConverter().toCDKAppsyncFieldLogLevel( + dataLogLevel + )!; + const retention = new LogRetentionConverter().toCDKRetentionDays( + logRetention + )!; + + return { + excludeVerboseContent, + fieldLogLevel, + retention, + }; +}; diff --git a/packages/backend-data/src/types.ts b/packages/backend-data/src/types.ts index d0b0dd153be..254fc9a6ea7 100644 --- a/packages/backend-data/src/types.ts +++ b/packages/backend-data/src/types.ts @@ -2,7 +2,12 @@ import { DerivedCombinedSchema, DerivedModelSchema, } from '@aws-amplify/data-schema-types'; -import { AmplifyFunction, ConstructFactory } from '@aws-amplify/plugin-types'; +import { + AmplifyFunction, + ConstructFactory, + LogLevel, + LogRetention, +} from '@aws-amplify/plugin-types'; /** * Authorization modes used in by client side Amplify represented in camelCase. @@ -145,6 +150,9 @@ export type DataProps = { * { : } */ stackMapping?: Record; + * Logging configuration for the API. + */ + logging?: DataLoggingOptions; }; export type AmplifyDataError = @@ -153,3 +161,83 @@ export type AmplifyDataError = | 'InvalidSchemaError' | 'MultipleSingletonResourcesError' | 'UnresolvedEntryPathError'; + +/** + * The logging configuration when writing GraphQL operations and tracing to Amazon CloudWatch for an AWS AppSync GraphQL API. + * Values can be `true` or a `DataLogConfig` object. + * + * ### Defaults + * Default settings will be applied when logging is set to `true` or an empty object, or for unspecified fields: + * - `excludeVerboseContent`: `true` + * - `fieldLogLevel`: `none` + * - `retention`: `1 week` + * + * **WARNING**: Verbose logging will log the full incoming query including user parameters. + * Sensitive information may be exposed in CloudWatch logs. Ensure that your IAM policies only grant access to authorized users. + * + * For information on AppSync's LogConfig, refer to https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-graphqlapi-logconfig.html. + */ +export type DataLoggingOptions = true | DataLogConfig; + +/** + * Customizable logging configuration when writing GraphQL operations and tracing to Amazon CloudWatch for an AWS AppSync GraphQL API. + * + * **WARNING**: Verbose logging will log the full incoming query including user parameters. + * Sensitive information may be exposed in CloudWatch logs. Ensure that your IAM policies only grant access to authorized users. + * + * For information on AppSync's LogConfig, refer to https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-graphqlapi-logconfig.html. + * For information on RetentionDays, refer to https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.RetentionDays.html. + * @default excludeVerboseContent: true, fieldLogLevel: 'none', retention: '1 week' + */ +export type DataLogConfig = { + /** + * The number of days log events are kept in CloudWatch Logs. + * @default RetentionDays.ONE_WEEK + * @see https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.RetentionDays.html + */ + retention?: LogRetention; + + /** + * When set to `true`, excludes verbose information from the logs, such as: + * - GraphQL Query + * - Request Headers + * - Response Headers + * - Context + * - Evaluated Mapping Templates + * + * This setting applies regardless of the specified logging level. + * + * **WARNING**: Verbose logging will log the full incoming query including user parameters. + * Sensitive information may be exposed in CloudWatch logs. Ensure that your IAM policies only grant access to authorized users. + * @default true + */ + excludeVerboseContent?: boolean; + + /** + * The field logging level. Values can be `'none'`, `'error'`, `'info'`, `'debug'`, or `'all'`. + * + * - **'none'**: No field-level logs are captured. + * - **'error'**: Logs the following information only for the fields that are in the error category: + * - The error section in the server response. + * - Field-level errors. + * - The generated request/response functions that got resolved for error fields. + * - **'info'**: Logs the following information only for the fields that are in the info and error categories: + * - Info-level messages. + * - The user messages sent through `$util.log.info` and `console.log`. + * - Field-level tracing and mapping logs are not shown. + * - **'debug'**: Logs the following information only for the fields that are in the debug, info, and error categories: + * - Debug-level messages. + * - The user messages sent through `$util.log.info`, `$util.log.debug`, `console.log`, and `console.debug`. + * - Field-level tracing and mapping logs are not shown. + * - **'all'**: The following information is logged for all fields in the query: + * - Field-level tracing information. + * - The generated request/response functions that were resolved for each field. + * @default 'none' + */ + fieldLogLevel?: DataLogLevel; +}; + +export type DataLogLevel = Extract< + LogLevel, + 'none' | 'all' | 'info' | 'debug' | 'error' +>; diff --git a/packages/backend-deployer/CHANGELOG.md b/packages/backend-deployer/CHANGELOG.md index 3055dcdd90e..c904704ade9 100644 --- a/packages/backend-deployer/CHANGELOG.md +++ b/packages/backend-deployer/CHANGELOG.md @@ -1,5 +1,121 @@ # @aws-amplify/backend-deployer +## 1.1.13 + +### Patch Changes + +- a7506f9: catch and wrap deployment in progress when deleting the backend +- Updated dependencies [a7506f9] +- Updated dependencies [a7506f9] + - @aws-amplify/platform-core@1.5.0 + - @aws-amplify/plugin-types@1.7.0 + +## 1.1.12 + +### Patch Changes + +- dedcc27: add error mapping for role is invalid or cannot be assumed error' +- 95942c5: expand wrapping of credentials related errors +- 1eced2c: add mapping for circular dependency failures with resolution +- Updated dependencies [95942c5] +- Updated dependencies [f679cf6] +- Updated dependencies [f193105] + - @aws-amplify/platform-core@1.4.0 + +## 1.1.11 + +### Patch Changes + +- 1593ce8: add error mapping for lambda bundling into an empty zip +- a406263: Narrow the error parsing for ExpiredToken regex +- 37d8564: handle cdk error mapping for more generic invalid credentials +- d66ab17: added mapping for additional EPERM errors +- 5a47d21: add error mapping for lambda exceeding max size +- 72b2fe0: update aws-cdk lib to ^2.168.0 +- 0a360fb: extract generic cdk asset publish failures +- 6015595: Catches most common EPERM error and update to resolution message for stack in failed state +- daaedb6: add cdk error mapping for error "cdk command not found" +- f6ba240: Upgrade execa +- Updated dependencies [cfdc854] +- Updated dependencies [72b2fe0] +- Updated dependencies [65abf6a] +- Updated dependencies [f6ba240] + - @aws-amplify/platform-core@1.3.0 + - @aws-amplify/plugin-types@1.6.0 + +## 1.1.10 + +### Patch Changes + +- d332c51: map a form of deletion/destroy failed error +- a6fa42e: specifically catch AppSync "Code contains one or more errors" +- a23f656: add more forms of transform errors to cdk error mapping +- 754d0f7: map another form of access denied validation error +- 716f844: Handle ENOENT error from npm +- 691e7ca: truncate large error messages before printing to customer +- b6f4c54: handle not authorized to perform on resource error +- 0114549: Handle invalid package.json error +- Updated dependencies [249c0e5] + - @aws-amplify/platform-core@1.2.2 + +## 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 6165fd7fa98..b3648344362 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.13", "type": "module", "publishConfig": { "access": "public" @@ -19,13 +19,14 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/platform-core": "^1.0.6", - "@aws-amplify/plugin-types": "^1.2.1", - "execa": "^8.0.1", - "tsx": "^4.6.1" + "@aws-amplify/platform-core": "^1.5.0", + "@aws-amplify/plugin-types": "^1.7.0", + "execa": "^9.5.1", + "tsx": "^4.6.1", + "strip-ansi": "^6.0.1" }, "peerDependencies": { - "aws-cdk": "^2.152.0", + "aws-cdk": "^2.168.0", "typescript": "^5.0.0" } } diff --git a/packages/backend-deployer/src/cdk_deployer.test.ts b/packages/backend-deployer/src/cdk_deployer.test.ts index 24981682a44..254cb0ff3d5 100644 --- a/packages/backend-deployer/src/cdk_deployer.test.ts +++ b/packages/backend-deployer/src/cdk_deployer.test.ts @@ -46,6 +46,7 @@ void describe('invokeCDKCommand', () => { runWithPackageManager: mock.fn(() => Promise.resolve() as never), getCommand: (args: string[]) => `'npx ${args.join(' ')}'`, allowsSignalPropagation: () => true, + tryGetDependencies: mock.fn(() => Promise.resolve([])), }; const invoker = new CDKDeployer( diff --git a/packages/backend-deployer/src/cdk_deployer.ts b/packages/backend-deployer/src/cdk_deployer.ts index 586f68d7462..4989b126015 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 7e6e1415de9..fbb5a331070 100644 --- a/packages/backend-deployer/src/cdk_error_mapper.test.ts +++ b/packages/backend-deployer/src/cdk_error_mapper.test.ts @@ -16,11 +16,30 @@ const testErrorMappings = [ expectedDownstreamErrorMessage: undefined, }, { - errorMessage: 'ExpiredToken', + errorMessage: + 'ExpiredToken: The security token included in the request is expired', + expectedTopLevelErrorMessage: + 'The security token included in the request is invalid.', + errorName: 'ExpiredTokenError', + expectedDownstreamErrorMessage: + 'ExpiredToken: The security token included in the request is expired', + }, + { + errorMessage: 'The security token included in the request is expired', + expectedTopLevelErrorMessage: + 'The security token included in the request is invalid.', + errorName: 'ExpiredTokenError', + expectedDownstreamErrorMessage: + 'The security token included in the request is expired', + }, + { + errorMessage: + 'InvalidClientTokenId: The security token included in the request is invalid', expectedTopLevelErrorMessage: 'The security token included in the request is invalid.', errorName: 'ExpiredTokenError', - expectedDownstreamErrorMessage: 'ExpiredToken', + expectedDownstreamErrorMessage: + 'The security token included in the request is invalid', }, { errorMessage: 'Access Denied', @@ -55,6 +74,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 +106,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 +172,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: Some amazing error message (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: Some amazing error message (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 +231,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 +266,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 +360,332 @@ const testErrorMappings = [ errorName: 'FilePermissionsError', expectedDownstreamErrorMessage: `EACCES: permission denied, unlink '.amplify/artifacts/cdk.out/synth.lock'`, }, + { + errorMessage: `[31mEPERM: operation not permitted, rename 'C:/Users/someUser/.amplify/artifacts/cdk.out/synth.lock.6785_1' → 'C:/Users/someUser/amplify/artifacts/cdk.out/synth.lock' [31m`, + expectedTopLevelErrorMessage: `Not permitted to rename file: 'C:/Users/someUser/.amplify/artifacts/cdk.out/synth.lock.6785_1'`, + errorName: 'FilePermissionsError', + expectedDownstreamErrorMessage: undefined, + }, + { + errorMessage: `[31mEPERM: operation not permitted, unlink '.amplify/artifacts/cdk.out/read.4276_1.lock' [31m`, + expectedTopLevelErrorMessage: `Operation not permitted on file: '.amplify/artifacts/cdk.out/read.4276_1.lock'`, + errorName: 'FilePermissionsError', + expectedDownstreamErrorMessage: undefined, + }, + { + errorMessage: `[31mEPERM: operation not permitted, open '.amplify/artifacts/cdk.out/synth.lock.6785_1' [31m`, + expectedTopLevelErrorMessage: `Operation not permitted on file: '.amplify/artifacts/cdk.out/synth.lock.6785_1'`, + errorName: 'FilePermissionsError', + expectedDownstreamErrorMessage: undefined, + }, + { + errorMessage: `Could not create output directory .amplify/artifacts/cdk.out (EPERM: operation not permitted, mkdir '.amplify/artifacts/cdk.out')`, + expectedTopLevelErrorMessage: `Not permitted to create the directory '.amplify/artifacts/cdk.out'`, + errorName: 'FilePermissionsError', + expectedDownstreamErrorMessage: undefined, + }, + { + 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: `error Command cdk not found. Did you mean cdl?`, + expectedTopLevelErrorMessage: 'Unable to detect cdk installation', + errorName: 'CDKNotFoundError', + expectedDownstreamErrorMessage: `error Command cdk not found. Did you mean cdl?`, + }, + { + 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: `amplify-stack-user-sandbox failed: BadRequestException: The code contains one or more errors.`, + expectedTopLevelErrorMessage: + 'A custom resolver used in your defineData contains one or more errors', + errorName: 'AppSyncResolverSyntaxError', + expectedDownstreamErrorMessage: + 'amplify-stack-user-sandbox failed: BadRequestException: The code contains one or more errors.', + }, + { + errorMessage: `Deployment failed: Error: The stack named amplify-stack-user-sandbox failed to deploy: UPDATE_ROLLBACK_COMPLETE: Resource handler returned message: The code contains one or more errors. (Service: AppSync, Status Code: 400,...`, + expectedTopLevelErrorMessage: + 'A custom resolver used in your defineData contains one or more errors', + errorName: 'AppSyncResolverSyntaxError', + expectedDownstreamErrorMessage: + 'Deployment failed: Error: The stack named amplify-stack-user-sandbox failed to deploy: UPDATE_ROLLBACK_COMPLETE: Resource handler returned message: The code contains one or more errors. (Service: AppSync, Status Code: 400,...', + }, + { + errorMessage: `User: some:great:user is not authorized to perform: appsync:StartSchemaCreation on resource: arn:aws:appsync:us-east-1:235494812930:/v1/api/myApi`, + expectedTopLevelErrorMessage: + 'Unable to deploy due to insufficient permissions', + errorName: 'AccessDeniedError', + expectedDownstreamErrorMessage: undefined, + }, + { + // eslint-disable-next-line spellcheck/spell-checker + errorMessage: `[31mamplify-stack-sandbox-11[22m: fail: Bucket named 'cdk-abc-assets-11-us-west-2' exists, but we do not have access to it. +amplify-stack-sandbox-11: fail: Bucket named 'cdk-abc-assets-11-us-west-2' exists, but we do not have access to it. +Failed to publish asset abc:current_account-current_region`, + expectedTopLevelErrorMessage: `CDK failed to publish assets due to 'Bucket named 'cdk-abc-assets-11-us-west-2' exists, but we do not have access to it.'`, + errorName: 'CDKAssetPublishError', + expectedDownstreamErrorMessage: undefined, + }, + { + // eslint-disable-next-line spellcheck/spell-checker + errorMessage: `[31mamplify-user-sandbox-c71414864a: fail: socket hang up + +Failed to publish asset abc:current_account-current_region`, + expectedTopLevelErrorMessage: `CDK failed to publish assets due to 'socket hang up'`, + errorName: 'CDKAssetPublishError', + 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, + }, + { + errorMessage: + // eslint-disable-next-line spellcheck/spell-checker + `Error: npm ERR! code ENOENT +npm ERR! syscall lstat +npm ERR! path /opt/homebrew/Cellar/node/22.2.0/lib +npm ERR! errno -2 +npm ERR! enoent ENOENT: no such file or directory, lstat '/opt/homebrew/Cellar/node/22.2.0/lib' +npm ERR! enoent This is related to npm not being able to find a file. +npm ERR! enoent +`, + expectedTopLevelErrorMessage: + // eslint-disable-next-line spellcheck/spell-checker + `NPM error occurred: npm ERR! code ENOENT +npm ERR! syscall lstat +npm ERR! path /opt/homebrew/Cellar/node/22.2.0/lib +npm ERR! errno -2 +npm ERR! enoent ENOENT: no such file or directory, lstat '/opt/homebrew/Cellar/node/22.2.0/lib' +npm ERR! enoent This is related to npm not being able to find a file. +npm ERR! enoent`, + errorName: 'CommonNPMError', + expectedDownstreamErrorMessage: undefined, + }, + { + errorMessage: + // eslint-disable-next-line spellcheck/spell-checker + `Error: npm error code ENOENT +npm error syscall lstat +npm error path C:\\Users\testUser\\AppData\\Roaming\\npm +npm error errno -4058 +npm error enoent ENOENT: no such file or directory, lstat 'C:\\Users\\testUser\\AppData\\Roaming\\npm' +npm error enoent This is related to npm not being able to find a file. +npm error enoent +`, + expectedTopLevelErrorMessage: + // eslint-disable-next-line spellcheck/spell-checker + `NPM error occurred: npm error code ENOENT +npm error syscall lstat +npm error path C:\\Users\testUser\\AppData\\Roaming\\npm +npm error errno -4058 +npm error enoent ENOENT: no such file or directory, lstat 'C:\\Users\\testUser\\AppData\\Roaming\\npm' +npm error enoent This is related to npm not being able to find a file. +npm error enoent`, + errorName: 'CommonNPMError', + expectedDownstreamErrorMessage: undefined, + }, + { + errorMessage: `[31m: destroy failed Error: The stack named amplify-some-stack is in a failed state. You may need to delete it from the AWS console : DELETE_FAILED (The following resource(s) failed to delete: [resource1, resource2]. )`, + expectedTopLevelErrorMessage: + 'The CloudFormation deletion failed due to amplify-some-stack being in DELETE_FAILED state. Ensure all your resources are able to be deleted', + errorName: 'CloudFormationDeletionError', + expectedDownstreamErrorMessage: undefined, + }, + { + errorMessage: `Error: some-stack failed: InvalidParameterValueException: Unzipped size must be smaller than 262144000 bytes`, + expectedTopLevelErrorMessage: 'Maximum Lambda size exceeded', + errorName: 'LambdaMaxSizeExceededError', + expectedDownstreamErrorMessage: undefined, + }, + { + errorMessage: `Error: some-stack failed: InvalidParameterValueException: Function code combined with layers exceeds the maximum allowed size of 262144000 bytes. The actual size is 306703523 bytes.`, + expectedTopLevelErrorMessage: 'Maximum Lambda size exceeded', + errorName: 'LambdaMaxSizeExceededError', + expectedDownstreamErrorMessage: undefined, + }, + { + errorMessage: `Error: some-stack failed: InvalidParameterValueException: Uploaded file must be a non-empty zip`, + expectedTopLevelErrorMessage: 'Lambda bundled into an empty zip', + errorName: 'LambdaEmptyZipFault', + expectedDownstreamErrorMessage: undefined, + }, + { + errorMessage: `Error: some-stack failed: ValidationError: Role role-arn is invalid or cannot be assumed`, + expectedTopLevelErrorMessage: + 'Role role-arn is invalid or cannot be assumed', + errorName: 'InvalidOrCannotAssumeRoleError', + expectedDownstreamErrorMessage: undefined, + }, + { + errorMessage: `some-stack failed: ValidationError: Circular dependency between resources: [storage1, data1, function1] `, + expectedTopLevelErrorMessage: + 'The CloudFormation deployment failed due to circular dependency found between nested stacks [storage1, data1, function1]', + errorName: 'CloudformationStackCircularDependencyError', + expectedDownstreamErrorMessage: undefined, + }, + { + errorMessage: `The stack named named-stack failed to deploy: UPDATE_ROLLBACK_COMPLETE: Circular dependency between resources: [resource1, resource2] `, + expectedTopLevelErrorMessage: + 'The CloudFormation deployment failed due to circular dependency found between resources [resource1, resource2] in a single stack', + errorName: 'CloudformationResourceCircularDependencyError', + expectedDownstreamErrorMessage: undefined, + }, + { + errorMessage: `destroy failed Error: Stack [someStackArn] cannot be deleted while in status UPDATE_COMPLETE_CLEANUP_IN_PROGRESS`, + expectedTopLevelErrorMessage: + 'Backend failed to be deleted since the previous deployment is still in progress.', + errorName: 'DeleteFailedWhileDeploymentInProgressError', + expectedDownstreamErrorMessage: undefined, + }, ]; void describe('invokeCDKCommand', { concurrency: 1 }, () => { @@ -274,8 +701,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 a00715d08c8..f1917069268 100644 --- a/packages/backend-deployer/src/cdk_error_mapper.ts +++ b/packages/backend-deployer/src/cdk_error_mapper.ts @@ -4,6 +4,7 @@ import { AmplifyFault, AmplifyUserError, } from '@aws-amplify/platform-core'; +import stripANSI from 'strip-ansi'; import { BackendDeployerOutputFormatter } from './types.js'; /** @@ -27,29 +28,42 @@ export class CdkErrorMapper { return amplifyError; } + const errorMessage = stripANSI(error.message); const matchingError = this.getKnownErrors().find((knownError) => - knownError.errorRegex.test(error.message) + knownError.errorRegex.test(errorMessage) ); if (matchingError) { // Extract meaningful contextual information if available - const matchGroups = error.message.match(matchingError.errorRegex); + const matchGroups = errorMessage.match(matchingError.errorRegex); if (matchGroups && matchGroups.length > 1) { // If the contextual information can be used in the error message use it, else consider it as a downstream cause 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 +98,12 @@ export class CdkErrorMapper { classification: AmplifyErrorClassification; }> => [ { - errorRegex: /ExpiredToken/, + errorRegex: + /ExpiredToken: .*|The security token included in the request is (expired|invalid)/, 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 +126,57 @@ 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: /Command cdk not found/, + humanReadableErrorMessage: 'Unable to detect cdk installation', + resolutionMessage: + "Ensure dependencies in your project are installed with your package manager. For example, by running 'yarn install' or 'npm install'", + errorName: 'CDKNotFoundError', + classification: 'ERROR', + }, + { + errorRegex: + /ValidationError: Role (?.*) is invalid or cannot be assumed/, + humanReadableErrorMessage: + 'Role {roleArn} is invalid or cannot be assumed', + resolutionMessage: + 'Ensure the role exists and AWS credentials have an IAM policy that grants sts:AssumeRole for the role', + errorName: 'InvalidOrCannotAssumeRoleError', + 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.', @@ -138,6 +202,30 @@ export class CdkErrorMapper { errorName: 'FilePermissionsError', classification: 'ERROR', }, + { + errorRegex: + /EPERM: operation not permitted, rename (?(.*)\/synth\.lock\.\S+) → '(.*)\/synth\.lock'/, + humanReadableErrorMessage: 'Not permitted to rename file: {fileName}', + resolutionMessage: `Try running the command again and ensure that only one instance of sandbox is running. If it still doesn't work check the permissions of '.amplify' folder`, + errorName: 'FilePermissionsError', + classification: 'ERROR', + }, + { + errorRegex: + /EPERM: operation not permitted, mkdir '(.*).amplify\/artifacts\/cdk.out'/, + humanReadableErrorMessage: `Not permitted to create the directory '.amplify/artifacts/cdk.out'`, + resolutionMessage: `Check the permissions of '.amplify' folder and try running the command again`, + errorName: 'FilePermissionsError', + classification: 'ERROR', + }, + { + errorRegex: + /EPERM: operation not permitted, (unlink|open) (?(.*)\.lock\S+)/, + humanReadableErrorMessage: 'Operation not permitted on file: {fileName}', + resolutionMessage: `Check the permissions of '.amplify' folder and try running the command again`, + errorName: 'FilePermissionsError', + classification: 'ERROR', + }, { errorRegex: new RegExp( `\\[ERR_MODULE_NOT_FOUND\\]:(.*)${this.multiLineEolRegex}|Error: Cannot find module (.*)` @@ -158,6 +246,63 @@ export class CdkErrorMapper { errorName: 'MultipleSandboxInstancesError', classification: 'ERROR', }, + { + errorRegex: + /InvalidParameterValueException:(.*) (size must be smaller than|exceeds the maximum allowed size of) (?\d+) bytes/, + humanReadableErrorMessage: 'Maximum Lambda size exceeded', + resolutionMessage: + 'Make sure your Lambda bundled packages with layers and dependencies is smaller than {maxSize} bytes unzipped.', + errorName: 'LambdaMaxSizeExceededError', + classification: 'ERROR', + }, + { + errorRegex: + /InvalidParameterValueException: Uploaded file must be a non-empty zip/, + humanReadableErrorMessage: 'Lambda bundled into an empty zip', + resolutionMessage: `Try removing '.amplify/artifacts' then running the command again. If it still doesn't work, see https://github.com/aws/aws-cdk/issues/18459 for more methods.`, + errorName: 'LambdaEmptyZipFault', + classification: 'FAULT', + }, + { + 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', + }, + { + //This has some overlap with "User:__ is not authorized to perform:__ on resource: __" - some resources cannot be deleted due to lack of permissions + errorRegex: + /The stack named (?.*) is in a failed state. You may need to delete it from the AWS console : DELETE_FAILED \(The following resource\(s\) failed to delete: (?.*). \)/, + humanReadableErrorMessage: + 'The CloudFormation deletion failed due to {stackName} being in DELETE_FAILED state. Ensure all your resources are able to be deleted', + resolutionMessage: + 'The following resource(s) failed to delete: {resources}. Check the error message for more details and ensure your resources are in a state where they can be deleted. Check the CloudFormation AWS Console for this stack to find additional information.', + errorName: 'CloudFormationDeletionError', + 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', + }, + { + errorRegex: + /User:(.*) is not authorized to perform:(?.*) on resource:(?.*)/, + 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 +316,21 @@ export class CdkErrorMapper { classification: 'ERROR', }, { + // Also extracts the first line in the stack where the error happened errorRegex: new RegExp( - `\\[TransformError\\]: Transform failed with .* error:${this.multiLineEolRegex}(?.*)` + `[✘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\\]|Error): Transform failed with .* error(s?):${this.multiLineEolRegex}(?(.*ERROR:.*${this.multiLineEolRegex})+)` ), humanReadableErrorMessage: '{esBuildErrorMessage}', resolutionMessage: @@ -180,6 +338,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 +387,24 @@ 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', + }, + { + errorRegex: new RegExp( + `(?(npm error|npm ERR!) code ENOENT${this.multiLineEolRegex}((npm error|npm ERR!) (.*)${this.multiLineEolRegex})*)` + ), + humanReadableErrorMessage: 'NPM error occurred: {npmError}', + resolutionMessage: `See https://docs.npmjs.com/common-errors for resolution.`, + errorName: 'CommonNPMError', + classification: 'ERROR', + }, { // Error: .* is printed to stderr during cdk synth // Also extracts the first line in the stack where the error happened @@ -232,6 +419,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 +453,88 @@ export class CdkErrorMapper { errorName: 'SecretNotSetError', classification: 'ERROR', }, + { + errorRegex: + /BadRequestException: The code contains one or more errors|The code contains one or more errors.*AppSync/, + humanReadableErrorMessage: `A custom resolver used in your defineData contains one or more errors`, + resolutionMessage: `Check for any syntax errors in your custom resolvers code.`, + errorName: 'AppSyncResolverSyntaxError', + classification: 'ERROR', + }, + { + errorRegex: new RegExp( + `amplify-.*-(branch|sandbox)-.*fail: (?.*)${this.multiLineEolRegex}.*Failed to publish asset`, + 'm' + ), + humanReadableErrorMessage: `CDK failed to publish assets due to '{publishFailure}'`, + resolutionMessage: `Check the error message for more details.`, + errorName: 'CDKAssetPublishError', + classification: 'ERROR', + }, + { + // We capture the parameter name to show relevant error message + errorRegex: + /destroy failed Error: Stack \[(?.*)\] cannot be deleted while in status /, + humanReadableErrorMessage: `Backend failed to be deleted since the previous deployment is still in progress.`, + resolutionMessage: `Wait for the previous deployment for stack {stackArn} to be completed before attempting to delete again.`, + errorName: 'DeleteFailedWhileDeploymentInProgressError', + classification: 'ERROR', + }, + // Generic error printed by CDK. Order matters so keep this towards the bottom of this list + { + // Error: .* is printed to stderr during cdk synth + // Also extracts the first line in the stack where the error happened + errorRegex: new RegExp( + `^Error: (.*${this.multiLineEolRegex}.*at.*)`, + 'm' + ), + humanReadableErrorMessage: + 'Unable to build the Amplify backend definition.', + resolutionMessage: + 'Check your backend definition in the `amplify` folder for syntax and type errors.', + errorName: 'BackendSynthError', + classification: 'ERROR', + }, + { + // This error pattern is observed when circular dependency is between stacks but not resources in a stack + errorRegex: + /ValidationError: Circular dependency between resources: \[(?.*)\]/, + humanReadableErrorMessage: + 'The CloudFormation deployment failed due to circular dependency found between nested stacks [{resources}]', + resolutionMessage: `If you are using functions then you can assign them to existing nested stacks that are dependent on functions or functions depend on them, for example: +1. If your function is defined as auth triggers, you should assign this function to auth stack. +2. If your function is used as data resolver or calls data API, you should assign this function to data stack. +To assign a function to a different stack, use the property 'resourceGroupName' in the defineFunction call and choose auth, data or any custom stack. + +If your circular dependency issue is not resolved with this workaround, please create an issue here https://github.com/aws-amplify/amplify-backend/issues/new/choose +`, + errorName: 'CloudformationStackCircularDependencyError', + classification: 'ERROR', + }, + { + // This error pattern is observed when circular dependency is between resources in a single stack, i.e. ValidationError is absent from the error message + errorRegex: + /(?.*)\]/, + humanReadableErrorMessage: + 'The CloudFormation deployment failed due to circular dependency found between resources [{resources}] in a single stack', + resolutionMessage: `If you are creating custom stacks or adding new CDK resources to amplify stacks, ensure that there are no cyclic dependencies. For more details see: https://aws.amazon.com/blogs/infrastructure-and-automation/handling-circular-dependency-errors-in-aws-cloudformation/`, + errorName: 'CloudformationResourceCircularDependencyError', + 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: @@ -268,18 +547,34 @@ export class CdkErrorMapper { export type CDKDeploymentError = | 'AccessDeniedError' + | 'AppSyncResolverSyntaxError' | 'BackendBuildError' | 'BackendSynthError' | 'BootstrapNotDetectedError' + | 'BootstrapDetectionError' + | 'BootstrapOutdatedError' + | 'CDKAssetPublishError' + | 'CDKNotFoundError' | 'CDKResolveAWSAccountError' + | 'CDKVersionMismatchError' | 'CFNUpdateNotSupportedError' + | 'CloudformationResourceCircularDependencyError' + | 'CloudformationStackCircularDependencyError' + | 'CloudFormationDeletionError' | 'CloudFormationDeploymentError' + | 'CommonNPMError' + | 'DeleteFailedWhileDeploymentInProgressError' | 'FilePermissionsError' + | 'MissingDefineBackendError' | 'MultipleSandboxInstancesError' | 'ESBuildError' | 'ExpiredTokenError' | 'FileConventionError' - | 'FileConventionError' | 'ModuleNotFoundError' + | 'InvalidOrCannotAssumeRoleError' + | 'InvalidPackageJsonError' | 'SecretNotSetError' - | 'SyntaxError'; + | 'SyntaxError' + | 'GetLambdaLayerVersionError' + | 'LambdaEmptyZipFault' + | 'LambdaMaxSizeExceededError'; diff --git a/packages/backend-function/API.md b/packages/backend-function/API.md index 86a98d7e1e6..e2a6f120607 100644 --- a/packages/backend-function/API.md +++ b/packages/backend-function/API.md @@ -4,11 +4,29 @@ ```ts +import { AmplifyResourceGroupName } from '@aws-amplify/plugin-types'; import { BackendSecret } from '@aws-amplify/plugin-types'; +import { Construct } from 'constructs'; import { ConstructFactory } from '@aws-amplify/plugin-types'; import { FunctionResources } from '@aws-amplify/plugin-types'; +import { IFunction } from 'aws-cdk-lib/aws-lambda'; +import { LogLevel } from '@aws-amplify/plugin-types'; +import { LogRetention } from '@aws-amplify/plugin-types'; import { ResourceAccessAcceptorFactory } from '@aws-amplify/plugin-types'; import { ResourceProvider } from '@aws-amplify/plugin-types'; +import { S3Client } from '@aws-sdk/client-s3'; +import { StackProvider } from '@aws-amplify/plugin-types'; + +declare namespace __export__runtime { + export { + getAmplifyDataClientConfig, + DataClientConfig, + DataClientEnv, + LibraryOptions, + ResourceConfig + } +} +export { __export__runtime } // @public (undocumented) export type AddEnvironmentFactory = { @@ -18,8 +36,50 @@ export type AddEnvironmentFactory = { // @public (undocumented) export type CronSchedule = `${string} ${string} ${string} ${string} ${string}` | `${string} ${string} ${string} ${string} ${string} ${string}`; -// @public -export const defineFunction: (props?: FunctionProps) => ConstructFactory & ResourceAccessAcceptorFactory & AddEnvironmentFactory>; +// @public (undocumented) +type DataClientConfig = { + resourceConfig: ResourceConfig; + libraryOptions: LibraryOptions; +}; + +// @public (undocumented) +type DataClientEnv = { + AWS_ACCESS_KEY_ID: string; + AWS_SECRET_ACCESS_KEY: string; + AWS_SESSION_TOKEN: string; + AWS_REGION: string; + AMPLIFY_DATA_DEFAULT_NAME: string; +} & Record; + +// @public (undocumented) +export function defineFunction(props?: FunctionProps): ConstructFactory & ResourceAccessAcceptorFactory & AddEnvironmentFactory & StackProvider>; + +// @public (undocumented) +export function defineFunction(provider: (scope: Construct) => IFunction, providerProps?: ProvidedFunctionProps): ConstructFactory & ResourceAccessAcceptorFactory & StackProvider>; + +// @public (undocumented) +export type FunctionArchitecture = 'x86_64' | 'arm64'; + +// @public (undocumented) +export type FunctionBundlingOptions = { + minify?: boolean; +}; + +// @public (undocumented) +export type FunctionLoggingOptions = ({ + format: 'json'; + level?: FunctionLogLevel; +} | { + format?: 'text'; +}) & { + retention?: FunctionLogRetention; +}; + +// @public (undocumented) +export type FunctionLogLevel = Extract; + +// @public (undocumented) +export type FunctionLogRetention = LogRetention; // @public (undocumented) export type FunctionProps = { @@ -27,16 +87,58 @@ export type FunctionProps = { entry?: string; timeoutSeconds?: number; memoryMB?: number; + ephemeralStorageSizeMB?: number; environment?: Record; runtime?: NodeVersion; + architecture?: FunctionArchitecture; schedule?: FunctionSchedule | FunctionSchedule[]; + layers?: Record; + bundling?: FunctionBundlingOptions; + resourceGroupName?: AmplifyResourceGroupName; + logging?: FunctionLoggingOptions; }; // @public (undocumented) export type FunctionSchedule = TimeInterval | CronSchedule; +// @public +const getAmplifyDataClientConfig: (env: DataClientEnv, s3Client?: S3Client) => Promise; + +// @public (undocumented) +type LibraryOptions = { + Auth: { + credentialsProvider: { + getCredentialsAndIdentityId: () => Promise<{ + credentials: { + accessKeyId: string; + secretAccessKey: string; + sessionToken: string; + }; + }>; + clearCredentialsAndIdentityId: () => void; + }; + }; +}; + +// @public (undocumented) +export type NodeVersion = 16 | 18 | 20 | 22; + // @public (undocumented) -export type NodeVersion = 16 | 18 | 20; +export type ProvidedFunctionProps = { + resourceGroupName?: AmplifyResourceGroupName; +}; + +// @public (undocumented) +type ResourceConfig = { + API: { + GraphQL: { + endpoint: string; + region: string; + defaultAuthMode: 'iam'; + modelIntrospection: any; + }; + }; +}; // @public (undocumented) export type TimeInterval = `every ${number}m` | `every ${number}h` | `every day` | `every week` | `every month` | `every year`; diff --git a/packages/backend-function/CHANGELOG.md b/packages/backend-function/CHANGELOG.md index 68161b9d38e..2305b3c4879 100644 --- a/packages/backend-function/CHANGELOG.md +++ b/packages/backend-function/CHANGELOG.md @@ -1,5 +1,138 @@ # @aws-amplify/backend-function +## 1.12.0 + +### Minor Changes + +- 3f521c3: add custom provided function support to define function + +### Patch Changes + +- c5d54c2: Update getAmplifyDataClient to have strict env type and remove narrowing logic + +## 1.11.0 + +### Minor Changes + +- a7506f9: added data logging api to defineData +- a7506f9: adds support for architecture property on defineFunction + +### Patch Changes + +- Updated dependencies [a7506f9] + - @aws-amplify/plugin-types@1.7.0 + +## 1.10.0 + +### Minor Changes + +- d32e4cd: Add ephemeralStorageSizeMB option to defineFunction +- 560878f: updates layer to also use layername:version +- f193105: Update getAmplifyDataClientConfig to work with named data backend + +## 1.9.0 + +### Minor Changes + +- 5cbe318: Add lambda data client +- 72b2fe0: Add support to `@aws-amplify/backend-function` for Node 22 + + Add support to `@aws-amplify/backend-function` for Node 22, which is a [supported Lambda runtime](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html#runtime-deprecation-levels) that was added in [`aws-cdk-lib/aws-lambda` version `2.168.0`](https://github.com/aws/aws-cdk/releases/tag/v2.168.0) on November 20th, 2024 + +- 65abf6a: Add options to control log settings + +### Patch Changes + +- 72b2fe0: update aws-cdk lib to ^2.168.0 +- d227f96: change errors in FunctionFactory to AmplifyUserError +- f6ba240: Upgrade execa +- d227f96: add validation for environment prop +- Updated dependencies [72b2fe0] +- Updated dependencies [f6ba240] + - @aws-amplify/backend-output-storage@1.1.4 + - @aws-amplify/plugin-types@1.6.0 + +## 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/api-extractor.json b/packages/backend-function/api-extractor.json index 0f56de03f66..cc2ebea8cf9 100644 --- a/packages/backend-function/api-extractor.json +++ b/packages/backend-function/api-extractor.json @@ -1,3 +1,4 @@ { - "extends": "../../api-extractor.base.json" + "extends": "../../api-extractor.base.json", + "mainEntryPointFilePath": "/lib/index.internal.d.ts" } diff --git a/packages/backend-function/package.json b/packages/backend-function/package.json index bdb9e459826..71a545b1331 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.12.0", "type": "module", "publishConfig": { "access": "public" @@ -10,6 +10,11 @@ "types": "./lib/index.d.ts", "import": "./lib/index.js", "require": "./lib/index.js" + }, + "./runtime": { + "types": "./lib/runtime/index.d.ts", + "import": "./lib/runtime/index.js", + "require": "./lib/runtime/index.js" } }, "main": "lib/index.js", @@ -19,20 +24,22 @@ }, "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", - "execa": "^8.0.1" + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/backend-output-storage": "^1.1.4", + "@aws-amplify/plugin-types": "^1.7.0", + "@aws-sdk/client-s3": "^3.624.0", + "execa": "^9.5.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.7", + "@aws-amplify/platform-core": "^1.5.1", + "@aws-sdk/client-s3": "^3.624.0", "@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.168.0", "constructs": "^10.0.0" } } diff --git a/packages/backend-function/src/factory.test.ts b/packages/backend-function/src/factory.test.ts index 316271131e1..2bf4610f941 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, @@ -13,10 +13,18 @@ import { } from '@aws-amplify/backend-platform-test-stubs'; import { defaultLambda } from './test-assets/default-lambda/resource.js'; import { Template } from 'aws-cdk-lib/assertions'; -import { NodeVersion, defineFunction } from './factory.js'; +import { + FunctionArchitecture, + NodeVersion, + defineFunction, +} from './factory.js'; import { lambdaWithDependencies } from './test-assets/lambda-with-dependencies/resource.js'; -import { Runtime } from 'aws-cdk-lib/aws-lambda'; +import { Architecture, 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'; +import { AmplifyUserError } from '@aws-amplify/platform-core'; +import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; const createStackAndSetContext = (): Stack => { const app = new App(); @@ -52,6 +60,15 @@ void describe('AmplifyFunctionFactory', () => { }; }); + after(async () => { + // clean up generated env files + await fsp.rm(path.join(process.cwd(), '.amplify'), { + recursive: true, + force: true, + maxRetries: 3, + }); + }); + void it('creates singleton function instance', () => { const functionFactory = defaultLambda; const instance1 = functionFactory.getInstance(getInstanceProps); @@ -59,10 +76,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 +102,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 +119,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 +136,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 +160,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 +182,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 +200,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, @@ -191,9 +214,10 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', timeoutSeconds: 0, }).getInstance(getInstanceProps), - new Error( - 'timeoutSeconds must be a whole number between 1 and 900 inclusive' - ) + new AmplifyUserError('InvalidTimeoutError', { + message: `Invalid function timeout of 0`, + resolution: `timeoutSeconds must be a whole number between 1 and 900 inclusive`, + }) ); }); @@ -204,9 +228,10 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', timeoutSeconds: 901, }).getInstance(getInstanceProps), - new Error( - 'timeoutSeconds must be a whole number between 1 and 900 inclusive' - ) + new AmplifyUserError('InvalidTimeoutError', { + message: `Invalid function timeout of 901`, + resolution: `timeoutSeconds must be a whole number between 1 and 900 inclusive`, + }) ); }); @@ -217,9 +242,10 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', timeoutSeconds: 10.5, }).getInstance(getInstanceProps), - new Error( - 'timeoutSeconds must be a whole number between 1 and 900 inclusive' - ) + new AmplifyUserError('InvalidTimeoutError', { + message: `Invalid function timeout of 10.5`, + resolution: `timeoutSeconds must be a whole number between 1 and 900 inclusive`, + }) ); }); }); @@ -230,7 +256,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 +267,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, @@ -255,9 +281,10 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', memoryMB: 127, }).getInstance(getInstanceProps), - new Error( - 'memoryMB must be a whole number between 128 and 10240 inclusive' - ) + new AmplifyUserError('InvalidMemoryMBError', { + message: `Invalid function memoryMB of 127`, + resolution: `memoryMB must be a whole number between 128 and 10240 inclusive`, + }) ); }); @@ -268,9 +295,10 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', memoryMB: 10241, }).getInstance(getInstanceProps), - new Error( - 'memoryMB must be a whole number between 128 and 10240 inclusive' - ) + new AmplifyUserError('InvalidMemoryMBError', { + message: `Invalid function memoryMB of 10241`, + resolution: `memoryMB must be a whole number between 128 and 10240 inclusive`, + }) ); }); @@ -281,9 +309,103 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', memoryMB: 256.2, }).getInstance(getInstanceProps), - new Error( - 'memoryMB must be a whole number between 128 and 10240 inclusive' - ) + new AmplifyUserError('InvalidMemoryMBError', { + message: `Invalid function memoryMB of 256.2`, + resolution: `memoryMB must be a whole number between 128 and 10240 inclusive`, + }) + ); + }); + }); + + void describe('environment property', () => { + void it('sets valid environment', () => { + const functionFactory = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'myCoolLambda', + environment: { + TEST_ENV: 'testValue', + }, + }); + const lambda = functionFactory.getInstance(getInstanceProps); + const stack = lambda.stack; + const template = Template.fromStack(stack); + template.resourceCountIs('AWS::Lambda::Function', 1); + template.hasResourceProperties('AWS::Lambda::Function', { + Environment: { + Variables: { + TEST_ENV: 'testValue', + }, + }, + }); + }); + + void it('sets default environment', () => { + const functionFactory = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'myCoolLambda', + }); + const lambda = functionFactory.getInstance(getInstanceProps); + const stack = lambda.stack; + const template = Template.fromStack(stack); + template.resourceCountIs('AWS::Lambda::Function', 1); + template.hasResourceProperties('AWS::Lambda::Function', { + Environment: {}, + }); + }); + + void it('throws when adding environment variables with invalid key', () => { + assert.throws( + () => + defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'myCoolLambda', + environment: { + 'this.is.wrong': 'testValue', + }, + }).getInstance(getInstanceProps), + new AmplifyUserError('InvalidEnvironmentKeyError', { + message: `Invalid function environment key(s): this.is.wrong`, + resolution: + 'Environment keys must match [a-zA-Z]([a-zA-Z0-9_])+ and be at least 2 characters', + }) + ); + }); + + void it('throws when adding environment variables with key less than 2 characters', () => { + assert.throws( + () => + defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'myCoolLambda', + environment: { + A: 'testValue', + }, + }).getInstance(getInstanceProps), + new AmplifyUserError('InvalidEnvironmentKeyError', { + message: `Invalid function environment key(s): A`, + resolution: + 'Environment keys must match [a-zA-Z]([a-zA-Z0-9_])+ and be at least 2 characters', + }) + ); + }); + + void it('throws when multiple environment variables are invalid', () => { + assert.throws( + () => + defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'lambdaWithMultipleEnvVars', + environment: { + A: 'testValueA', + TEST_ENV: 'envValue', + 'this.is.wrong': 'testValue', + }, + }).getInstance(getInstanceProps), + new AmplifyUserError('InvalidEnvironmentKeyError', { + message: `Invalid function environment key(s): A, this.is.wrong`, + resolution: + 'Environment keys must match [a-zA-Z]([a-zA-Z0-9_])+ and be at least 2 characters', + }) ); }); }); @@ -292,12 +414,12 @@ void describe('AmplifyFunctionFactory', () => { void it('sets valid runtime', () => { const lambda = defineFunction({ entry: './test-assets/default-lambda/handler.ts', - runtime: 16, + runtime: 22, }).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, + Runtime: Runtime.NODEJS_22_X.name, }); }); @@ -305,7 +427,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, @@ -319,7 +441,10 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', runtime: 14 as NodeVersion, }).getInstance(getInstanceProps), - new Error('runtime must be one of the following: 16, 18, 20') + new AmplifyUserError('InvalidRuntimeError', { + message: `Invalid function runtime of 14`, + resolution: 'runtime must be one of the following: 16, 18, 20, 22', + }) ); }); @@ -334,13 +459,41 @@ void describe('AmplifyFunctionFactory', () => { }); }); + void describe('architecture property', () => { + void it('sets valid architecture', () => { + const lambda = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + architecture: 'arm64', + }).getInstance(getInstanceProps); + const template = Template.fromStack(lambda.stack); + + template.hasResourceProperties('AWS::Lambda::Function', { + Architectures: [Architecture.ARM_64.name], + }); + }); + }); + + void it('throws on invalid architecture', () => { + assert.throws( + () => + defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + architecture: 'invalid' as FunctionArchitecture, + }).getInstance(getInstanceProps), + new AmplifyUserError('InvalidArchitectureError', { + message: `Invalid function architecture of invalid`, + resolution: 'architecture must be one of the following: arm64, x86_64', + }) + ); + }); + void describe('schedule property', () => { void it('sets valid schedule - rate', () => { const lambda = defineFunction({ 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 +514,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 +535,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 +552,64 @@ 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('logging options', () => { + void it('sets logging options', () => { + const lambda = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + bundling: { + minify: false, + }, + logging: { + format: 'json', + level: 'warn', + retention: '13 months', + }, + }).getInstance(getInstanceProps); + const template = Template.fromStack(lambda.stack); + // Enabling log retention adds extra lambda. + template.resourceCountIs('AWS::Lambda::Function', 2); + const lambdas = template.findResources('AWS::Lambda::Function'); + assert.ok( + Object.keys(lambdas).some((key) => key.startsWith('LogRetention')) + ); + template.hasResourceProperties('Custom::LogRetention', { + RetentionInDays: 400, + }); + template.hasResourceProperties('AWS::Lambda::Function', { + Handler: 'index.handler', + LoggingConfig: { + ApplicationLogLevel: 'WARN', + LogFormat: 'JSON', + }, + }); + }); + }); + void describe('resourceAccessAcceptor', () => { void it('attaches policy to execution role and configures ssm environment context', () => { const functionFactory = defineFunction({ @@ -412,7 +617,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 +708,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( @@ -513,4 +716,102 @@ void describe('AmplifyFunctionFactory', () => { 'function-Lambda' ); }); + + void describe('ephemeralStorageSizeMB property', () => { + void it('sets valid ephemeralStorageSize', () => { + const lambda = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + ephemeralStorageSizeMB: 1024, + }).getInstance(getInstanceProps); + const template = Template.fromStack(lambda.stack); + + template.hasResourceProperties('AWS::Lambda::Function', { + EphemeralStorage: { Size: 1024 }, + }); + }); + + void it('sets default ephemeralStorageSizeMB', () => { + const lambda = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + }).getInstance(getInstanceProps); + const template = Template.fromStack(lambda.stack); + + template.hasResourceProperties('AWS::Lambda::Function', { + EphemeralStorage: { Size: 512 }, + }); + }); + + void it('throws on ephemeralStorageSizeMB below 512 MB', () => { + assert.throws( + () => + defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + ephemeralStorageSizeMB: 511, + }).getInstance(getInstanceProps), + new AmplifyUserError('InvalidEphemeralStorageSizeMBError', { + message: `Invalid function ephemeralStorageSizeMB of 511`, + resolution: `ephemeralStorageSizeMB must be a whole number between 512 and 10240 inclusive`, + }) + ); + }); + + void it('throws on ephemeralStorageSizeMB above 10240 MB', () => { + assert.throws( + () => + defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + ephemeralStorageSizeMB: 10241, + }).getInstance(getInstanceProps), + new AmplifyUserError('InvalidEphemeralStorageSizeMBError', { + message: `Invalid function ephemeralStorageSizeMB of 10241`, + resolution: `ephemeralStorageSizeMB must be a whole number between 512 and 10240 inclusive`, + }) + ); + }); + + void it('throws on fractional ephemeralStorageSizeMB', () => { + assert.throws( + () => + defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + ephemeralStorageSizeMB: 512.5, + }).getInstance(getInstanceProps), + new AmplifyUserError('InvalidEphemeralStorageSizeMBError', { + message: `Invalid function ephemeralStorageSizeMB of 512.5`, + resolution: `ephemeralStorageSizeMB must be a whole number between 512 and 10240 inclusive`, + }) + ); + }); + }); + + void describe('provided function runtime property', () => { + void it('sets valid runtime', () => { + const lambda = defineFunction((scope) => { + return new NodejsFunction(scope, 'nodejs-provided', { + entry: + './packages/backend-function/src/test-assets/default-lambda/handler.ts', + runtime: Runtime.NODEJS_22_X, + }); + }).getInstance(getInstanceProps); + const template = Template.fromStack(lambda.stack); + + template.hasResourceProperties('AWS::Lambda::Function', { + Runtime: Runtime.NODEJS_22_X.name, + }); + }); + + void it('provided function defaults to oldest runtime', () => { + const lambda = defineFunction((scope) => { + return new NodejsFunction(scope, 'nodejs-provided', { + entry: + './packages/backend-function/src/test-assets/default-lambda/handler.ts', + }); + }).getInstance(getInstanceProps); + const template = Template.fromStack(lambda.stack); + + template.hasResourceProperties('AWS::Lambda::Function', { + Runtime: Runtime.NODEJS_16_X.name, + }); + }); + }); }); diff --git a/packages/backend-function/src/factory.ts b/packages/backend-function/src/factory.ts index e32e72cfb69..4c1be471e29 100644 --- a/packages/backend-function/src/factory.ts +++ b/packages/backend-function/src/factory.ts @@ -1,4 +1,11 @@ +import { FunctionOutput } from '@aws-amplify/backend-output-schemas'; import { + AmplifyUserError, + CallerDirectoryExtractor, + TagName, +} from '@aws-amplify/platform-core'; +import { + AmplifyResourceGroupName, BackendOutputStorageStrategy, BackendSecret, BackendSecretResolver, @@ -7,38 +14,42 @@ import { ConstructFactoryGetInstanceProps, FunctionResources, GenerateContainerEntryProps, + LogLevel, + LogRetention, + ResourceAccessAcceptor, ResourceAccessAcceptorFactory, ResourceNameValidator, ResourceProvider, - SsmEnvironmentEntry, + StackProvider, } from '@aws-amplify/plugin-types'; -import { Construct } from 'constructs'; +import { Duration, Size, Stack, Tags } from 'aws-cdk-lib'; +import { Rule } from 'aws-cdk-lib/aws-events'; +import * as targets from 'aws-cdk-lib/aws-events-targets'; +import { + Architecture, + CfnFunction, + IFunction, + ILayerVersion, + LayerVersion, + Runtime, +} from 'aws-cdk-lib/aws-lambda'; import { NodejsFunction, OutputFormat } from 'aws-cdk-lib/aws-lambda-nodejs'; -import * as path from 'path'; -import { Duration, Stack, Tags } from 'aws-cdk-lib'; -import { CfnFunction, Runtime } from 'aws-cdk-lib/aws-lambda'; -import { createRequire } from 'module'; -import { FunctionEnvironmentTranslator } from './function_env_translator.js'; -import { Policy } from 'aws-cdk-lib/aws-iam'; +import { Construct } from 'constructs'; import { readFileSync } from 'fs'; +import { createRequire } from 'module'; import { EOL } from 'os'; -import { - FunctionOutput, - functionOutputKey, -} from '@aws-amplify/backend-output-schemas'; +import * as path from 'path'; +import { FunctionEnvironmentTranslator } from './function_env_translator.js'; import { FunctionEnvironmentTypeGenerator } from './function_env_type_generator.js'; -import { AttributionMetadataStorage } from '@aws-amplify/backend-output-storage'; -import { fileURLToPath } from 'node:url'; -import { - AmplifyUserError, - CallerDirectoryExtractor, - TagName, -} from '@aws-amplify/platform-core'; +import { FunctionLayerArnParser } from './layer_parser.js'; +import { convertLoggingOptionsToCDK } from './logging_options_parser.js'; import { convertFunctionSchedulesToRuleSchedules } from './schedule_parser.js'; -import * as targets from 'aws-cdk-lib/aws-events-targets'; -import { Rule } from 'aws-cdk-lib/aws-events'; - -const functionStackType = 'function-Lambda'; +import { + ProvidedFunctionFactory, + ProvidedFunctionProps, +} from './provided_function_factory.js'; +import { AmplifyFunctionBase } from './function_construct_base.js'; +import { FunctionResourceAccessAcceptor } from './resource_access_acceptor.js'; export type AddEnvironmentFactory = { addEnvironment: (key: string, value: string | BackendSecret) => void; @@ -56,16 +67,44 @@ export type TimeInterval = | `every year`; export type FunctionSchedule = TimeInterval | CronSchedule; -/** - * Entry point for defining a function in the Amplify ecosystem - */ -export const defineFunction = ( - props: FunctionProps = {} +export type FunctionLogLevel = Extract< + LogLevel, + 'info' | 'debug' | 'warn' | 'error' | 'fatal' | 'trace' +>; +export type FunctionLogRetention = LogRetention; + +export function defineFunction( + props?: FunctionProps ): ConstructFactory< ResourceProvider & ResourceAccessAcceptorFactory & - AddEnvironmentFactory -> => new FunctionFactory(props, new Error().stack); + AddEnvironmentFactory & + StackProvider +>; +export function defineFunction( + provider: (scope: Construct) => IFunction, + providerProps?: ProvidedFunctionProps +): ConstructFactory< + ResourceProvider & + ResourceAccessAcceptorFactory & + StackProvider +>; +/** + * Entry point for defining a function in the Amplify ecosystem + */ +// This is the "implementation overload", it's not visible in public api. +// We have to use function notation instead of arrow notation. +// Arrow notation does not support overloads. +// eslint-disable-next-line no-restricted-syntax +export function defineFunction( + propsOrProvider: FunctionProps | ((scope: Construct) => IFunction) = {}, + providerProps?: ProvidedFunctionProps +): unknown { + if (propsOrProvider && typeof propsOrProvider === 'function') { + return new ProvidedFunctionFactory(propsOrProvider, providerProps); + } + return new FunctionFactory(propsOrProvider, new Error().stack); +} export type FunctionProps = { /** @@ -96,10 +135,17 @@ export type FunctionProps = { /** * An amount of memory (RAM) to allocate to the function between 128 and 10240 MB. * Must be a whole number. - * Default is 128MB. + * Default is 512MB. */ memoryMB?: number; + /** + * The size of the function's /tmp directory in MB. + * Must be a whole number. + * @default 512 + */ + ephemeralStorageSizeMB?: number; + /** * Environment variables that will be available during function execution */ @@ -112,6 +158,12 @@ export type FunctionProps = { */ runtime?: NodeVersion; + /** + * The architecture of the target platform for the lambda environment. + * Defaults to X86_64. + */ + architecture?: FunctionArchitecture; + /** * A time interval string to periodically run the function. * This can be either a string of `"every "`, `"every day|week|month|year"` or cron expression. @@ -124,6 +176,60 @@ export type FunctionProps = { * schedule: "0 9 ? * 2 *" // every Monday at 9am */ schedule?: FunctionSchedule | FunctionSchedule[]; + + /** + * Attach Lambda layers to a function + * - A Lambda layer is represented by an object of key/value pair where the key is the module name that is exported from your layer and the value is the ARN of the layer. The key (module name) is used to externalize the module dependency so it doesn't get bundled with your lambda function + * - Maximum of 5 layers can be attached to a function and must be in the same region as the function. + * @example + * layers: { + * "@aws-lambda-powertools/logger": "arn:aws:lambda::094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:11" + * }, + * or + * @example + * layers: { + * "Sharp": "SharpLayer:1" + * }, + * @see [Amplify documentation for Lambda layers](https://docs.amplify.aws/react/build-a-backend/functions/add-lambda-layers) + * @see [AWS documentation for Lambda layers](https://docs.aws.amazon.com/lambda/latest/dg/chapter-layers.html) + */ + layers?: Record; + + /* + * Options for bundling the function code. + */ + bundling?: FunctionBundlingOptions; + + /** + * Group the function with existing Amplify resources or separate the function into its own group. + * @default 'function' // grouping with other Amplify functions + * @example + * resourceGroupName: 'auth' // to group an auth trigger with an auth resource + */ + resourceGroupName?: AmplifyResourceGroupName; + + logging?: FunctionLoggingOptions; +}; + +export type FunctionBundlingOptions = { + /** + * Whether to minify the function code. + * + * Defaults to true. + */ + minify?: boolean; +}; + +export type FunctionLoggingOptions = ( + | { + format: 'json'; + level?: FunctionLogLevel; + } + | { + format?: 'text'; + } +) & { + retention?: FunctionLogRetention; }; /** @@ -161,14 +267,21 @@ class FunctionFactory implements ConstructFactory { ): HydratedFunctionProps => { const name = this.resolveName(); resourceNameValidator?.validate(name); + return { name, entry: this.resolveEntry(), timeoutSeconds: this.resolveTimeout(), memoryMB: this.resolveMemory(), - environment: this.props.environment ?? {}, + ephemeralStorageSizeMB: this.resolveEphemeralStorageSize(), + environment: this.resolveEnvironment(), runtime: this.resolveRuntime(), + architecture: this.resolveArchitecture(), schedule: this.resolveSchedule(), + bundling: this.resolveBundling(), + layers: this.props.layers ?? {}, + resourceGroupName: this.props.resourceGroupName ?? 'function', + logging: this.props.logging ?? {}, }; }; @@ -224,9 +337,10 @@ class FunctionFactory implements ConstructFactory { timeoutMax ) ) { - throw new Error( - `timeoutSeconds must be a whole number between ${timeoutMin} and ${timeoutMax} inclusive` - ); + throw new AmplifyUserError('InvalidTimeoutError', { + message: `Invalid function timeout of ${this.props.timeoutSeconds}`, + resolution: `timeoutSeconds must be a whole number between ${timeoutMin} and ${timeoutMax} inclusive`, + }); } return this.props.timeoutSeconds; }; @@ -241,13 +355,63 @@ class FunctionFactory implements ConstructFactory { if ( !isWholeNumberBetweenInclusive(this.props.memoryMB, memoryMin, memoryMax) ) { - throw new Error( - `memoryMB must be a whole number between ${memoryMin} and ${memoryMax} inclusive` - ); + throw new AmplifyUserError('InvalidMemoryMBError', { + message: `Invalid function memoryMB of ${this.props.memoryMB}`, + resolution: `memoryMB must be a whole number between ${memoryMin} and ${memoryMax} inclusive`, + }); } return this.props.memoryMB; }; + private resolveEphemeralStorageSize = () => { + const ephemeralStorageSizeMin = 512; + const ephemeralStorageSizeMax = 10240; + const ephemeralStorageSizeDefault = 512; + if (this.props.ephemeralStorageSizeMB === undefined) { + return ephemeralStorageSizeDefault; + } + if ( + !isWholeNumberBetweenInclusive( + this.props.ephemeralStorageSizeMB, + ephemeralStorageSizeMin, + ephemeralStorageSizeMax + ) + ) { + throw new AmplifyUserError('InvalidEphemeralStorageSizeMBError', { + message: `Invalid function ephemeralStorageSizeMB of ${this.props.ephemeralStorageSizeMB}`, + resolution: `ephemeralStorageSizeMB must be a whole number between ${ephemeralStorageSizeMin} and ${ephemeralStorageSizeMax} inclusive`, + }); + } + return this.props.ephemeralStorageSizeMB; + }; + + private resolveEnvironment = () => { + if (this.props.environment === undefined) { + return {}; + } + + const invalidKeys: string[] = []; + + Object.keys(this.props.environment).forEach((key) => { + // validate using key pattern from https://docs.aws.amazon.com/lambda/latest/api/API_Environment.html + if (!key.match(/^[a-zA-Z]([a-zA-Z0-9_])+$/)) { + invalidKeys.push(key); + } + }); + + if (invalidKeys.length > 0) { + throw new AmplifyUserError('InvalidEnvironmentKeyError', { + message: `Invalid function environment key(s): ${invalidKeys.join( + ', ' + )}`, + resolution: + 'Environment keys must match [a-zA-Z]([a-zA-Z0-9_])+ and be at least 2 characters', + }); + } + + return this.props.environment; + }; + private resolveRuntime = () => { const runtimeDefault = 18; @@ -257,16 +421,36 @@ class FunctionFactory implements ConstructFactory { } if (!(this.props.runtime in nodeVersionMap)) { - throw new Error( - `runtime must be one of the following: ${Object.keys( + throw new AmplifyUserError('InvalidRuntimeError', { + message: `Invalid function runtime of ${this.props.runtime}`, + resolution: `runtime must be one of the following: ${Object.keys( nodeVersionMap - ).join(', ')}` - ); + ).join(', ')}`, + }); } return this.props.runtime; }; + private resolveArchitecture = () => { + const architectureDefault = 'x86_64'; + + if (!this.props.architecture) { + return architectureDefault; + } + + if (!(this.props.architecture in architectureMap)) { + throw new AmplifyUserError('InvalidArchitectureError', { + message: `Invalid function architecture of ${this.props.architecture}`, + resolution: `architecture must be one of the following: ${Object.keys( + architectureMap + ).join(', ')}`, + }); + } + + return this.props.architecture; + }; + private resolveSchedule = () => { if (!this.props.schedule) { return []; @@ -274,26 +458,68 @@ class FunctionFactory implements ConstructFactory { return this.props.schedule; }; + + private resolveBundling = () => { + const bundlingDefault = { + format: OutputFormat.ESM, + bundleAwsSDK: true, + loader: { + '.node': 'file', + }, + minify: true, + sourceMap: true, + }; + + return { + ...bundlingDefault, + minify: this.resolveMinify(this.props.bundling), + }; + }; + + private resolveMinify = (bundling?: FunctionBundlingOptions) => { + return bundling?.minify === undefined ? true : bundling.minify; + }; } type HydratedFunctionProps = Required; class FunctionGenerator implements ConstructContainerEntryGenerator { - readonly resourceGroupName = 'function'; + readonly resourceGroupName: AmplifyResourceGroupName; constructor( private readonly props: HydratedFunctionProps, private readonly outputStorageStrategy: BackendOutputStorageStrategy - ) {} + ) { + this.resourceGroupName = props.resourceGroupName; + } generateContainerEntry = ({ scope, backendSecretResolver, }: GenerateContainerEntryProps) => { + // Move layer resolution here where we have access to scope + const parser = new FunctionLayerArnParser( + Stack.of(scope).region, + Stack.of(scope).account + ); + const resolvedLayerArns = parser.parseLayers( + this.props.layers ?? {}, + this.props.name + ); + + // resolve layers to LayerVersion objects for the NodejsFunction constructor + const resolvedLayers = Object.entries(resolvedLayerArns).map(([key, arn]) => + LayerVersion.fromLayerVersionArn( + scope, + `${this.props.name}-${key}-layer`, + arn + ) + ); + return new AmplifyFunction( scope, this.props.name, - this.props, + { ...this.props, resolvedLayers }, backendSecretResolver, this.outputStorageStrategy ); @@ -301,22 +527,19 @@ class FunctionGenerator implements ConstructContainerEntryGenerator { } class AmplifyFunction - extends Construct - implements - ResourceProvider, - ResourceAccessAcceptorFactory, - AddEnvironmentFactory + extends AmplifyFunctionBase + implements AddEnvironmentFactory { readonly resources: FunctionResources; private readonly functionEnvironmentTranslator: FunctionEnvironmentTranslator; constructor( scope: Construct, id: string, - props: HydratedFunctionProps, + props: HydratedFunctionProps & { resolvedLayers: ILayerVersion[] }, backendSecretResolver: BackendSecretResolver, outputStorageStrategy: BackendOutputStorageStrategy ) { - super(scope, id); + super(scope, id, outputStorageStrategy); const runtime = nodeVersionMap[props.runtime]; @@ -354,30 +577,43 @@ class AmplifyFunction functionEnvironmentTypeGenerator.generateProcessEnvShim(); let functionLambda: NodejsFunction; + const cdkLoggingOptions = convertLoggingOptionsToCDK(props.logging); try { functionLambda = new NodejsFunction(scope, `${id}-lambda`, { entry: props.entry, timeout: Duration.seconds(props.timeoutSeconds), memorySize: props.memoryMB, + architecture: architectureMap[props.architecture], + ephemeralStorageSize: Size.mebibytes(props.ephemeralStorageSizeMB), runtime: nodeVersionMap[props.runtime], + layers: props.resolvedLayers, bundling: { - format: OutputFormat.ESM, + ...props.bundling, banner: bannerCode, - bundleAwsSDK: true, inject: shims, - loader: { - '.node': 'file', - }, - minify: true, - sourceMap: true, + externalModules: Object.keys(props.layers), }, + logRetention: cdkLoggingOptions.retention, + applicationLogLevelV2: cdkLoggingOptions.level, + loggingFormat: cdkLoggingOptions.format, }); } catch (error) { + // If the error is from ES Bundler which is executed as a child process by CDK, + // then the error from CDK contains the command that was executed along with the exit status. + // Wrapping it here would cause the cdk_deployer to re-throw this wrapped exception + // instead of scraping the stderr for actual ESBuild error. + if ( + error instanceof Error && + error.message.match(/Failed to bundle asset.*exited with status/) + ) { + throw error; + } throw new AmplifyUserError( 'NodeJSFunctionConstructInitializationError', { message: 'Failed to instantiate nodejs function construct', - resolution: 'See the underlying error message for more details.', + resolution: + 'See the underlying error message for more details. Use `--debug` for additional debugging information.', }, error as Error ); @@ -425,52 +661,18 @@ class AmplifyFunction }, }; - this.storeOutput(outputStorageStrategy); - - new AttributionMetadataStorage().storeAttributionMetadata( - Stack.of(this), - functionStackType, - fileURLToPath(new URL('../package.json', import.meta.url)) - ); + this.storeOutput(); } addEnvironment = (key: string, value: string | BackendSecret) => { this.functionEnvironmentTranslator.addEnvironmentEntry(key, value); }; - getResourceAccessAcceptor = () => ({ - identifier: `${this.node.id}LambdaResourceAccessAcceptor`, - acceptResourceAccess: ( - policy: Policy, - ssmEnvironmentEntries: SsmEnvironmentEntry[] - ) => { - const role = this.resources.lambda.role; - if (!role) { - // This should never happen since we are using the Function L2 construct - throw new Error( - 'No execution role found to attach lambda permissions to' - ); - } - policy.attachToRole(role); - ssmEnvironmentEntries.forEach(({ name, path }) => { - this.functionEnvironmentTranslator.addSsmEnvironmentEntry(name, path); - }); - }, - }); - - /** - * Store storage outputs using provided strategy - */ - private storeOutput = ( - outputStorageStrategy: BackendOutputStorageStrategy - ): void => { - outputStorageStrategy.appendToBackendOutputList(functionOutputKey, { - version: '1', - payload: { - definedFunctions: this.resources.lambda.functionName, - }, - }); - }; + getResourceAccessAcceptor = (): ResourceAccessAcceptor => + new FunctionResourceAccessAcceptor( + this, + this.functionEnvironmentTranslator + ); } const isWholeNumberBetweenInclusive = ( @@ -479,10 +681,18 @@ const isWholeNumberBetweenInclusive = ( max: number ) => min <= test && test <= max && test % 1 === 0; -export type NodeVersion = 16 | 18 | 20; +export type NodeVersion = 16 | 18 | 20 | 22; const nodeVersionMap: Record = { 16: Runtime.NODEJS_16_X, 18: Runtime.NODEJS_18_X, 20: Runtime.NODEJS_20_X, + 22: Runtime.NODEJS_22_X, +}; + +export type FunctionArchitecture = 'x86_64' | 'arm64'; + +const architectureMap: Record = { + arm64: Architecture.ARM_64, + x86_64: Architecture.X86_64, }; diff --git a/packages/backend-function/src/function_construct_base.ts b/packages/backend-function/src/function_construct_base.ts new file mode 100644 index 00000000000..73de12e5dba --- /dev/null +++ b/packages/backend-function/src/function_construct_base.ts @@ -0,0 +1,58 @@ +import { Construct } from 'constructs'; +import { + BackendOutputStorageStrategy, + FunctionResources, + ResourceAccessAcceptor, + ResourceAccessAcceptorFactory, + ResourceProvider, +} from '@aws-amplify/plugin-types'; +import { Stack } from 'aws-cdk-lib'; +import { + FunctionOutput, + functionOutputKey, +} from '@aws-amplify/backend-output-schemas'; +import { AttributionMetadataStorage } from '@aws-amplify/backend-output-storage'; +import { fileURLToPath } from 'node:url'; + +const functionStackType = 'function-Lambda'; + +/** + * A base class for function constructs. + */ +export abstract class AmplifyFunctionBase + extends Construct + implements ResourceProvider, ResourceAccessAcceptorFactory +{ + readonly stack: Stack; + abstract resources: FunctionResources; + + abstract getResourceAccessAcceptor: () => ResourceAccessAcceptor; + + /** + * Creates base function construct. + */ + protected constructor( + scope: Construct, + id: string, + private readonly outputStorageStrategy: BackendOutputStorageStrategy + ) { + super(scope, id); + + this.stack = Stack.of(scope); + + new AttributionMetadataStorage().storeAttributionMetadata( + Stack.of(this), + functionStackType, + fileURLToPath(new URL('../package.json', import.meta.url)) + ); + } + + protected storeOutput = (): void => { + this.outputStorageStrategy.appendToBackendOutputList(functionOutputKey, { + version: '1', + payload: { + definedFunctions: this.resources.lambda.functionName, + }, + }); + }; +} diff --git a/packages/backend-function/src/function_env_translator.test.ts b/packages/backend-function/src/function_env_translator.test.ts index c20535b61a7..2bccfb39097 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,15 @@ 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, + maxRetries: 3, + }); + }); + 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 d988e06fec4..9916ef2ee08 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,21 @@ -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, + maxRetries: 3, + }); + }); + void it('generates a type definition file', () => { const fsOpenSyncMock = mock.method(fs, 'openSync'); const fsWriteFileSyncMock = mock.method(fs, 'writeFileSync', () => null); @@ -57,7 +67,6 @@ void describe('FunctionEnvironmentTypeGenerator', () => { }); void it('generated type definition file has valid syntax', async () => { - const targetDirectory = await fsp.mkdtemp('func_env_type_gen_test'); const functionEnvironmentTypeGenerator = new FunctionEnvironmentTypeGenerator('testFunction'); const filePath = `${process.cwd()}/.amplify/generated/env/testFunction.ts`; @@ -66,7 +75,37 @@ void describe('FunctionEnvironmentTypeGenerator', () => { // import to validate syntax of type definition file await import(pathToFileURL(filePath).toString()); + }); + + 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'); - await fsp.rm(targetDirectory, { recursive: true, force: true }); + 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 ea650e26c9b..d01a701a190 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/index.internal.ts b/packages/backend-function/src/index.internal.ts new file mode 100644 index 00000000000..cc29b4a317a --- /dev/null +++ b/packages/backend-function/src/index.internal.ts @@ -0,0 +1,10 @@ +export * from './index.js'; +// eslint-disable-next-line @typescript-eslint/naming-convention +import * as __export__runtime from './runtime/index.js'; + +/* + Api-extractor does not ([yet](https://github.com/microsoft/rushstack/issues/1596)) support multiple package entry points + Because this package has a submodule export, we are working around this issue by including that export here and directing api-extract to this entry point instead + This allows api-extractor to pick up the submodule exports in its analysis + */ +export { __export__runtime }; diff --git a/packages/backend-function/src/index.ts b/packages/backend-function/src/index.ts index cb95b1ef8d8..8d9b853ad39 100644 --- a/packages/backend-function/src/index.ts +++ b/packages/backend-function/src/index.ts @@ -1 +1,3 @@ export * from './factory.js'; +import { ProvidedFunctionProps } from './provided_function_factory.js'; +export { ProvidedFunctionProps }; 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 00000000000..d76e6a9f417 --- /dev/null +++ b/packages/backend-function/src/layer_parser.test.ts @@ -0,0 +1,261 @@ +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 { Match, Template } from 'aws-cdk-lib/assertions'; +import fsp from 'fs/promises'; +import assert from 'node:assert'; +import path from 'node:path'; +import { after, beforeEach, describe, it } from 'node:test'; +import { defineFunction } from './factory.js'; + +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, + maxRetries: 3, + }); + }); + + 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 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], + }); + }); + + void it('accepts and converts name:version format to ARN', () => { + const functionFactory = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'lambdaWithNameVersionLayer', + layers: { + myLayer: 'my-layer:1', + }, + }); + const lambda = functionFactory.getInstance(getInstanceProps); + const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + + template.hasResourceProperties('AWS::Lambda::Function', { + Handler: 'index.handler', + Layers: [ + { + 'Fn::Join': [ + '', + [ + 'arn:aws:lambda:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':layer:my-layer:1', + ], + ], + }, + ], + }); + }); + + void it('throws an error for invalid name:version format', () => { + const invalidFormat = 'my-layer'; // missing version number + const functionFactory = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'lambdaWithInvalidNameVersion', + layers: { + myLayer: invalidFormat, + }, + }); + + assert.throws( + () => functionFactory.getInstance(getInstanceProps), + (error: AmplifyUserError) => { + assert.strictEqual( + error.message, + `Invalid format for layer: ${invalidFormat}` + ); + assert.ok(error.resolution); + return true; + } + ); + }); + + void it('accepts mixed format of ARNs and name:version', () => { + const fullArn = + 'arn:aws:lambda:us-east-1:123456789012:layer:full-arn-layer:1'; + const functionFactory = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'lambdaWithMixedLayerFormats', + layers: { + fullArnLayer: fullArn, + nameVersionLayer: 'name-version-layer:2', + }, + }); + const lambda = functionFactory.getInstance(getInstanceProps); + const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + + template.hasResourceProperties('AWS::Lambda::Function', { + Handler: 'index.handler', + Layers: Match.arrayWith([fullArn]), + }); + }); +}); diff --git a/packages/backend-function/src/layer_parser.ts b/packages/backend-function/src/layer_parser.ts new file mode 100644 index 00000000000..e5c25361f74 --- /dev/null +++ b/packages/backend-function/src/layer_parser.ts @@ -0,0 +1,99 @@ +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]+' + ); + private nameVersionPattern = new RegExp('^[a-zA-Z0-9-_]+:[0-9]+$'); + + /** + * Creates a new FunctionLayerArnParser + * @param region - AWS region + * @param account - AWS account ID + */ + constructor( + private readonly region: string, + private readonly account: string + ) {} + + /** + * Parse the layers for a function + * @param layers - Layers to be attached to the function. Each layer can be specified as either: + * - A full ARN (arn:aws:lambda:::layer::) + * - A name:version format (e.g., "my-layer:1") + * @param functionName - Name of the function + * @returns Valid layers for the function with resolved ARNs + * @throws AmplifyUserError if the layer ARN or name:version format 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, value] of Object.entries(layers)) { + let arn: string; + + if (this.isValidLayerArn(value)) { + // If it's already a valid ARN, use it as is + arn = value; + } else if (this.isValidNameVersion(value)) { + // If it's in name:version format, construct the ARN using provided region and account + const [name, version] = value.split(':'); + arn = `arn:aws:lambda:${this.region}:${this.account}:layer:${name}:${version}`; + } else { + throw new AmplifyUserError('InvalidLayerFormatError', { + message: `Invalid format for layer: ${value}`, + resolution: `Layer must be either a full ARN (arn:aws:lambda:::layer::) or name:version format for function: ${functionName}`, + }); + } + + // Ensure we don't add duplicate ARNs + if (!uniqueArns.has(arn)) { + uniqueArns.add(arn); + validLayers[key] = arn; + } + } + + this.validateLayerCount(uniqueArns); + return validLayers; + } + + /** + * Validate the ARN format for a Lambda Layer + * @param arn - The ARN string to validate + * @returns boolean indicating if the ARN format is valid + */ + private isValidLayerArn(arn: string): boolean { + return this.arnPattern.test(arn); + } + + /** + * Validate the name:version format for a Lambda Layer + * @param value - The string to validate in format "name:version" + * @returns boolean indicating if the format is valid + */ + private isValidNameVersion(value: string): boolean { + return this.nameVersionPattern.test(value); + } + + /** + * Validate the number of layers attached to a function + * @param uniqueArns - Set of unique layer ARNs + * @throws AmplifyUserError if the number of layers exceeds 5 + * @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-function/src/logging_options_parser.test.ts b/packages/backend-function/src/logging_options_parser.test.ts new file mode 100644 index 00000000000..4d6abec9589 --- /dev/null +++ b/packages/backend-function/src/logging_options_parser.test.ts @@ -0,0 +1,56 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { FunctionLoggingOptions } from './factory.js'; +import { + CDKLoggingOptions, + convertLoggingOptionsToCDK, +} from './logging_options_parser.js'; +import { ApplicationLogLevel, LoggingFormat } from 'aws-cdk-lib/aws-lambda'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; + +type TestCase = { + input: FunctionLoggingOptions; + expectedOutput: CDKLoggingOptions; +}; + +const testCases: Array = [ + { + input: {}, + expectedOutput: { + format: undefined, + level: undefined, + retention: undefined, + }, + }, + { + input: { + format: 'text', + retention: '13 months', + }, + expectedOutput: { + format: LoggingFormat.TEXT, + retention: RetentionDays.THIRTEEN_MONTHS, + level: undefined, + }, + }, + { + input: { + format: 'json', + level: 'debug', + }, + expectedOutput: { + format: LoggingFormat.JSON, + retention: undefined, + level: ApplicationLogLevel.DEBUG, + }, + }, +]; + +void describe('LoggingOptions converter', () => { + testCases.forEach((testCase, index) => { + void it(`converts to cdk options[${index}]`, () => { + const convertedOptions = convertLoggingOptionsToCDK(testCase.input); + assert.deepStrictEqual(convertedOptions, testCase.expectedOutput); + }); + }); +}); diff --git a/packages/backend-function/src/logging_options_parser.ts b/packages/backend-function/src/logging_options_parser.ts new file mode 100644 index 00000000000..ce5693d6253 --- /dev/null +++ b/packages/backend-function/src/logging_options_parser.ts @@ -0,0 +1,48 @@ +import { FunctionLoggingOptions } from './factory.js'; +import { ApplicationLogLevel, LoggingFormat } from 'aws-cdk-lib/aws-lambda'; +import { + LogLevelConverter, + LogRetentionConverter, +} from '@aws-amplify/platform-core/cdk'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; + +export type CDKLoggingOptions = { + level?: ApplicationLogLevel; + retention?: RetentionDays; + format?: LoggingFormat; +}; + +/** + * Converts logging options to CDK. + */ +export const convertLoggingOptionsToCDK = ( + loggingOptions: FunctionLoggingOptions +): CDKLoggingOptions => { + let level: ApplicationLogLevel | undefined = undefined; + if ('level' in loggingOptions) { + level = new LogLevelConverter().toCDKLambdaApplicationLogLevel( + loggingOptions.level + ); + } + const retention = new LogRetentionConverter().toCDKRetentionDays( + loggingOptions.retention + ); + const format = convertFormat(loggingOptions.format); + + return { + level, + retention, + format, + }; +}; + +const convertFormat = (format: 'json' | 'text' | undefined) => { + switch (format) { + case undefined: + return undefined; + case 'json': + return LoggingFormat.JSON; + case 'text': + return LoggingFormat.TEXT; + } +}; diff --git a/packages/backend-function/src/provided_function_factory.ts b/packages/backend-function/src/provided_function_factory.ts new file mode 100644 index 00000000000..2d6becb0117 --- /dev/null +++ b/packages/backend-function/src/provided_function_factory.ts @@ -0,0 +1,142 @@ +import { + AmplifyFunction, + AmplifyResourceGroupName, + BackendOutputStorageStrategy, + ConstructContainerEntryGenerator, + ConstructFactory, + ConstructFactoryGetInstanceProps, + FunctionResources, + GenerateContainerEntryProps, + ResourceAccessAcceptor, +} from '@aws-amplify/plugin-types'; +import { Construct } from 'constructs'; +import { CfnFunction, IFunction } from 'aws-cdk-lib/aws-lambda'; +import { FunctionOutput } from '@aws-amplify/backend-output-schemas'; +import { Tags } from 'aws-cdk-lib'; +import { AmplifyUserError, TagName } from '@aws-amplify/platform-core'; +import { AmplifyFunctionBase } from './function_construct_base.js'; +import { FunctionResourceAccessAcceptor } from './resource_access_acceptor.js'; + +export type ProvidedFunctionProps = { + /** + * Group the function with existing Amplify resources or separate the function into its own group. + * @default 'function' // grouping with other Amplify functions + * @example + * resourceGroupName: 'auth' // to group an auth trigger with an auth resource + */ + resourceGroupName?: AmplifyResourceGroupName; +}; + +/** + * Adapts provided CDK function as Amplify function. + */ +export class ProvidedFunctionFactory + implements ConstructFactory +{ + private generator: ConstructContainerEntryGenerator; + + /** + * Creates provided function factory. + */ + constructor( + private readonly functionProvider: (scope: Construct) => IFunction, + private readonly props?: ProvidedFunctionProps + ) {} + + /** + * Creates a function instance. + */ + getInstance(props: ConstructFactoryGetInstanceProps): AmplifyFunction { + if (!this.generator) { + this.generator = new ProvidedFunctionGenerator( + this.functionProvider, + props.outputStorageStrategy, + this.props + ); + } + return props.constructContainer.getOrCompute( + this.generator + ) as AmplifyFunction; + } +} + +class ProvidedFunctionGenerator implements ConstructContainerEntryGenerator { + readonly resourceGroupName: AmplifyResourceGroupName; + + constructor( + private readonly functionProvider: (scope: Construct) => IFunction, + private readonly outputStorageStrategy: BackendOutputStorageStrategy, + props?: ProvidedFunctionProps + ) { + this.resourceGroupName = props?.resourceGroupName ?? 'function'; + } + + generateContainerEntry = ({ scope }: GenerateContainerEntryProps) => { + let providedFunction: IFunction; + try { + providedFunction = this.functionProvider(scope); + } catch (e) { + if ( + e instanceof Error && + (e.message.includes('docker exited with status 1') || + e.message.includes('docker ENOENT')) + ) { + throw new AmplifyUserError( + 'CustomFunctionProviderDockerError', + { + message: 'Failed to instantiate custom function provider', + resolution: + 'See https://docs.amplify.aws/react/build-a-backend/functions/custom-functions for more details about current limitations and troubleshooting steps.', + }, + e + ); + } else { + throw new AmplifyUserError( + 'CustomFunctionProviderError', + { + message: 'Failed to instantiate custom function provider', + resolution: + 'Check the definition of your custom function provided in `defineFunction` and refer to the logs for more information. See https://docs.amplify.aws/react/build-a-backend/functions/custom-functions for more details.', + }, + e instanceof Error ? e : undefined + ); + } + } + return new ProvidedAmplifyFunction( + scope, + `${providedFunction.node.id}-provided`, + this.outputStorageStrategy, + providedFunction + ); + }; +} + +class ProvidedAmplifyFunction extends AmplifyFunctionBase { + readonly resources: FunctionResources; + constructor( + scope: Construct, + id: string, + outputStorageStrategy: BackendOutputStorageStrategy, + providedFunction: IFunction + ) { + super(scope, id, outputStorageStrategy); + + const cfnFunction = providedFunction.node.findChild( + 'Resource' + ) as CfnFunction; + + Tags.of(cfnFunction).add(TagName.FRIENDLY_NAME, providedFunction.node.id); + + this.resources = { + lambda: providedFunction, + cfnResources: { + cfnFunction, + }, + }; + + this.storeOutput(); + } + + getResourceAccessAcceptor = (): ResourceAccessAcceptor => + new FunctionResourceAccessAcceptor(this); +} diff --git a/packages/backend-function/src/resource_access_acceptor.ts b/packages/backend-function/src/resource_access_acceptor.ts new file mode 100644 index 00000000000..e3ba57aaf66 --- /dev/null +++ b/packages/backend-function/src/resource_access_acceptor.ts @@ -0,0 +1,43 @@ +import { + ResourceAccessAcceptor, + SsmEnvironmentEntry, +} from '@aws-amplify/plugin-types'; +import { FunctionEnvironmentTranslator } from './function_env_translator.js'; +import { AmplifyFunctionBase } from './function_construct_base.js'; +import { Policy } from 'aws-cdk-lib/aws-iam'; + +/** + * A function resource acceptor. + */ +export class FunctionResourceAccessAcceptor implements ResourceAccessAcceptor { + readonly identifier: string; + + /** + * Creates function resource acceptor. + */ + constructor( + private readonly func: AmplifyFunctionBase, + private readonly functionEnvironmentTranslator?: FunctionEnvironmentTranslator + ) { + this.identifier = `${func.node.id}LambdaResourceAccessAcceptor`; + } + + acceptResourceAccess = ( + policy: Policy, + ssmEnvironmentEntries: SsmEnvironmentEntry[] + ) => { + const role = this.func.resources.lambda.role; + if (!role) { + // This should never happen since we are using the Function L2 construct + throw new Error( + 'No execution role found to attach lambda permissions to' + ); + } + policy.attachToRole(role); + if (this.functionEnvironmentTranslator) { + for (const { name, path } of ssmEnvironmentEntries) { + this.functionEnvironmentTranslator.addSsmEnvironmentEntry(name, path); + } + } + }; +} diff --git a/packages/backend-function/src/runtime/get_amplify_clients_configuration.test.ts b/packages/backend-function/src/runtime/get_amplify_clients_configuration.test.ts new file mode 100644 index 00000000000..c39d45106d0 --- /dev/null +++ b/packages/backend-function/src/runtime/get_amplify_clients_configuration.test.ts @@ -0,0 +1,139 @@ +import { beforeEach, describe, it, mock } from 'node:test'; +import assert from 'assert'; +import { NoSuchKey, S3, S3ServiceException } from '@aws-sdk/client-s3'; + +import { getAmplifyDataClientConfig } from './get_amplify_clients_configuration.js'; + +const validDefaultEnv = { + AMPLIFY_DATA_MODEL_INTROSPECTION_SCHEMA_BUCKET_NAME: + 'TEST_VALUE for AMPLIFY_DATA_MODEL_INTROSPECTION_SCHEMA_BUCKET_NAME', + AMPLIFY_DATA_MODEL_INTROSPECTION_SCHEMA_KEY: + 'TEST_VALUE for AMPLIFY_DATA_MODEL_INTROSPECTION_SCHEMA_KEY', + AWS_ACCESS_KEY_ID: 'TEST_VALUE for AWS_ACCESS_KEY_ID', + AWS_SECRET_ACCESS_KEY: 'TEST_VALUE for AWS_SECRET_ACCESS_KEY', + AWS_SESSION_TOKEN: 'TEST_VALUE for AWS_SESSION_TOKEN', + AWS_REGION: 'TEST_VALUE for AWS_REGION', + AMPLIFY_DATA_GRAPHQL_ENDPOINT: 'TEST_VALUE for AMPLIFY_DATA_GRAPHQL_ENDPOINT', + AMPLIFY_DATA_DEFAULT_NAME: 'AmplifyData', +}; + +const validNamedEnv = { + AMPLIFY_DATA_TEST_NAME_MODEL_INTROSPECTION_SCHEMA_BUCKET_NAME: + 'TEST_VALUE for AMPLIFY_DATA_MODEL_INTROSPECTION_SCHEMA_BUCKET_NAME', + AMPLIFY_DATA_TEST_NAME_MODEL_INTROSPECTION_SCHEMA_KEY: + 'TEST_VALUE for AMPLIFY_DATA_MODEL_INTROSPECTION_SCHEMA_KEY', + AWS_ACCESS_KEY_ID: 'TEST_VALUE for AWS_ACCESS_KEY_ID', + AWS_SECRET_ACCESS_KEY: 'TEST_VALUE for AWS_SECRET_ACCESS_KEY', + AWS_SESSION_TOKEN: 'TEST_VALUE for AWS_SESSION_TOKEN', + AWS_REGION: 'TEST_VALUE for AWS_REGION', + AMPLIFY_DATA_TEST_NAME_GRAPHQL_ENDPOINT: + 'TEST_VALUE for AMPLIFY_DATA_GRAPHQL_ENDPOINT', + AMPLIFY_DATA_DEFAULT_NAME: 'AmplifyDataTestName', +}; + +let mockS3Client: S3; + +void describe('getAmplifyDataClientConfig', () => { + beforeEach(() => { + mockS3Client = new S3(); + }); + + [ + { + name: 'no set name', + validEnv: validDefaultEnv, + }, + { + name: 'an explicit name', + validEnv: validNamedEnv, + }, + ].forEach(({ name, validEnv }) => { + void describe(`env variable with ${name} for the data backend`, () => { + void it('raises a custom error message when the model introspection schema is missing from the s3 bucket', async () => { + const s3ClientSendMock = mock.method(mockS3Client, 'send', async () => { + throw new NoSuchKey({ message: 'TEST_ERROR', $metadata: {} }); + }); + mock.method(mockS3Client, 'send', s3ClientSendMock); + + await assert.rejects( + async () => await getAmplifyDataClientConfig(validEnv, mockS3Client), + new Error( + 'Error retrieving the schema from S3. Please confirm that your project has a `defineData` included in the `defineBackend` definition.' + ) + ); + }); + + void it('raises a custom error message when there is a S3ServiceException error retrieving the model introspection schema from the s3 bucket', async () => { + const s3ClientSendMock = mock.method(mockS3Client, 'send', async () => { + throw new S3ServiceException({ + name: 'TEST_ERROR', + message: 'TEST_MESSAGE', + $fault: 'server', + $metadata: {}, + }); + }); + mock.method(mockS3Client, 'send', s3ClientSendMock); + + await assert.rejects( + async () => await getAmplifyDataClientConfig(validEnv, mockS3Client), + new Error( + 'Error retrieving the schema from S3. You may need to grant this function authorization on the schema. TEST_ERROR: TEST_MESSAGE.' + ) + ); + }); + + void it('re-raises a non-S3 error received when retrieving the model introspection schema from the s3 bucket', async () => { + const s3ClientSendMock = mock.method(mockS3Client, 'send', async () => { + throw new Error('Test Error'); + }); + mock.method(mockS3Client, 'send', s3ClientSendMock); + + await assert.rejects( + async () => await getAmplifyDataClientConfig(validEnv, mockS3Client), + new Error('Test Error') + ); + }); + + void it('returns the expected libraryOptions and resourceConfig values in the happy case', async () => { + const s3ClientSendMock = mock.method(mockS3Client, 'send', () => { + return Promise.resolve({ + Body: { + transformToString: () => + JSON.stringify({ testSchema: 'TESTING' }), + }, + }); + }); + mock.method(mockS3Client, 'send', s3ClientSendMock); + + const { resourceConfig, libraryOptions } = + await getAmplifyDataClientConfig(validEnv, mockS3Client); + + assert.deepEqual( + await libraryOptions.Auth.credentialsProvider.getCredentialsAndIdentityId?.(), + { + credentials: { + accessKeyId: 'TEST_VALUE for AWS_ACCESS_KEY_ID', + secretAccessKey: 'TEST_VALUE for AWS_SECRET_ACCESS_KEY', + sessionToken: 'TEST_VALUE for AWS_SESSION_TOKEN', + }, + } + ); + assert.deepEqual( + await libraryOptions.Auth.credentialsProvider.clearCredentialsAndIdentityId?.(), + undefined + ); + + assert.deepEqual(resourceConfig, { + API: { + GraphQL: { + endpoint: 'TEST_VALUE for AMPLIFY_DATA_GRAPHQL_ENDPOINT', + region: 'TEST_VALUE for AWS_REGION', + defaultAuthMode: 'iam', + modelIntrospection: { testSchema: 'TESTING' }, + }, + }, + }); + }); + }); + }); +}); diff --git a/packages/backend-function/src/runtime/get_amplify_clients_configuration.ts b/packages/backend-function/src/runtime/get_amplify_clients_configuration.ts new file mode 100644 index 00000000000..51e1132ea7a --- /dev/null +++ b/packages/backend-function/src/runtime/get_amplify_clients_configuration.ts @@ -0,0 +1,193 @@ +import { NamingConverter } from '@aws-amplify/platform-core'; +import { + GetObjectCommand, + NoSuchKey, + S3Client, + S3ServiceException, +} from '@aws-sdk/client-s3'; + +const dataKeyNameContent = '_MODEL_INTROSPECTION_SCHEMA_KEY'; +const dataBucketNameContent = '_MODEL_INTROSPECTION_SCHEMA_BUCKET_NAME'; +const dataEndpointNameContent = '_GRAPHQL_ENDPOINT'; + +export type DataClientEnv = { + /* eslint-disable @typescript-eslint/naming-convention */ + AWS_ACCESS_KEY_ID: string; + AWS_SECRET_ACCESS_KEY: string; + AWS_SESSION_TOKEN: string; + AWS_REGION: string; + AMPLIFY_DATA_DEFAULT_NAME: string; + /* eslint-enable @typescript-eslint/naming-convention */ +} & Record; + +type DataEnvExtension = { + dataBucket: string; + dataKey: string; + dataEndpoint: string; +}; + +type ExtendedAmplifyClientEnv = DataClientEnv & DataEnvExtension; + +/* eslint-disable @typescript-eslint/naming-convention */ +export type ResourceConfig = { + API: { + GraphQL: { + endpoint: string; + region: string; + defaultAuthMode: 'iam'; + // Using `any` to avoid reproducing 100+ lines of typing to match the expected shape defined in aws-amplify: + // https://github.com/aws-amplify/amplify-js/blob/main/packages/core/src/singleton/API/types.ts#L143-L153 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + modelIntrospection: any; + }; + }; +}; +/* eslint-enable @typescript-eslint/naming-convention */ + +const getResourceConfig = ( + env: ExtendedAmplifyClientEnv, + modelIntrospectionSchema: object +): ResourceConfig => { + return { + API: { + GraphQL: { + endpoint: env.dataEndpoint, + region: env.AWS_REGION, + defaultAuthMode: 'iam' as const, + modelIntrospection: modelIntrospectionSchema, + }, + }, + }; +}; + +export type LibraryOptions = { + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + Auth: { + credentialsProvider: { + getCredentialsAndIdentityId: () => Promise<{ + credentials: { + accessKeyId: string; + secretAccessKey: string; + sessionToken: string; + }; + }>; + clearCredentialsAndIdentityId: () => void; + }; + }; +}; + +const getLibraryOptions = (env: DataClientEnv): LibraryOptions => { + return { + Auth: { + credentialsProvider: { + getCredentialsAndIdentityId: async () => ({ + credentials: { + accessKeyId: env.AWS_ACCESS_KEY_ID, + secretAccessKey: env.AWS_SECRET_ACCESS_KEY, + sessionToken: env.AWS_SESSION_TOKEN, + }, + }), + clearCredentialsAndIdentityId: () => { + /* noop */ + }, + }, + }, + }; +}; + +export type DataClientConfig = { + resourceConfig: ResourceConfig; + libraryOptions: LibraryOptions; +}; + +const extendEnv = ( + env: DataClientEnv & Record, + dataName: string +): ExtendedAmplifyClientEnv => { + const bucketName = `${dataName}${dataBucketNameContent}`; + const keyName = `${dataName}${dataKeyNameContent}`; + const endpointName = `${dataName}${dataEndpointNameContent}`; + if ( + !( + bucketName in env && + keyName in env && + endpointName in env && + typeof env[bucketName] === 'string' && + typeof env[keyName] === 'string' && + typeof env[endpointName] === 'string' + ) + ) { + throw new Error( + `The data environment variables are malformed. env=${JSON.stringify(env)}` + ); + } + + const dataBucket = env[bucketName] as string; + const dataKey = env[keyName] as string; + const dataEndpoint = env[endpointName] as string; + + return { + ...env, + dataBucket, + dataKey, + dataEndpoint, + }; +}; + +/** + * Generate the `resourceConfig` and `libraryOptions` need to configure + * Amplify for the data client in a lambda. + * + * Your function needs to be granted resource access on your schema for this to work + * `a.schema(...).authorization((allow) => [a.resource(myFunction)])` + * @param env - The environment variables for the data client + * @returns An object containing the `resourceConfig` and `libraryOptions` + */ +export const getAmplifyDataClientConfig = async ( + env: DataClientEnv, + s3Client?: S3Client +): Promise => { + if (!s3Client) { + s3Client = new S3Client(); + } + + const dataName = new NamingConverter().toScreamingSnakeCase( + env.AMPLIFY_DATA_DEFAULT_NAME + ); + const extendedEnv = extendEnv(env, dataName); + + let modelIntrospectionSchema: object; + + try { + const response = await s3Client.send( + new GetObjectCommand({ + Bucket: extendedEnv.dataBucket, + Key: extendedEnv.dataKey, + }) + ); + const modelIntrospectionSchemaJson = + await response.Body?.transformToString(); + modelIntrospectionSchema = JSON.parse(modelIntrospectionSchemaJson ?? '{}'); + } catch (caught) { + if (caught instanceof NoSuchKey) { + throw new Error( + 'Error retrieving the schema from S3. Please confirm that your project has a `defineData` included in the `defineBackend` definition.' + ); + } else if (caught instanceof S3ServiceException) { + throw new Error( + `Error retrieving the schema from S3. You may need to grant this function authorization on the schema. ${caught.name}: ${caught.message}.` + ); + } else { + throw caught; + } + } + + const libraryOptions = getLibraryOptions(env); + + const resourceConfig = getResourceConfig( + extendedEnv, + modelIntrospectionSchema + ); + + return { resourceConfig, libraryOptions }; +}; diff --git a/packages/backend-function/src/runtime/index.ts b/packages/backend-function/src/runtime/index.ts new file mode 100644 index 00000000000..675c3bee51d --- /dev/null +++ b/packages/backend-function/src/runtime/index.ts @@ -0,0 +1,7 @@ +export { + getAmplifyDataClientConfig, + DataClientConfig, + DataClientEnv, + LibraryOptions, + ResourceConfig, +} from './get_amplify_clients_configuration.js'; diff --git a/packages/backend-output-schemas/API.md b/packages/backend-output-schemas/API.md index 41fbd8abeda..23c5a41d89f 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 ff823df4ef7..0212c02ffd6 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 e72ee388c35..93e0106dcbc 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 00000000000..29b0d413976 --- /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 00000000000..cb50b29cda1 --- /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 af4d65cc8c3..a17c2cd2370 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 ec7cef02096..11bfb14cd2d 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 5095714a813..0827b34d69e 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 51e28f0faab..d0990132cfd 100644 --- a/packages/backend-output-storage/CHANGELOG.md +++ b/packages/backend-output-storage/CHANGELOG.md @@ -1,5 +1,33 @@ # @aws-amplify/backend-output-storage +## 1.1.4 + +### Patch Changes + +- 72b2fe0: update aws-cdk lib to ^2.168.0 +- Updated dependencies [cfdc854] +- Updated dependencies [72b2fe0] +- Updated dependencies [65abf6a] +- Updated dependencies [f6ba240] + - @aws-amplify/platform-core@1.3.0 + - @aws-amplify/plugin-types@1.6.0 + +## 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 a854f673d9a..e7489845c9f 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.4", "type": "commonjs", "publishConfig": { "access": "public" @@ -20,10 +20,10 @@ "license": "Apache-2.0", "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/platform-core": "^1.3.0", + "@aws-amplify/plugin-types": "^1.6.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0" + "aws-cdk-lib": "^2.168.0" } } diff --git a/packages/backend-platform-test-stubs/CHANGELOG.md b/packages/backend-platform-test-stubs/CHANGELOG.md index 6c1a63bd463..792c3c28ca9 100644 --- a/packages/backend-platform-test-stubs/CHANGELOG.md +++ b/packages/backend-platform-test-stubs/CHANGELOG.md @@ -1,5 +1,30 @@ # @aws-amplify/backend-platform-test-stubs +## 0.3.7 + +### Patch Changes + +- 72b2fe0: update aws-cdk lib to ^2.168.0 +- Updated dependencies [72b2fe0] +- Updated dependencies [f6ba240] + - @aws-amplify/plugin-types@1.6.0 + +## 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 409cb649109..b9eea93ef71 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.7", "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.6.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.0.0" } } diff --git a/packages/backend-secret/CHANGELOG.md b/packages/backend-secret/CHANGELOG.md index ddf0cac6187..3ff634508c8 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 56835c984dd..8d3f372b9c8 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 c33da91cc17..fbde647a4d6 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 c8c34219756..cd5b44563fb 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 93e42d479bf..88c29dd5981 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 85e771db1c2..7d18d1c4891 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 e7472b5770f..aa65ada47b2 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 2be32cc7492..24e36df757f 100644 --- a/packages/backend-storage/CHANGELOG.md +++ b/packages/backend-storage/CHANGELOG.md @@ -1,5 +1,60 @@ # @aws-amplify/backend-storage +## 1.2.4 + +### Patch Changes + +- 72b2fe0: update aws-cdk lib to ^2.168.0 +- Updated dependencies [72b2fe0] +- Updated dependencies [f6ba240] + - @aws-amplify/backend-output-storage@1.1.4 + - @aws-amplify/plugin-types@1.6.0 + +## 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 c1bcb05b335..d824e9c4749 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.4", "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.4", + "@aws-amplify/plugin-types": "^1.6.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.7", + "@aws-amplify/platform-core": "^1.3.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.0.0" } } diff --git a/packages/backend-storage/src/access_builder.ts b/packages/backend-storage/src/access_builder.ts index 129c8b64ee3..7309e1efa73 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 8ee0e17bd51..7588e609976 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 1cbdb716701..4308b3cecaa 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 d3f425a6d2b..0ffd504d67a 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 49eff9ba1e6..4c5212c9649 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 8c7e53f27ac..8daaf2af48a 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 ffcc031de31..75cecc30baf 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 e4b4c0e22a3..35d2553ab42 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 90fbd59bf32..441b54bb86c 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 7487f0864d5..cccf5ba94c9 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 9e9a9fcfa03..dd448937a53 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 39f0a4dead3..caf7abc1c05 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 1cb83ee09f3..d677b05e814 100644 --- a/packages/backend/API.md +++ b/packages/backend/API.md @@ -25,13 +25,22 @@ import { defineFunction } from '@aws-amplify/backend-function'; import { defineStorage } from '@aws-amplify/backend-storage'; import { FunctionResources } from '@aws-amplify/plugin-types'; import { GenerateContainerEntryProps } from '@aws-amplify/plugin-types'; +import { getAmplifyDataClientConfig } from '@aws-amplify/backend-function/runtime'; 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'; import { SsmEnvironmentEntry } from '@aws-amplify/plugin-types'; import { Stack } from 'aws-cdk-lib'; +declare namespace __export__function__runtime { + export { + getAmplifyDataClientConfig + } +} +export { __export__function__runtime } + export { a } export { AuthCfnResources } @@ -49,6 +58,7 @@ export type Backend = BackendBase & { export type BackendBase = { createStack: (name: string) => Stack; addOutput: (clientConfigPart: DeepPartialAmplifyGeneratedConfigs) => void; + stack: Stack; }; export { BackendOutputEntry } @@ -89,6 +99,8 @@ export { GenerateContainerEntryProps } export { ImportPathVerifier } +export { referenceAuth } + export { ResourceProvider } // @public diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index cc029c0cb07..08939e209b4 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,273 @@ # @aws-amplify/backend +## 1.13.0 + +### Minor Changes + +- 3f521c3: add custom provided function support to define function + +### Patch Changes + +- c5d54c2: Update getAmplifyDataClient to have strict env type and remove narrowing logic +- Updated dependencies [c5d54c2] +- Updated dependencies [3f521c3] +- Updated dependencies [a712983] + - @aws-amplify/backend-function@1.12.0 + - @aws-amplify/platform-core@1.5.1 + +## 1.12.0 + +### Minor Changes + +- a7506f9: added data logging api to defineData +- a7506f9: adds support for architecture property on defineFunction + +### Patch Changes + +- Updated dependencies [a7506f9] +- Updated dependencies [a7506f9] +- Updated dependencies [a7506f9] +- Updated dependencies [a7506f9] + - @aws-amplify/client-config@1.5.5 + - @aws-amplify/backend-function@1.11.0 + - @aws-amplify/platform-core@1.5.0 + - @aws-amplify/backend-data@1.4.0 + - @aws-amplify/plugin-types@1.7.0 + +## 1.11.0 + +### Minor Changes + +- fbf209e: Add GraphQL API ID and Amplify environment name to custom JS resolver stash + +### Patch Changes + +- 07fe7d4: Allow apiKeyAuthorizationMode to be undefined if defaultAuthorizationMode is apiKey +- Updated dependencies [07fe7d4] +- Updated dependencies [fbf209e] + - @aws-amplify/backend-data@1.3.0 + +## 1.10.0 + +### Minor Changes + +- 560878f: updates layer to also use layername:version + +### Patch Changes + +- Updated dependencies [95942c5] +- Updated dependencies [3cf0738] +- Updated dependencies [f679cf6] +- Updated dependencies [d32e4cd] +- Updated dependencies [560878f] +- Updated dependencies [f193105] + - @aws-amplify/platform-core@1.4.0 + - @aws-amplify/client-config@1.5.4 + - @aws-amplify/backend-function@1.10.0 + - @aws-amplify/backend-data@1.2.3 + +## 1.9.0 + +### Minor Changes + +- 5cbe318: Add lambda data client +- 72b2fe0: Add support to `@aws-amplify/backend-function` for Node 22 + + Add support to `@aws-amplify/backend-function` for Node 22, which is a [supported Lambda runtime](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html#runtime-deprecation-levels) that was added in [`aws-cdk-lib/aws-lambda` version `2.168.0`](https://github.com/aws/aws-cdk/releases/tag/v2.168.0) on November 20th, 2024 + +- 65abf6a: Add options to control log settings + +### Patch Changes + +- 72b2fe0: update aws-cdk lib to ^2.168.0 +- e0e62bd: Backend Secrets now use a single custom resource to reduce concurrent lambda executions. +- Updated dependencies [5cbe318] +- Updated dependencies [cfdc854] +- Updated dependencies [72b2fe0] +- Updated dependencies [72b2fe0] +- Updated dependencies [65abf6a] +- Updated dependencies [d227f96] +- Updated dependencies [f6ba240] +- Updated dependencies [d227f96] + - @aws-amplify/backend-function@1.9.0 + - @aws-amplify/backend-data@1.2.2 + - @aws-amplify/platform-core@1.3.0 + - @aws-amplify/backend-output-storage@1.1.4 + - @aws-amplify/backend-storage@1.2.4 + - @aws-amplify/client-config@1.5.3 + - @aws-amplify/backend-auth@1.4.2 + - @aws-amplify/plugin-types@1.6.0 + +## 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 a411301bfe4..c021c33d880 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.13.0", "type": "module", "publishConfig": { "access": "public" @@ -11,6 +11,11 @@ "import": "./lib/index.js", "require": "./lib/index.js" }, + "./function/runtime": { + "types": "./lib/function/runtime/index.d.ts", + "import": "./lib/function/runtime/index.js", + "require": "./lib/function/runtime/index.js" + }, "./types/platform": { "types": "./lib/types/platform.d.ts" } @@ -25,22 +30,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-sdk/client-amplify": "^3.624.0", - "lodash.snakecase": "^4.1.1" + "@aws-amplify/data-schema": "^1.13.4", + "@aws-amplify/backend-auth": "^1.4.2", + "@aws-amplify/backend-function": "^1.12.0", + "@aws-amplify/backend-data": "^1.4.0", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/backend-output-storage": "^1.1.4", + "@aws-amplify/backend-secret": "^1.1.4", + "@aws-amplify/backend-storage": "^1.2.4", + "@aws-amplify/client-config": "^1.5.5", + "@aws-amplify/platform-core": "^1.5.1", + "@aws-amplify/plugin-types": "^1.7.0", + "@aws-sdk/client-amplify": "^3.624.0" }, "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.0.0" }, "devDependencies": { diff --git a/packages/backend/src/backend.ts b/packages/backend/src/backend.ts index a4e767ada23..bd2d5d3d2f7 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 4016e50b782..17567b85a8b 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 d9bf2f545a3..3a7218b3aa4 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 707cb7f5373..a24b898f570 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 4c7475658bc..03691563aad 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/backend-secret/backend_secret.ts b/packages/backend/src/engine/backend-secret/backend_secret.ts index bb12316859a..af08742d3b5 100644 --- a/packages/backend/src/engine/backend-secret/backend_secret.ts +++ b/packages/backend/src/engine/backend-secret/backend_secret.ts @@ -16,7 +16,7 @@ export class CfnTokenBackendSecret implements BackendSecret { * The name of the secret to fetch. */ constructor( - private readonly name: string, + private readonly secretName: string, private readonly secretResourceFactory: BackendSecretFetcherFactory ) {} /** @@ -28,11 +28,11 @@ export class CfnTokenBackendSecret implements BackendSecret { ): SecretValue => { const secretResource = this.secretResourceFactory.getOrCreate( scope, - this.name, + this.secretName, backendIdentifier ); - const val = secretResource.getAttString('secretValue'); + const val = secretResource.getAttString(`${this.secretName}`); return SecretValue.unsafePlainText(val); // safe since 'val' is a cdk token. }; @@ -43,11 +43,11 @@ export class CfnTokenBackendSecret implements BackendSecret { return { branchSecretPath: ParameterPathConversions.toParameterFullPath( backendIdentifier, - this.name + this.secretName ), sharedSecretPath: ParameterPathConversions.toParameterFullPath( backendIdentifier.namespace, - this.name + this.secretName ), }; }; diff --git a/packages/backend/src/engine/backend-secret/backend_secret_fetcher_factory.test.ts b/packages/backend/src/engine/backend-secret/backend_secret_fetcher_factory.test.ts index b2d5cd1bb9a..b9b95006bac 100644 --- a/packages/backend/src/engine/backend-secret/backend_secret_fetcher_factory.test.ts +++ b/packages/backend/src/engine/backend-secret/backend_secret_fetcher_factory.test.ts @@ -9,7 +9,7 @@ import { } from './backend_secret_fetcher_factory.js'; import { BackendIdentifier } from '@aws-amplify/plugin-types'; -const secretResourceType = 'Custom::SecretFetcherResource'; +const secretResourceType = 'Custom::AmplifySecretFetcherResource'; const namespace = 'testId'; const name = 'testBranch'; const secretName1 = 'testSecretName1'; @@ -33,26 +33,18 @@ void describe('getOrCreate', () => { resourceFactory.getOrCreate(stack, secretName2, backendId); const template = Template.fromStack(stack); - template.resourceCountIs(secretResourceType, 2); - let customResources = template.findResources(secretResourceType, { + // only one custom resource is created that fetches all secrets + template.resourceCountIs(secretResourceType, 1); + const customResources = template.findResources(secretResourceType, { Properties: { namespace, name, - secretName: secretName1, + secretNames: [secretName1, secretName2], secretLastUpdated, }, }); assert.equal(Object.keys(customResources).length, 1); - customResources = template.findResources(secretResourceType, { - Properties: { - namespace, - name, - secretName: secretName2, - }, - }); - assert.equal(Object.keys(customResources).length, 1); - // only 1 secret fetcher lambda and 1 resource provider lambda are created. const providers = template.findResources('AWS::Lambda::Function'); const names = Object.keys(providers); @@ -67,6 +59,8 @@ void describe('getOrCreate', () => { void it('does not create duplicate resource for the same secret name', () => { const app = new App(); const stack = new Stack(app); + + // ensure only 1 resource is created even if this is called twice resourceFactory.getOrCreate(stack, secretName1, backendId); resourceFactory.getOrCreate(stack, secretName1, backendId); @@ -78,6 +72,7 @@ void describe('getOrCreate', () => { const body = customResources[resourceName]['Properties']; assert.strictEqual(body['namespace'], namespace); assert.strictEqual(body['name'], name); - assert.strictEqual(body['secretName'], secretName1); + assert.equal(body['secretNames'].length, 1); + assert.equal(body['secretNames'][0], secretName1); }); }); diff --git a/packages/backend/src/engine/backend-secret/backend_secret_fetcher_factory.ts b/packages/backend/src/engine/backend-secret/backend_secret_fetcher_factory.ts index f3fd0a2164e..097a6d36d37 100644 --- a/packages/backend/src/engine/backend-secret/backend_secret_fetcher_factory.ts +++ b/packages/backend/src/engine/backend-secret/backend_secret_fetcher_factory.ts @@ -1,18 +1,38 @@ import { Construct } from 'constructs'; import { BackendSecretFetcherProviderFactory } from './backend_secret_fetcher_provider_factory.js'; -import { CustomResource } from 'aws-cdk-lib'; +import { CustomResource, CustomResourceProps, Lazy } from 'aws-cdk-lib'; import { BackendIdentifier } from '@aws-amplify/plugin-types'; import { SecretResourceProps } from './lambda/backend_secret_fetcher_types.js'; /** * Resource provider ID for the backend secret resource. */ -export const SECRET_RESOURCE_PROVIDER_ID = 'SecretFetcherResourceProvider'; +export const SECRET_RESOURCE_PROVIDER_ID = + 'AmplifySecretFetcherResourceProvider'; + +class SecretFetcherCustomResource extends CustomResource { + private secrets: Set; + constructor( + scope: Construct, + id: string, + props: CustomResourceProps, + secrets: Set + ) { + super(scope, id, { + ...props, + }); + this.secrets = secrets; + } + + public addSecret = (secretName: string) => { + this.secrets.add(secretName); + }; +} /** * Type of the backend custom CFN resource. */ -const SECRET_RESOURCE_TYPE = `Custom::SecretFetcherResource`; +const SECRET_RESOURCE_TYPE = `Custom::AmplifySecretFetcherResource`; /** * The factory to create backend secret-fetcher resource. @@ -33,15 +53,18 @@ export class BackendSecretFetcherFactory { scope: Construct, secretName: string, backendIdentifier: BackendIdentifier - ): CustomResource => { - const secretResourceId = `${secretName}SecretFetcherResource`; + ): SecretFetcherCustomResource => { + const secretResourceId = `AmplifySecretFetcherResource`; const existingResource = scope.node.tryFindChild( secretResourceId - ) as CustomResource; + ) as SecretFetcherCustomResource; if (existingResource) { + existingResource.addSecret(secretName); return existingResource; } + const secrets: Set = new Set(); + secrets.add(secretName); const provider = this.secretProviderFactory.getOrCreateInstance( scope, @@ -59,16 +82,25 @@ export class BackendSecretFetcherFactory { namespace: backendIdentifier.namespace, name: backendIdentifier.name, type: backendIdentifier.type, - secretName: secretName, + secretNames: Lazy.list({ + produce: () => { + return Array.from(secrets); + }, + }), }; - return new CustomResource(scope, secretResourceId, { - serviceToken: provider.serviceToken, - properties: { - ...customResourceProps, - secretLastUpdated, // this property is only to trigger resource update event. + return new SecretFetcherCustomResource( + scope, + secretResourceId, + { + serviceToken: provider.serviceToken, + properties: { + ...customResourceProps, + secretLastUpdated, // this property is only to trigger resource update event. + }, + resourceType: SECRET_RESOURCE_TYPE, }, - resourceType: SECRET_RESOURCE_TYPE, - }); + secrets + ); }; } diff --git a/packages/backend/src/engine/backend-secret/lambda/backend_secret_fetcher.test.ts b/packages/backend/src/engine/backend-secret/lambda/backend_secret_fetcher.test.ts index 0cb0773446e..71c4e6cb500 100644 --- a/packages/backend/src/engine/backend-secret/lambda/backend_secret_fetcher.test.ts +++ b/packages/backend/src/engine/backend-secret/lambda/backend_secret_fetcher.test.ts @@ -43,7 +43,7 @@ const customResourceEventCommon = { ResourceType: 'AWS::CloudFormation::CustomResource', ResourceProperties: { ...testBackendIdentifier, - secretName: testSecretName, + secretNames: [testSecretName], ServiceToken: 'token', }, OldResourceProperties: {}, @@ -84,7 +84,7 @@ void describe('handleCreateUpdateEvent', () => { Promise.resolve(testSecret) ); const val = await handleCreateUpdateEvent(secretHandler, createCfnEvent); - assert.equal(val, testSecretValue); + assert.equal(val[testSecretName], testSecretValue); assert.equal(mockGetSecret.mock.callCount(), 1); assert.deepStrictEqual(mockGetSecret.mock.calls[0].arguments, [ @@ -120,7 +120,7 @@ void describe('handleCreateUpdateEvent', () => { ); const val = await handleCreateUpdateEvent(secretHandler, createCfnEvent); - assert.equal(val, testSecretValue); + assert.equal(val[testSecretName], testSecretValue); assert.equal(mockGetSecret.mock.callCount(), 2); assert.deepStrictEqual(mockGetSecret.mock.calls[0].arguments, [ @@ -145,7 +145,7 @@ void describe('handleCreateUpdateEvent', () => { } ); const val = await handleCreateUpdateEvent(secretHandler, createCfnEvent); - assert.equal(val, testSecretValue); + assert.equal(val[testSecretName], testSecretValue); assert.equal(mockGetSecret.mock.callCount(), 2); assert.deepStrictEqual(mockGetSecret.mock.calls[0].arguments, [ diff --git a/packages/backend/src/engine/backend-secret/lambda/backend_secret_fetcher.ts b/packages/backend/src/engine/backend-secret/lambda/backend_secret_fetcher.ts index 6c434c211d4..f240eb45dbe 100644 --- a/packages/backend/src/engine/backend-secret/lambda/backend_secret_fetcher.ts +++ b/packages/backend/src/engine/backend-secret/lambda/backend_secret_fetcher.ts @@ -22,11 +22,11 @@ export const handler = async ( const physicalId = event.RequestType === 'Create' ? randomUUID() : event.PhysicalResourceId; - let data: { secretValue: string } | undefined = undefined; + let data: Record | undefined = undefined; if (event.RequestType === 'Update' || event.RequestType === 'Create') { - const val = await handleCreateUpdateEvent(secretClient, event); + const secretMap = await handleCreateUpdateEvent(secretClient, event); data = { - secretValue: val, + ...secretMap, }; } @@ -47,54 +47,59 @@ export const handler = async ( export const handleCreateUpdateEvent = async ( secretClient: SecretClient, event: CloudFormationCustomResourceEvent -): Promise => { +): Promise> => { const props = event.ResourceProperties as unknown as SecretResourceProps; - let secret: string | undefined; + const secretMap: Record = {}; + for (const secretName of props.secretNames) { + let secretValue: string | undefined = undefined; + try { + const resp = await secretClient.getSecret( + { + namespace: props.namespace, + name: props.name, + type: props.type, + }, + { + name: secretName, + } + ); + secretValue = resp.value; + } catch (err) { + const secretErr = err as SecretError; + if (secretErr.httpStatusCode && secretErr.httpStatusCode >= 500) { + throw new Error( + `Failed to retrieve backend secret '${secretName}' for '${ + props.namespace + }/${props.name}'. Reason: ${JSON.stringify(err)}` + ); + } + } - try { - const resp = await secretClient.getSecret( - { - namespace: props.namespace, - name: props.name, - type: props.type, - }, - { - name: props.secretName, + // if the secret is not available in branch path, try retrieving it at the app-level. + if (!secretValue) { + try { + const resp = await secretClient.getSecret(props.namespace, { + name: secretName, + }); + secretValue = resp.value; + } catch (err) { + throw new Error( + `Failed to retrieve backend secret '${secretName}' for '${ + props.namespace + }'. Reason: ${JSON.stringify(err)}` + ); } - ); - secret = resp?.value; - } catch (err) { - const secretErr = err as SecretError; - if (secretErr.httpStatusCode && secretErr.httpStatusCode >= 500) { - throw new Error( - `Failed to retrieve backend secret '${props.secretName}' for '${ - props.namespace - }/${props.name}'. Reason: ${JSON.stringify(err)}` - ); } - } - // if the secret is not available in branch path, try retrieving it at the app-level. - if (!secret) { - try { - const resp = await secretClient.getSecret(props.namespace, { - name: props.secretName, - }); - secret = resp?.value; - } catch (err) { + if (!secretValue) { throw new Error( - `Failed to retrieve backend secret '${props.secretName}' for '${ - props.namespace - }'. Reason: ${JSON.stringify(err)}` + `Unable to find backend secret for backend '${props.namespace}', branch '${props.name}', name '${secretName}'` ); } - } - if (!secret) { - throw new Error( - `Unable to find backend secret for backend '${props.namespace}', branch '${props.name}', name '${props.secretName}'` - ); + // store the secret->secretValue pair in the secret map + secretMap[secretName] = secretValue; } - return secret; + return secretMap; }; diff --git a/packages/backend/src/engine/backend-secret/lambda/backend_secret_fetcher_types.ts b/packages/backend/src/engine/backend-secret/lambda/backend_secret_fetcher_types.ts index a5cb72b4de3..3e625238b53 100644 --- a/packages/backend/src/engine/backend-secret/lambda/backend_secret_fetcher_types.ts +++ b/packages/backend/src/engine/backend-secret/lambda/backend_secret_fetcher_types.ts @@ -1,5 +1,5 @@ import { BackendIdentifier } from '@aws-amplify/plugin-types'; export type SecretResourceProps = Omit & { - secretName: string; + secretNames: string[]; }; diff --git a/packages/backend/src/engine/backend_id_scoped_ssm_environment_entries_generator.ts b/packages/backend/src/engine/backend_id_scoped_ssm_environment_entries_generator.ts index 067ccf32bac..c104f96b028 100644 --- a/packages/backend/src/engine/backend_id_scoped_ssm_environment_entries_generator.ts +++ b/packages/backend/src/engine/backend_id_scoped_ssm_environment_entries_generator.ts @@ -1,11 +1,13 @@ -import { ParameterPathConversions } from '@aws-amplify/platform-core'; +import { + NamingConverter, + ParameterPathConversions, +} from '@aws-amplify/platform-core'; import { BackendIdentifier, SsmEnvironmentEntriesGenerator, } from '@aws-amplify/plugin-types'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; -import { toScreamingSnakeCase } from './naming_convention_conversions.js'; /** * Generates SsmEnvironmentEntry[] with SSM parameters that are scoped to a specific backend identifier @@ -50,7 +52,9 @@ export class BackendIdScopedSsmEnvironmentEntriesGenerator */ generateSsmEnvironmentEntries = (scopeContext: Record) => Object.entries(scopeContext).map(([contextKey, contextValue]) => { - const sanitizedContextKey = toScreamingSnakeCase(contextKey); + const sanitizedContextKey = new NamingConverter().toScreamingSnakeCase( + contextKey + ); const parameterPath = ParameterPathConversions.toResourceReferenceFullPath( this.backendId, diff --git a/packages/backend/src/engine/custom_outputs_accumulator.test.ts b/packages/backend/src/engine/custom_outputs_accumulator.test.ts index 6962175e4c2..77c4e4896d0 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/engine/naming_convention_conversions.ts b/packages/backend/src/engine/naming_convention_conversions.ts deleted file mode 100644 index 272601da6f6..00000000000 --- a/packages/backend/src/engine/naming_convention_conversions.ts +++ /dev/null @@ -1,8 +0,0 @@ -import snakeCase from 'lodash.snakecase'; - -/** - * Converts input string to SCREAMING_SNAKE_CASE - */ -export const toScreamingSnakeCase = (input: string): string => { - return snakeCase(input).toUpperCase(); -}; diff --git a/packages/backend/src/function/runtime/index.ts b/packages/backend/src/function/runtime/index.ts new file mode 100644 index 00000000000..a82f939faa6 --- /dev/null +++ b/packages/backend/src/function/runtime/index.ts @@ -0,0 +1 @@ +export { getAmplifyDataClientConfig } from '@aws-amplify/backend-function/runtime'; diff --git a/packages/backend/src/index.internal.ts b/packages/backend/src/index.internal.ts index edcc5711d60..dcb3dad53c3 100644 --- a/packages/backend/src/index.internal.ts +++ b/packages/backend/src/index.internal.ts @@ -1,4 +1,6 @@ export * from './index.js'; +// eslint-disable-next-line @typescript-eslint/naming-convention +import * as __export__function__runtime from './function/runtime/index.js'; /* Api-extractor does not ([yet](https://github.com/microsoft/rushstack/issues/1596)) support multiple package entry points @@ -7,3 +9,4 @@ export * from './index.js'; */ export * from './types/platform.js'; +export { __export__function__runtime }; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 46adc6515e8..5150f93a8e2 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 fbadb59b576..04e3d091c5c 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/API.md b/packages/cli-core/API.md index b966cfeca14..616c6aee113 100644 --- a/packages/cli-core/API.md +++ b/packages/cli-core/API.md @@ -13,7 +13,12 @@ import { WriteStream } from 'node:tty'; export class AmplifyPrompter { static input: (options: { message: string; + required?: never; defaultValue?: string; + } | { + message: string; + required: true; + defaultValue?: never; }) => Promise; static secretValue: (promptMessage?: string) => Promise; static yesOrNo: (options: { diff --git a/packages/cli-core/CHANGELOG.md b/packages/cli-core/CHANGELOG.md index a99721d8219..9ced34d88d5 100644 --- a/packages/cli-core/CHANGELOG.md +++ b/packages/cli-core/CHANGELOG.md @@ -1,5 +1,27 @@ # @aws-amplify/cli-core +## 1.2.1 + +### Patch Changes + +- 0cf5c26: add a required input prompt for use in region input +- f6ba240: Upgrade execa +- Updated dependencies [cfdc854] +- Updated dependencies [65abf6a] + - @aws-amplify/platform-core@1.3.0 + +## 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 b752e107634..266bbd5932a 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.1", "type": "module", "publishConfig": { "access": "public" @@ -19,9 +19,10 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/platform-core": "^1.0.5", + "@aws-amplify/platform-core": "^1.3.0", "@inquirer/prompts": "^3.0.0", - "execa": "^8.0.1", - "kleur": "^4.1.5" + "execa": "^9.5.1", + "kleur": "^4.1.5", + "zod": "^3.22.2" } } diff --git a/packages/cli-core/src/package-manager-controller/execute_with_debugger_logger.ts b/packages/cli-core/src/package-manager-controller/execute_with_debugger_logger.ts index 2df5626e423..5df16c849dc 100644 --- a/packages/cli-core/src/package-manager-controller/execute_with_debugger_logger.ts +++ b/packages/cli-core/src/package-manager-controller/execute_with_debugger_logger.ts @@ -1,6 +1,7 @@ import { LogLevel } from '../printer/printer.js'; -import { type Options, execa as _execa } from 'execa'; +import { ExecaMethod, execa as _execa } from 'execa'; import { printer } from '../printer.js'; +import { ExecaOptions } from '@aws-amplify/plugin-types'; /** * Abstracts the execution of a command and pipes outputs/errors to `Printer.debug` @@ -10,8 +11,8 @@ export const executeWithDebugLogger = ( executable: string, args?: Readonly, execa = _execa, - options?: Options<'utf8'> -) => { + options?: ExecaOptions +): ReturnType => { try { const childProcess = execa(executable, args, { stdin: 'inherit', diff --git a/packages/cli-core/src/package-manager-controller/lock-file-reader/npm_lock_file_reader.test.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/npm_lock_file_reader.test.ts new file mode 100644 index 00000000000..fff5b1c46b6 --- /dev/null +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/npm_lock_file_reader.test.ts @@ -0,0 +1,75 @@ +import assert from 'assert'; +import fsp from 'fs/promises'; +import { afterEach, describe, it, mock } from 'node:test'; +import path from 'path'; +import { NpmLockFileReader } from './npm_lock_file_reader.js'; + +void describe('NpmLockFileReader', () => { + const fspReadFileMock = mock.method(fsp, 'readFile', () => + JSON.stringify({ + name: 'test_project', + version: '1.0.0', + packages: { + '': { + name: 'test_project', + version: '1.0.0', + }, + 'node_modules/test_dep': { + version: '1.2.3', + }, + 'node_modules/some_other_dep': { + version: '12.13.14', + }, + }, + }) + ); + const npmLockFileReader = new NpmLockFileReader(); + + afterEach(() => { + fspReadFileMock.mock.resetCalls(); + }); + + void it('can get lock file contents from cwd', async () => { + const lockFileContents = + await npmLockFileReader.getLockFileContentsFromCwd(); + const expectedLockFileContents = { + dependencies: [ + { + name: 'test_dep', // "node_modules/" prefix is removed + version: '1.2.3', + }, + { + name: 'some_other_dep', // "node_modules/" prefix is removed + version: '12.13.14', + }, + ], + }; + assert.deepEqual(lockFileContents, expectedLockFileContents); + assert.strictEqual( + fspReadFileMock.mock.calls[0].arguments[0], + path.resolve(process.cwd(), 'package-lock.json') + ); + assert.strictEqual(fspReadFileMock.mock.callCount(), 1); + }); + + void it('returns undefined when package-lock.json is not present or parse-able', async () => { + fspReadFileMock.mock.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + const lockFileContents = + await npmLockFileReader.getLockFileContentsFromCwd(); + assert.deepEqual(lockFileContents, undefined); + }); + + void it('returns empty dependency array when package-lock.json does not have dependencies', async () => { + fspReadFileMock.mock.mockImplementationOnce(() => + JSON.stringify({ + name: 'test_project', + version: '1.0.0', + }) + ); + const lockFileContents = + await npmLockFileReader.getLockFileContentsFromCwd(); + assert.deepEqual(lockFileContents, { dependencies: [] }); + }); +}); diff --git a/packages/cli-core/src/package-manager-controller/lock-file-reader/npm_lock_file_reader.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/npm_lock_file_reader.ts new file mode 100644 index 00000000000..6f412085b56 --- /dev/null +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/npm_lock_file_reader.ts @@ -0,0 +1,66 @@ +import { Dependency } from '@aws-amplify/plugin-types'; +import fsp from 'fs/promises'; +import path from 'path'; +import z from 'zod'; +import { LockFileContents, LockFileReader } from './types.js'; +import { printer } from '../../printer.js'; +import { LogLevel } from '../../printer/printer.js'; + +/** + * NpmLockFileReader is an abstraction around the logic used to read and parse lock file contents + */ +export class NpmLockFileReader implements LockFileReader { + getLockFileContentsFromCwd = async (): Promise< + LockFileContents | undefined + > => { + const dependencies: Array = []; + const packageLockJsonPath = path.resolve( + process.cwd(), + 'package-lock.json' + ); + let packageLockJson; + try { + const jsonLockContents = await fsp.readFile(packageLockJsonPath, 'utf-8'); + const jsonLockParsedValue = JSON.parse(jsonLockContents); + // This will strip fields that are not part of the package lock schema + packageLockJson = packageLockJsonSchema.parse(jsonLockParsedValue); + } catch (error) { + printer.log( + `Failed to get lock file contents because ${packageLockJsonPath} does not exist or is not parse-able`, + LogLevel.DEBUG + ); + return; + } + + for (const key in packageLockJson.packages) { + if (key === '') { + // Skip root project in packages + continue; + } + const dependencyVersion = packageLockJson.packages[key].version; + + // Version may not exist if package is a symbolic link + if (dependencyVersion) { + // Remove "node_modules/" prefix + const dependencyName = key.replace(/^node_modules\//, ''); + dependencies.push({ + name: dependencyName, + version: dependencyVersion, + }); + } + } + + return { dependencies }; + }; +} + +const packageLockJsonSchema = z.object({ + packages: z + .record( + z.string(), + z.object({ + version: z.string().optional(), + }) + ) + .optional(), +}); diff --git a/packages/cli-core/src/package-manager-controller/lock-file-reader/pnpm_lock_file_reader.test.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/pnpm_lock_file_reader.test.ts new file mode 100644 index 00000000000..443a5f6e5cf --- /dev/null +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/pnpm_lock_file_reader.test.ts @@ -0,0 +1,145 @@ +import assert from 'assert'; +import fsp from 'fs/promises'; +import { afterEach, describe, it, mock } from 'node:test'; +import path from 'path'; +import { PnpmLockFileReader } from './pnpm_lock_file_reader.js'; + +void describe('PnpmLockFileReader', () => { + const fspReadFileMock = mock.method( + fsp, + 'readFile', + () => `lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + aws-amplify: + specifier: ^6.12.0 + version: 6.12.0 + devDependencies: + '@aws-amplify/backend': + specifier: ^1.11.0 + version: 1.12.0(@aws-sdk/client-cloudformation@3.723.0)(@aws-sdk/client-s3@3.723.0)(@aws-sdk/client-sso-oidc@3.621.0(@aws-sdk/client-sts@3.621.0))(@aws-sdk/types@3.723.0)(aws-cdk-lib@2.174.1(constructs@10.4.2))(constructs@10.4.2)(zod@3.24.1) + '@aws-amplify/backend-cli': + specifier: ^1.4.5 + version: 1.4.6(@aws-sdk/client-sso-oidc@3.621.0(@aws-sdk/client-sts@3.621.0))(@aws-sdk/client-sts@3.621.0)(@aws-sdk/types@3.723.0)(aws-cdk-lib@2.174.1(constructs@10.4.2))(aws-cdk@2.174.1)(constructs@10.4.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2) + aws-cdk: + specifier: ^2.173.4 + version: 2.174.1 + aws-cdk-lib: + specifier: ^2.173.4 + version: 2.174.1(constructs@10.4.2) + constructs: + specifier: ^10.4.2 + version: 10.4.2 + esbuild: + specifier: ^0.24.2 + version: 0.24.2 + tsx: + specifier: ^4.19.2 + version: 4.19.2 + typescript: + specifier: ^5.7.2 + version: 5.7.2 + +packages: + + '@test_dep@1.2.3': + resolution: {integrity: some-sha} + engines: {node: '>=6.0.0'} + + 'some_other_dep@12.13.14': + resolution: {integrity: some-other-sha} + engines: {node: '>=8'}` + ); + const pnpmLockFileReader = new PnpmLockFileReader(); + + afterEach(() => { + fspReadFileMock.mock.resetCalls(); + }); + + void it('can get lock file contents from cwd', async () => { + const lockFileContents = + await pnpmLockFileReader.getLockFileContentsFromCwd(); + const expectedLockFileContents = { + dependencies: [ + { + name: '@test_dep', + version: '1.2.3', + }, + { + name: 'some_other_dep', + version: '12.13.14', + }, + ], + }; + assert.deepEqual(lockFileContents, expectedLockFileContents); + assert.strictEqual( + fspReadFileMock.mock.calls[0].arguments[0], + path.resolve(process.cwd(), 'pnpm-lock.yaml') + ); + assert.strictEqual(fspReadFileMock.mock.callCount(), 1); + }); + + void it('returns empty lock file contents when pnpm-lock.yaml is not present or parse-able', async () => { + fspReadFileMock.mock.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + const lockFileContents = + await pnpmLockFileReader.getLockFileContentsFromCwd(); + assert.deepEqual(lockFileContents, undefined); + }); + + void it('returns empty dependency array when pnpm-lock.yaml does not have dependencies', async () => { + mock.method( + fsp, + 'readFile', + () => `lockfileVersion: '9.0' + + settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + + importers: + + .: + dependencies: + aws-amplify: + specifier: ^6.12.0 + version: 6.12.0 + devDependencies: + '@aws-amplify/backend': + specifier: ^1.11.0 + version: 1.12.0(@aws-sdk/client-cloudformation@3.723.0)(@aws-sdk/client-s3@3.723.0)(@aws-sdk/client-sso-oidc@3.621.0(@aws-sdk/client-sts@3.621.0))(@aws-sdk/types@3.723.0)(aws-cdk-lib@2.174.1(constructs@10.4.2))(constructs@10.4.2)(zod@3.24.1) + '@aws-amplify/backend-cli': + specifier: ^1.4.5 + version: 1.4.6(@aws-sdk/client-sso-oidc@3.621.0(@aws-sdk/client-sts@3.621.0))(@aws-sdk/client-sts@3.621.0)(@aws-sdk/types@3.723.0)(aws-cdk-lib@2.174.1(constructs@10.4.2))(aws-cdk@2.174.1)(constructs@10.4.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2) + aws-cdk: + specifier: ^2.173.4 + version: 2.174.1 + aws-cdk-lib: + specifier: ^2.173.4 + version: 2.174.1(constructs@10.4.2) + constructs: + specifier: ^10.4.2 + version: 10.4.2 + esbuild: + specifier: ^0.24.2 + version: 0.24.2 + tsx: + specifier: ^4.19.2 + version: 4.19.2 + typescript: + specifier: ^5.7.2 + version: 5.7.2` + ); + const lockFileContents = + await pnpmLockFileReader.getLockFileContentsFromCwd(); + assert.deepEqual(lockFileContents, { dependencies: [] }); + }); +}); diff --git a/packages/cli-core/src/package-manager-controller/lock-file-reader/pnpm_lock_file_reader.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/pnpm_lock_file_reader.ts new file mode 100644 index 00000000000..caed7ecaf64 --- /dev/null +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/pnpm_lock_file_reader.ts @@ -0,0 +1,59 @@ +import { Dependency } from '@aws-amplify/plugin-types'; +import fsp from 'fs/promises'; +import path from 'path'; +import { LockFileContents, LockFileReader } from './types.js'; +import { printer } from '../../printer.js'; +import { LogLevel } from '../../printer/printer.js'; + +/** + * PnpmLockFileReader is an abstraction around the logic used to read and parse lock file contents + */ +export class PnpmLockFileReader implements LockFileReader { + getLockFileContentsFromCwd = async (): Promise< + LockFileContents | undefined + > => { + const eolRegex = '[\r\n]'; + const dependencies: Array = []; + const pnpmLockPath = path.resolve(process.cwd(), 'pnpm-lock.yaml'); + + try { + const pnpmLockContents = await fsp.readFile(pnpmLockPath, 'utf-8'); + const pnpmLockContentsArray = pnpmLockContents.split( + new RegExp(`${eolRegex}${eolRegex}`) + ); + + const startOfPackagesIndex = pnpmLockContentsArray.indexOf('packages:'); + if (startOfPackagesIndex === -1) { + return { dependencies }; + } + const pnpmLockPackages = pnpmLockContentsArray.slice( + startOfPackagesIndex + 1 + ); + + for (const pnpmDependencyBlock of pnpmLockPackages) { + // Get line that contains dependency name and version and remove quotes and colon + const pnpmDependencyLine = pnpmDependencyBlock + .trim() + .split(new RegExp(eolRegex))[0] + .replaceAll(/[':]/g, ''); + const dependencyName = pnpmDependencyLine.slice( + 0, + pnpmDependencyLine.lastIndexOf('@') + ); + const dependencyVersion = pnpmDependencyLine.slice( + pnpmDependencyLine.lastIndexOf('@') + 1 + ); + + dependencies.push({ name: dependencyName, version: dependencyVersion }); + } + } catch (error) { + printer.log( + `Failed to get lock file contents because ${pnpmLockPath} does not exist or is not parse-able`, + LogLevel.DEBUG + ); + return; + } + + return { dependencies }; + }; +} diff --git a/packages/cli-core/src/package-manager-controller/lock-file-reader/types.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/types.ts new file mode 100644 index 00000000000..6762c4a30de --- /dev/null +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/types.ts @@ -0,0 +1,9 @@ +import { Dependency } from '@aws-amplify/plugin-types'; + +export type LockFileContents = { + dependencies: Array; +}; + +export type LockFileReader = { + getLockFileContentsFromCwd: () => Promise; +}; diff --git a/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_classic_lock_file_reader.test.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_classic_lock_file_reader.test.ts new file mode 100644 index 00000000000..38afb7860b1 --- /dev/null +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_classic_lock_file_reader.test.ts @@ -0,0 +1,82 @@ +import assert from 'assert'; +import fsp from 'fs/promises'; +import { afterEach, describe, it, mock } from 'node:test'; +import path from 'path'; +import { YarnClassicLockFileReader } from './yarn_classic_lock_file_reader.js'; + +void describe('YarnClassicLockFileReader', () => { + const fspReadFileMock = mock.method( + fsp, + 'readFile', + () => `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@test_dep@^1.0.0": + version "1.2.3" + resolved "https://registry.yarnpkg.com/some_dep" + integrity some-sha + dependencies: + "sub_dep_1" "^0.3.5" + "sub_dep_2" "^0.3.24" + +some_other_dep@12.13.14: + version "12.13.14" + resolved "https://registry.yarnpkg.com/some_other_dep" + integrity some-other-sha + dependencies: + sub_dep_3 "~2.0.1"` + ); + const yarnClassicLockFileReader = new YarnClassicLockFileReader(); + + afterEach(() => { + fspReadFileMock.mock.resetCalls(); + }); + + void it('can get lock file contents from cwd', async () => { + const lockFileContents = + await yarnClassicLockFileReader.getLockFileContentsFromCwd(); + const expectedLockFileContents = { + dependencies: [ + { + name: '@test_dep', + version: '1.2.3', + }, + { + name: 'some_other_dep', + version: '12.13.14', + }, + ], + }; + assert.deepEqual(lockFileContents, expectedLockFileContents); + assert.strictEqual( + fspReadFileMock.mock.calls[0].arguments[0], + path.resolve(process.cwd(), 'yarn.lock') + ); + assert.strictEqual(fspReadFileMock.mock.callCount(), 1); + }); + + void it('returns undefined when yarn.lock is not present or parse-able', async () => { + fspReadFileMock.mock.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + const lockFileContents = + await yarnClassicLockFileReader.getLockFileContentsFromCwd(); + assert.deepEqual(lockFileContents, undefined); + }); + + void it('returns empty dependency array when yarn.lock does not have dependencies', async () => { + mock.method( + fsp, + 'readFile', + () => `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +` + ); + const lockFileContents = + await yarnClassicLockFileReader.getLockFileContentsFromCwd(); + assert.deepEqual(lockFileContents, { dependencies: [] }); + }); +}); diff --git a/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_classic_lock_file_reader.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_classic_lock_file_reader.ts new file mode 100644 index 00000000000..cd12059485c --- /dev/null +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_classic_lock_file_reader.ts @@ -0,0 +1,52 @@ +import { Dependency } from '@aws-amplify/plugin-types'; +import fsp from 'fs/promises'; +import path from 'path'; +import { LockFileContents, LockFileReader } from './types.js'; +import { printer } from '../../printer.js'; +import { LogLevel } from '../../printer/printer.js'; + +/** + * YarnClassicLockFileReader is an abstraction around the logic used to read and parse lock file contents + */ +export class YarnClassicLockFileReader implements LockFileReader { + getLockFileContentsFromCwd = async (): Promise< + LockFileContents | undefined + > => { + const eolRegex = '[\r\n]'; + const dependencies: Array = []; + const yarnLockPath = path.resolve(process.cwd(), 'yarn.lock'); + + try { + const yarnLockContents = await fsp.readFile(yarnLockPath, 'utf-8'); + const yarnLockContentsArray = yarnLockContents + .trim() + .split(new RegExp(`${eolRegex}${eolRegex}`)); + + // Slice to remove comment block at the start of the lock file + for (const yarnDependencyBlock of yarnLockContentsArray.slice(1)) { + const yarnDependencyLines = yarnDependencyBlock + .trim() + .split(new RegExp(eolRegex)); + const yarnDependencyName = yarnDependencyLines[0]; + const yarnDependencyVersion = yarnDependencyLines[1]; + + // Get dependency name before versioning info + const dependencyName = yarnDependencyName + .slice(0, yarnDependencyName.lastIndexOf('@')) + .replaceAll(/"/g, ''); + const versionMatch = yarnDependencyVersion.match(/"(.*)"/); + const dependencyVersion = versionMatch ? versionMatch[1] : ''; + + dependencies.push({ name: dependencyName, version: dependencyVersion }); + } + } catch (error) { + printer.log( + `Failed to get lock file contents because ${yarnLockPath} does not exist or is not parse-able`, + LogLevel.DEBUG + ); + return; + } + + return { dependencies }; + }; +} diff --git a/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_modern_lock_file_reader.test.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_modern_lock_file_reader.test.ts new file mode 100644 index 00000000000..461b88f66a9 --- /dev/null +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_modern_lock_file_reader.test.ts @@ -0,0 +1,97 @@ +import assert from 'assert'; +import fsp from 'fs/promises'; +import { afterEach, describe, it, mock } from 'node:test'; +import path from 'path'; +import { YarnModernLockFileReader } from './yarn_modern_lock_file_reader.js'; + +void describe('YarnModernLockFileReader', () => { + const fspReadFileMock = mock.method( + fsp, + 'readFile', + () => `# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"@test_dep@npm:^1.0.0": + version: 1.2.3 + resolution: "@test_dep@npm:1.2.3" + dependencies: + "@sub_dep_1": "npm:^0.3.5" + "sub_dep_2": "npm:^0.3.24" + checksum: some-checksum + languageName: node + linkType: hard + +"some_other_dep@npm:12.13.14": + version: 12.13.14 + resolution: "some_other_dep@npm:12.13.14" + dependencies: + sub-dep_3: "npm:~2.0.1" + checksum: some-other-checksum + languageName: node + linkType: hard` + ); + const yarnModernLockFileReader = new YarnModernLockFileReader(); + + afterEach(() => { + fspReadFileMock.mock.resetCalls(); + }); + + void it('can get lock file contents from cwd', async () => { + const lockFileContents = + await yarnModernLockFileReader.getLockFileContentsFromCwd(); + const expectedLockFileContents = { + dependencies: [ + { + name: '@test_dep', + version: '1.2.3', + }, + { + name: 'some_other_dep', + version: '12.13.14', + }, + ], + }; + assert.deepEqual(lockFileContents, expectedLockFileContents); + assert.strictEqual( + fspReadFileMock.mock.calls[0].arguments[0], + path.resolve(process.cwd(), 'yarn.lock') + ); + assert.strictEqual(fspReadFileMock.mock.callCount(), 1); + }); + + void it('returns undefined when yarn.lock is not present or parse-able', async () => { + fspReadFileMock.mock.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + const lockFileContents = + await yarnModernLockFileReader.getLockFileContentsFromCwd(); + assert.deepEqual(lockFileContents, undefined); + }); + + void it('returns empty dependency array when yarn.lock does not have dependencies', async () => { + mock.method( + fsp, + 'readFile', + () => `# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"testapp@workspace:.": + version: 0.0.0-use.local + resolution: "testapp@workspace:." + languageName: unknown + linkType: soft +` + ); + const lockFileContents = + await yarnModernLockFileReader.getLockFileContentsFromCwd(); + assert.deepEqual(lockFileContents, { dependencies: [] }); + }); +}); diff --git a/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_modern_lock_file_reader.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_modern_lock_file_reader.ts new file mode 100644 index 00000000000..868c6dc8d0e --- /dev/null +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_modern_lock_file_reader.ts @@ -0,0 +1,57 @@ +import { Dependency } from '@aws-amplify/plugin-types'; +import fsp from 'fs/promises'; +import path from 'path'; +import { LockFileContents, LockFileReader } from './types.js'; +import { printer } from '../../printer.js'; +import { LogLevel } from '../../printer/printer.js'; + +/** + * YarnModernLockFileReader is an abstraction around the logic used to read and parse lock file contents + */ +export class YarnModernLockFileReader implements LockFileReader { + getLockFileContentsFromCwd = async (): Promise< + LockFileContents | undefined + > => { + const eolRegex = '[\r\n]'; + const dependencies: Array = []; + const yarnLockPath = path.resolve(process.cwd(), 'yarn.lock'); + + try { + const yarnLockContents = await fsp.readFile(yarnLockPath, 'utf-8'); + const yarnLockContentsArray = yarnLockContents.split( + new RegExp(`${eolRegex}${eolRegex}`) + ); + + if (yarnLockContentsArray.length === 3) { + // Contents are only comment block, metadata, and workspace info + return { dependencies }; + } + + // Slice to remove comment block and metadata at the start of the lock file + for (const yarnDependencyBlock of yarnLockContentsArray.slice(2)) { + const yarnDependencyLines = yarnDependencyBlock + .trim() + .split(new RegExp(eolRegex)); + const yarnDependencyName = yarnDependencyLines[0]; + const yarnDependencyVersion = yarnDependencyLines[1]; + + // Get dependency name before versioning info + const dependencyName = yarnDependencyName + .slice(0, yarnDependencyName.lastIndexOf('@')) + .replaceAll(/"/g, ''); + const versionMatch = yarnDependencyVersion.match(/[\d.]+/); + const dependencyVersion = versionMatch ? versionMatch[0] : ''; + + dependencies.push({ name: dependencyName, version: dependencyVersion }); + } + } catch (error) { + printer.log( + `Failed to get lock file contents because ${yarnLockPath} does not exist or is not parse-able`, + LogLevel.DEBUG + ); + return; + } + + return { dependencies }; + }; +} diff --git a/packages/cli-core/src/package-manager-controller/npm_package_manager_controller.test.ts b/packages/cli-core/src/package-manager-controller/npm_package_manager_controller.test.ts index 938b1c580df..4f7aa2d9dbb 100644 --- a/packages/cli-core/src/package-manager-controller/npm_package_manager_controller.test.ts +++ b/packages/cli-core/src/package-manager-controller/npm_package_manager_controller.test.ts @@ -5,6 +5,7 @@ import assert from 'assert'; import { execa } from 'execa'; import { NpmPackageManagerController } from './npm_package_manager_controller.js'; import { executeWithDebugLogger } from './execute_with_debugger_logger.js'; +import { LockFileReader } from './lock-file-reader/types.js'; void describe('NpmPackageManagerController', () => { const fspMock = { @@ -124,4 +125,64 @@ void describe('NpmPackageManagerController', () => { assert.equal(fspMock.writeFile.mock.callCount(), 1); }); }); + + void describe('getDependencies', () => { + void it('successfully returns dependency versions', async () => { + const existsSyncMock = mock.fn(() => true); + const lockFileReaderMock = { + getLockFileContentsFromCwd: async () => + Promise.resolve({ + dependencies: [ + { + name: 'aws-cdk', + version: '1.2.3', + }, + { + name: 'aws-cdk-lib', + version: '12.13.14', + }, + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, + ], + }), + } as LockFileReader; + const npmPackageManagerController = new NpmPackageManagerController( + '/testProjectRoot', + fspMock as unknown as typeof fsp, + pathMock as unknown as typeof path, + execaMock as unknown as typeof execa, + executeWithDebugLoggerMock as unknown as typeof executeWithDebugLogger, + existsSyncMock, + lockFileReaderMock + ); + const dependencyVersions = + await npmPackageManagerController.tryGetDependencies(); + const expectedVersions = [ + { + name: 'aws-cdk', + version: '1.2.3', + }, + { + name: 'aws-cdk-lib', + version: '12.13.14', + }, + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, + ]; + + assert.deepEqual(dependencyVersions, expectedVersions); + }); + }); }); diff --git a/packages/cli-core/src/package-manager-controller/npm_package_manager_controller.ts b/packages/cli-core/src/package-manager-controller/npm_package_manager_controller.ts index f680458be27..0277fbf7417 100644 --- a/packages/cli-core/src/package-manager-controller/npm_package_manager_controller.ts +++ b/packages/cli-core/src/package-manager-controller/npm_package_manager_controller.ts @@ -4,6 +4,7 @@ import { execa as _execa } from 'execa'; import * as _path from 'path'; import { executeWithDebugLogger as _executeWithDebugLogger } from './execute_with_debugger_logger.js'; import { PackageManagerControllerBase } from './package_manager_controller_base.js'; +import { NpmLockFileReader } from './lock-file-reader/npm_lock_file_reader.js'; /** * NpmPackageManagerController is an abstraction around npm commands that are needed to initialize a project and install dependencies @@ -18,13 +19,15 @@ export class NpmPackageManagerController extends PackageManagerControllerBase { protected readonly path = _path, protected readonly execa = _execa, protected readonly executeWithDebugLogger = _executeWithDebugLogger, - protected readonly existsSync = _existsSync + protected readonly existsSync = _existsSync, + protected readonly lockFileReader = new NpmLockFileReader() ) { super( cwd, 'npm', ['init', '--yes'], 'install', + lockFileReader, fsp, path, execa, 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 d722ef84f57..be83839c875 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 @@ -1,12 +1,18 @@ import { existsSync as _existsSync } from 'fs'; import _fsp from 'fs/promises'; -import { type ExecaChildProcess, type Options, execa as _execa } from 'execa'; +import { execa as _execa } from 'execa'; import * as _path from 'path'; -import { type PackageManagerController } from '@aws-amplify/plugin-types'; +import { + Dependency, + ExecaChildProcess, + ExecaOptions, + type PackageManagerController, +} from '@aws-amplify/plugin-types'; import { LogLevel } from '../printer/printer.js'; import { printer } from '../printer.js'; import { executeWithDebugLogger as _executeWithDebugLogger } from './execute_with_debugger_logger.js'; import { getPackageManagerRunnerName } from './get_package_manager_name.js'; +import { LockFileReader } from './lock-file-reader/types.js'; /** * PackageManagerController is an abstraction around package manager commands that are needed to initialize a project and install dependencies @@ -23,6 +29,7 @@ export abstract class PackageManagerControllerBase protected readonly executable: string, protected readonly initDefault: string[], protected readonly installCommand: string, + protected readonly lockFileReader: LockFileReader, protected readonly fsp = _fsp, protected readonly path = _path, protected readonly execa = _execa, @@ -121,7 +128,7 @@ export abstract class PackageManagerControllerBase runWithPackageManager( args: string[] = [], dir: string, - options?: Options<'utf8'> + options?: ExecaOptions ): ExecaChildProcess { return this.executeWithDebugLogger( dir, @@ -137,9 +144,20 @@ 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; + /** + * tryGetDependencies - Tries to retrieve dependency versions from the lock file in the project root + */ + tryGetDependencies = async (): Promise | undefined> => { + const lockFileContents = + await this.lockFileReader.getLockFileContentsFromCwd(); + + return lockFileContents?.dependencies; + }; + /** * Check if a package.json file exists in projectRoot */ diff --git a/packages/cli-core/src/package-manager-controller/pnpm_package_manager_controller.test.ts b/packages/cli-core/src/package-manager-controller/pnpm_package_manager_controller.test.ts index 3895e622513..3e3c7bc3423 100644 --- a/packages/cli-core/src/package-manager-controller/pnpm_package_manager_controller.test.ts +++ b/packages/cli-core/src/package-manager-controller/pnpm_package_manager_controller.test.ts @@ -5,6 +5,7 @@ import assert from 'assert'; import { execa } from 'execa'; import { PnpmPackageManagerController } from './pnpm_package_manager_controller.js'; import { executeWithDebugLogger } from './execute_with_debugger_logger.js'; +import { LockFileReader } from './lock-file-reader/types.js'; void describe('PnpmPackageManagerController', () => { const fspMock = { @@ -124,4 +125,64 @@ void describe('PnpmPackageManagerController', () => { assert.equal(fspMock.writeFile.mock.callCount(), 1); }); }); + + void describe('getDependencies', () => { + void it('successfully returns dependency versions', async () => { + const existsSyncMock = mock.fn(() => true); + const lockFileReaderMock = { + getLockFileContentsFromCwd: async () => + Promise.resolve({ + dependencies: [ + { + name: 'aws-cdk', + version: '1.2.3', + }, + { + name: 'aws-cdk-lib', + version: '12.13.14', + }, + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, + ], + }), + } as LockFileReader; + const pnpmPackageManagerController = new PnpmPackageManagerController( + '/testProjectRoot', + fspMock as unknown as typeof fsp, + pathMock as unknown as typeof path, + execaMock as unknown as typeof execa, + executeWithDebugLoggerMock as unknown as typeof executeWithDebugLogger, + existsSyncMock, + lockFileReaderMock + ); + const dependencyVersions = + await pnpmPackageManagerController.tryGetDependencies(); + const expectedVersions = [ + { + name: 'aws-cdk', + version: '1.2.3', + }, + { + name: 'aws-cdk-lib', + version: '12.13.14', + }, + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, + ]; + + assert.deepEqual(dependencyVersions, expectedVersions); + }); + }); }); 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 321ad9cd26c..2cb6fb49336 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 @@ -4,6 +4,7 @@ import { existsSync as _existsSync } from 'fs'; import { execa as _execa } from 'execa'; import { executeWithDebugLogger as _executeWithDebugLogger } from './execute_with_debugger_logger.js'; import { PackageManagerControllerBase } from './package_manager_controller_base.js'; +import { PnpmLockFileReader } from './lock-file-reader/pnpm_lock_file_reader.js'; /** * PnpmPackageManagerController is an abstraction around pnpm commands that are needed to initialize a project and install dependencies @@ -18,13 +19,15 @@ export class PnpmPackageManagerController extends PackageManagerControllerBase { protected readonly path = _path, protected readonly execa = _execa, protected readonly executeWithDebugLogger = _executeWithDebugLogger, - protected readonly existsSync = _existsSync + protected readonly existsSync = _existsSync, + protected readonly lockFileReader = new PnpmLockFileReader() ) { super( cwd, 'pnpm', ['init'], 'install', + lockFileReader, fsp, path, execa, @@ -32,10 +35,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.test.ts b/packages/cli-core/src/package-manager-controller/yarn_classic_package_manager_controller.test.ts index 28987b2784b..4c35e60e77e 100644 --- a/packages/cli-core/src/package-manager-controller/yarn_classic_package_manager_controller.test.ts +++ b/packages/cli-core/src/package-manager-controller/yarn_classic_package_manager_controller.test.ts @@ -5,6 +5,7 @@ import assert from 'assert'; import { execa } from 'execa'; import { YarnClassicPackageManagerController } from './yarn_classic_package_manager_controller.js'; import { executeWithDebugLogger } from './execute_with_debugger_logger.js'; +import { LockFileReader } from './lock-file-reader/types.js'; void describe('YarnClassicPackageManagerController', () => { const fspMock = { @@ -128,4 +129,65 @@ void describe('YarnClassicPackageManagerController', () => { assert.equal(fspMock.writeFile.mock.callCount(), 1); }); }); + + void describe('getDependencies', () => { + void it('successfully returns dependency versions', async () => { + const existsSyncMock = mock.fn(() => true); + const lockFileReaderMock = { + getLockFileContentsFromCwd: async () => + Promise.resolve({ + dependencies: [ + { + name: 'aws-cdk', + version: '1.2.3', + }, + { + name: 'aws-cdk-lib', + version: '12.13.14', + }, + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, + ], + }), + } as LockFileReader; + const yarnClassicPackageManagerController = + new YarnClassicPackageManagerController( + '/testProjectRoot', + fspMock as unknown as typeof fsp, + pathMock as unknown as typeof path, + execaMock as unknown as typeof execa, + executeWithDebugLoggerMock as unknown as typeof executeWithDebugLogger, + existsSyncMock, + lockFileReaderMock + ); + const dependencyVersions = + await yarnClassicPackageManagerController.tryGetDependencies(); + const expectedVersions = [ + { + name: 'aws-cdk', + version: '1.2.3', + }, + { + name: 'aws-cdk-lib', + version: '12.13.14', + }, + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, + ]; + + assert.deepEqual(dependencyVersions, expectedVersions); + }); + }); }); 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 2f7a837e769..70c8e451546 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 @@ -4,6 +4,7 @@ import { execa as _execa } from 'execa'; import * as _path from 'path'; import { executeWithDebugLogger as _executeWithDebugLogger } from './execute_with_debugger_logger.js'; import { PackageManagerControllerBase } from './package_manager_controller_base.js'; +import { YarnClassicLockFileReader } from './lock-file-reader/yarn_classic_lock_file_reader.js'; /** * YarnClassicPackageManagerController is an abstraction around yarn classic commands that are needed to initialize a project and install dependencies @@ -18,13 +19,15 @@ export class YarnClassicPackageManagerController extends PackageManagerControlle protected readonly path = _path, protected readonly execa = _execa, protected readonly executeWithDebugLogger = _executeWithDebugLogger, - protected readonly existsSync = _existsSync + protected readonly existsSync = _existsSync, + protected readonly lockFileReader = new YarnClassicLockFileReader() ) { super( cwd, 'yarn', ['init', '--yes'], 'add', + lockFileReader, fsp, path, execa, @@ -37,12 +40,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-core/src/package-manager-controller/yarn_modern_package_manager_controller.test.ts b/packages/cli-core/src/package-manager-controller/yarn_modern_package_manager_controller.test.ts index 4dfac585d3c..83119e1aefa 100644 --- a/packages/cli-core/src/package-manager-controller/yarn_modern_package_manager_controller.test.ts +++ b/packages/cli-core/src/package-manager-controller/yarn_modern_package_manager_controller.test.ts @@ -6,6 +6,7 @@ import { execa } from 'execa'; import { Printer } from '../printer/printer.js'; import { YarnModernPackageManagerController } from './yarn_modern_package_manager_controller.js'; import { executeWithDebugLogger } from './execute_with_debugger_logger.js'; +import { LockFileReader } from './lock-file-reader/types.js'; void describe('YarnModernPackageManagerController', () => { const fspMock = { @@ -123,4 +124,66 @@ void describe('YarnModernPackageManagerController', () => { assert.equal(fspMock.writeFile.mock.callCount(), 2); }); }); + + void describe('getDependencies', () => { + void it('successfully returns dependency versions', async () => { + const existsSyncMock = mock.fn(() => true); + const lockFileReaderMock = { + getLockFileContentsFromCwd: async () => + Promise.resolve({ + dependencies: [ + { + name: 'aws-cdk', + version: '1.2.3', + }, + { + name: 'aws-cdk-lib', + version: '12.13.14', + }, + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, + ], + }), + } as LockFileReader; + const yarnModernPackageManagerController = + new YarnModernPackageManagerController( + '/testProjectRoot', + printerMock as unknown as Printer, + fspMock as unknown as typeof fsp, + pathMock as unknown as typeof path, + execaMock as unknown as typeof execa, + executeWithDebugLoggerMock as unknown as typeof executeWithDebugLogger, + existsSyncMock, + lockFileReaderMock + ); + const dependencyVersions = + await yarnModernPackageManagerController.tryGetDependencies(); + const expectedVersions = [ + { + name: 'aws-cdk', + version: '1.2.3', + }, + { + name: 'aws-cdk-lib', + version: '12.13.14', + }, + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, + ]; + + assert.deepEqual(dependencyVersions, expectedVersions); + }); + }); }); diff --git a/packages/cli-core/src/package-manager-controller/yarn_modern_package_manager_controller.ts b/packages/cli-core/src/package-manager-controller/yarn_modern_package_manager_controller.ts index 0188ec889de..f4d6003f1b8 100644 --- a/packages/cli-core/src/package-manager-controller/yarn_modern_package_manager_controller.ts +++ b/packages/cli-core/src/package-manager-controller/yarn_modern_package_manager_controller.ts @@ -6,6 +6,7 @@ import { LogLevel, Printer } from '../printer/printer.js'; import { format } from '../format/format.js'; import { executeWithDebugLogger as _executeWithDebugLogger } from './execute_with_debugger_logger.js'; import { PackageManagerControllerBase } from './package_manager_controller_base.js'; +import { YarnModernLockFileReader } from './lock-file-reader/yarn_modern_lock_file_reader.js'; /** * YarnModernPackageManagerController is an abstraction around yarn modern (yarn v2+) commands that are needed to initialize a project and install dependencies @@ -21,13 +22,15 @@ export class YarnModernPackageManagerController extends PackageManagerController protected readonly path = _path, protected readonly execa = _execa, protected readonly executeWithDebugLogger = _executeWithDebugLogger, - protected readonly existsSync = _existsSync + protected readonly existsSync = _existsSync, + protected readonly lockFileReader = new YarnModernLockFileReader() ) { super( cwd, 'yarn', ['init', '--yes'], 'add', + lockFileReader, fsp, path, execa, diff --git a/packages/cli-core/src/prompter/amplify_prompts.ts b/packages/cli-core/src/prompter/amplify_prompts.ts index d398687cbeb..447049ef4e1 100644 --- a/packages/cli-core/src/prompter/amplify_prompts.ts +++ b/packages/cli-core/src/prompter/amplify_prompts.ts @@ -43,16 +43,32 @@ export class AmplifyPrompter { * @param options for displaying the prompt * @param options.message display for the prompt * @param options.defaultValue if user submits without typing anything. Default: "." + * @param options.required if the user input is required, incompatible with options.defaultValue * @returns Promise the user input */ - static input = async (options: { - message: string; - defaultValue?: string; - }): Promise => { - const response = await input({ + static input = async ( + options: + | { + message: string; + required?: never; + defaultValue?: string; + } + | { + message: string; + required: true; + defaultValue?: never; + } + ): Promise => { + if (options.required) { + return input({ + message: options.message, + validate: (val: string) => + val && val.length > 0 ? true : 'Cannot be empty', + }); + } + return input({ message: options.message, default: options.defaultValue ?? '', }); - return response; }; } diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index ad5515725e5..f74b6b96d1f 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,178 @@ # @aws-amplify/backend-cli +## 1.4.6 + +### Patch Changes + +- a7506f9: wraps no outputs found error from backend output client +- Updated dependencies [a7506f9] +- Updated dependencies [a7506f9] +- Updated dependencies [a7506f9] +- Updated dependencies [a7506f9] + - @aws-amplify/model-generator@1.0.12 + - @aws-amplify/client-config@1.5.5 + - @aws-amplify/sandbox@1.2.9 + - @aws-amplify/backend-deployer@1.1.13 + - @aws-amplify/platform-core@1.5.0 + - @aws-amplify/plugin-types@1.7.0 + +## 1.4.5 + +### Patch Changes + +- 0ef53e2: add validation for sandbox set command secret-name +- Updated dependencies [107600b] + - @aws-amplify/model-generator@1.0.11 + +## 1.4.4 + +### Patch Changes + +- 3cf0738: update detection of BackendOutputClientErrors +- Updated dependencies [dedcc27] +- Updated dependencies [95942c5] +- Updated dependencies [1eced2c] +- Updated dependencies [3cf0738] +- Updated dependencies [3cf0738] +- Updated dependencies [f679cf6] +- Updated dependencies [f193105] + - @aws-amplify/backend-deployer@1.1.12 + - @aws-amplify/platform-core@1.4.0 + - @aws-amplify/deployed-backend-client@1.5.0 + - @aws-amplify/model-generator@1.0.10 + - @aws-amplify/client-config@1.5.4 + - @aws-amplify/sandbox@1.2.8 + +## 1.4.3 + +### Patch Changes + +- 7c5abe2: validate branch and app id inputs for pipeline-deploy command +- 7c5abe2: update logic for falling back to sandbox resolver for backend id +- 0cf5c26: add a required input prompt for use in region input +- fc7e4d4: validate input stack name for generate commands +- f6ba240: Upgrade execa +- Updated dependencies [1593ce8] +- Updated dependencies [a406263] +- Updated dependencies [37d8564] +- Updated dependencies [cfdc854] +- Updated dependencies [d66ab17] +- Updated dependencies [5a47d21] +- Updated dependencies [72b2fe0] +- Updated dependencies [65abf6a] +- Updated dependencies [0a360fb] +- Updated dependencies [6015595] +- Updated dependencies [daaedb6] +- Updated dependencies [0cf5c26] +- Updated dependencies [f6ba240] + - @aws-amplify/backend-deployer@1.1.11 + - @aws-amplify/platform-core@1.3.0 + - @aws-amplify/schema-generator@1.2.6 + - @aws-amplify/client-config@1.5.3 + - @aws-amplify/plugin-types@1.6.0 + - @aws-amplify/sandbox@1.2.7 + - @aws-amplify/cli-core@1.2.1 + +## 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 bd40712e75e..589cd58450d 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.6", "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.13", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/backend-secret": "^1.1.2", + "@aws-amplify/cli-core": "^1.2.1", + "@aws-amplify/client-config": "^1.5.5", + "@aws-amplify/deployed-backend-client": "^1.5.0", + "@aws-amplify/form-generator": "^1.0.3", + "@aws-amplify/model-generator": "^1.0.12", + "@aws-amplify/platform-core": "^1.5.0", + "@aws-amplify/plugin-types": "^1.7.0", + "@aws-amplify/sandbox": "^1.2.9", + "@aws-amplify/schema-generator": "^1.2.6", "@aws-sdk/client-amplify": "^3.624.0", "@aws-sdk/client-cloudformation": "^3.624.0", "@aws-sdk/client-s3": "^3.624.0", @@ -52,7 +52,7 @@ "@smithy/node-config-provider": "^2.1.3", "@smithy/shared-ini-file-loader": "^2.2.5", "envinfo": "^7.11.0", - "execa": "^8.0.1", + "execa": "^9.5.1", "is-ci": "^3.0.1", "open": "^9.1.0", "yargs": "^17.7.2", diff --git a/packages/cli/src/ampx.ts b/packages/cli/src/ampx.ts index 2ddb90ccb26..c9448a58d1d 100755 --- a/packages/cli/src/ampx.ts +++ b/packages/cli/src/ampx.ts @@ -13,7 +13,7 @@ import { import { fileURLToPath } from 'node:url'; import { verifyCommandName } from './verify_command_name.js'; import { hideBin } from 'yargs/helpers'; -import { format } from '@aws-amplify/cli-core'; +import { PackageManagerControllerFactory, format } from '@aws-amplify/cli-core'; const packageJson = new PackageJsonReader().read( fileURLToPath(new URL('../package.json', import.meta.url)) @@ -27,8 +27,13 @@ if (libraryVersion == undefined) { }); } +const dependencies = await new PackageManagerControllerFactory() + .getPackageManagerController() + .tryGetDependencies(); + const usageDataEmitter = await new UsageDataEmitterFactory().getInstance( - libraryVersion + libraryVersion, + dependencies ); attachUnhandledExceptionListeners(usageDataEmitter); 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 7493b02270a..dba0e76709e 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 1f0aeed51e8..0b74437a573 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 acb95b9d1cd..4e56b5c40e6 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,64 @@ 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); +}); + +// stack, appId and branch can be empty string if option is added to command but no value is present (eg. 'ampx generate outputs --stack') +// this shows intent for deployed backend id so we should not fallback to sandbox id +void it('does not use sandbox id if the default identifier resolver fails and stack, appId or branch are empty strings', 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({ + stack: '', + }); + 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 c7c632412a1..be7b49646e0 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,33 @@ 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 !== undefined || + args.appId !== undefined || + args.branch !== undefined + ) { + 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 !== undefined || + args.appId !== undefined || + args.branch !== undefined + ) { + 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 313b4a20039..386e4c7ab27 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 a06d288f74d..c1da30912d6 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/configure/configure_profile_command.test.ts b/packages/cli/src/commands/configure/configure_profile_command.test.ts index f387069205e..1c316875a2a 100644 --- a/packages/cli/src/commands/configure/configure_profile_command.test.ts +++ b/packages/cli/src/commands/configure/configure_profile_command.test.ts @@ -42,7 +42,7 @@ void describe('configure command', () => { emitSuccessMock.mock.resetCalls(); }); - void it('configures a profile with an IAM user', async (contextual) => { + void it('fails to configure a profile with a name that already has a profile', async (contextual) => { const mockProfileExists = mock.method( profileController, 'profileExists', @@ -65,7 +65,7 @@ void describe('configure command', () => { }); }); - void it('configures a profile with an IAM user', async (contextual) => { + void it('configures a profile with an existing IAM user credentials', async (contextual) => { const mockProfileExists = mock.method( profileController, 'profileExists', @@ -84,10 +84,10 @@ void describe('configure command', () => { } ); - const mockInput = contextual.mock.method( + const mockRequiredInput = contextual.mock.method( AmplifyPrompter, 'input', - (options: { message: string; defaultValue?: string }) => { + (options: { message: string; required: true }) => { if (options.message.includes('Enter the AWS region to use')) { return Promise.resolve(testRegion); } @@ -105,7 +105,7 @@ void describe('configure command', () => { assert.equal(mockProfileExists.mock.callCount(), 1); assert.equal(mockSecretValue.mock.callCount(), 2); - assert.equal(mockInput.mock.callCount(), 1); + assert.equal(mockRequiredInput.mock.callCount(), 1); assert.equal(mockHasIAMUser.mock.callCount(), 1); assert.equal(mockAppendAWSFiles.mock.callCount(), 1); assert.deepStrictEqual(mockAppendAWSFiles.mock.calls[0].arguments[0], { @@ -121,7 +121,7 @@ void describe('configure command', () => { }); }); - void it('configures a profile without an IAM user', async (contextual) => { + void it('configures a profile without an existing IAM user', async (contextual) => { const mockProfileExists = mock.method( profileController, 'profileExists', diff --git a/packages/cli/src/commands/configure/configure_profile_command.ts b/packages/cli/src/commands/configure/configure_profile_command.ts index 7be59b0a497..72d3a1d785b 100644 --- a/packages/cli/src/commands/configure/configure_profile_command.ts +++ b/packages/cli/src/commands/configure/configure_profile_command.ts @@ -73,6 +73,7 @@ export class ConfigureProfileCommand const region = await AmplifyPrompter.input({ message: `Enter the AWS region to use with the '${profileName}' profile (eg us-east-1, us-west-2, etc):`, + required: true, }); await this.profileController.createOrAppendAWSFiles({ 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 b22f770e26b..de53013e1b0 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, @@ -330,10 +344,71 @@ void describe('generate forms command', () => { ); }); + void it('throws user error if the stack outputs are undefined', async () => { + const fakeSandboxId = 'my-fake-app-my-fake-username'; + const backendIdResolver = { + resolveDeployedBackendIdentifier: mock.fn(() => + Promise.resolve({ + namespace: fakeSandboxId, + name: fakeSandboxId, + type: 'sandbox', + }) + ), + resolveBackendIdentifier: mock.fn(() => + Promise.resolve({ + namespace: fakeSandboxId, + name: fakeSandboxId, + type: 'sandbox', + }) + ), + } as BackendIdentifierResolver; + const formGenerationHandler = new FormGenerationHandler({ + awsClientProvider, + }); + + const fakedBackendOutputClient = { + getOutput: mock.fn(() => { + throw new BackendOutputClientError( + BackendOutputClientErrorType.NO_OUTPUTS_FOUND, + 'stack outputs are undefined' + ); + }), + }; + + const generateFormsCommand = new GenerateFormsCommand( + backendIdResolver, + () => fakedBackendOutputClient, + formGenerationHandler + ); + + const parser = yargs().command( + generateFormsCommand as unknown as CommandModule + ); + const commandRunner = new TestCommandRunner(parser); + await assert.rejects( + () => commandRunner.runCommand('forms'), + (error: TestCommandError) => { + assert.strictEqual(error.error.name, 'AmplifyOutputsNotFoundError'); + assert.strictEqual( + error.error.message, + 'Amplify outputs not found in stack metadata' + ); + return true; + } + ); + }); + 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 +455,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 63afe6aaea8..1977c74f346 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(); @@ -75,64 +82,64 @@ export class GenerateFormsCommand try { output = await backendOutputClient.getOutput(backendIdentifier); } catch (error) { - if ( - error instanceof BackendOutputClientError && - error.code === BackendOutputClientErrorType.DEPLOYMENT_IN_PROGRESS - ) { - throw new AmplifyUserError( - 'DeploymentInProgressError', - { - message: 'Deployment is currently in progress.', - resolution: 'Re-run this command once the deployment completes.', - }, - error - ); + if (BackendOutputClientError.isBackendOutputClientError(error)) { + switch (error.code) { + case BackendOutputClientErrorType.DEPLOYMENT_IN_PROGRESS: + throw new AmplifyUserError( + 'DeploymentInProgressError', + { + message: 'Deployment is currently in progress.', + resolution: + 'Re-run this command once the deployment completes.', + }, + error + ); + case BackendOutputClientErrorType.NO_STACK_FOUND: + throw new AmplifyUserError( + 'StackDoesNotExistError', + { + message: 'Stack does not exist.', + resolution: + 'Ensure the CloudFormation stack ID or Amplify App ID and branch specified are correct and exists, then re-run this command.', + }, + error + ); + case BackendOutputClientErrorType.NO_OUTPUTS_FOUND: + throw new AmplifyUserError( + 'AmplifyOutputsNotFoundError', + { + message: 'Amplify outputs not found in stack metadata', + resolution: `Ensure the CloudFormation stack ID or Amplify App ID and branch specified are correct and exists. + If this is a new sandbox or branch deployment, wait for the deployment to be successfully finished and try again.`, + }, + error + ); + case BackendOutputClientErrorType.CREDENTIALS_ERROR: + throw new AmplifyUserError( + 'CredentialsError', + { + message: + 'Unable to get backend outputs due to invalid credentials.', + resolution: + 'Ensure your AWS credentials are correctly set and refreshed.', + }, + error + ); + case BackendOutputClientErrorType.ACCESS_DENIED: + throw new AmplifyUserError( + 'AccessDeniedError', + { + message: + 'Unable to get backend outputs due to insufficient permissions.', + resolution: + 'Ensure you have permissions to call cloudformation:GetTemplateSummary.', + }, + error + ); + default: + throw error; + } } - if ( - error instanceof BackendOutputClientError && - error.code === BackendOutputClientErrorType.NO_STACK_FOUND - ) { - throw new AmplifyUserError( - 'StackDoesNotExistError', - { - message: 'Stack does not exist.', - resolution: - 'Ensure the CloudFormation stack ID or Amplify App ID and branch specified are correct and exists, then re-run this command.', - }, - error - ); - } - if ( - error instanceof BackendOutputClientError && - error.code === BackendOutputClientErrorType.CREDENTIALS_ERROR - ) { - throw new AmplifyUserError( - 'CredentialsError', - { - message: - 'Unable to get backend outputs due to invalid credentials.', - resolution: - 'Ensure your AWS credentials are correctly set and refreshed.', - }, - error - ); - } - if ( - error instanceof BackendOutputClientError && - error.code === BackendOutputClientErrorType.ACCESS_DENIED - ) { - throw new AmplifyUserError( - 'AccessDeniedError', - { - message: - 'Unable to get backend outputs due to insufficient permissions.', - resolution: - 'Ensure you have permissions to call cloudformation:GetTemplateSummary.', - }, - error - ); - } - throw error; } diff --git a/packages/cli/src/commands/generate/generate_command.ts b/packages/cli/src/commands/generate/generate_command.ts index 4596da971b6..a84157a5481 100644 --- a/packages/cli/src/commands/generate/generate_command.ts +++ b/packages/cli/src/commands/generate/generate_command.ts @@ -4,6 +4,7 @@ import { GenerateFormsCommand } from './forms/generate_forms_command.js'; import { GenerateGraphqlClientCodeCommand } from './graphql-client-code/generate_graphql_client_code_command.js'; import { CommandMiddleware } from '../../command_middleware.js'; import { GenerateSchemaCommand } from './schema-from-database/generate_schema_command.js'; +import { AmplifyUserError } from '@aws-amplify/platform-core'; /** * An entry point for generate command. @@ -62,6 +63,20 @@ export class GenerateCommand implements CommandModule { type: 'string', array: false, }) + .check(async (argv) => { + const stackNameRegex = + /^[a-zA-Z][-a-zA-Z0-9]{1,127}$|^arn:[-a-zA-Z0-9:/._+]$/; + if (argv['stack'] && typeof argv['stack'] === 'string') { + if (!argv.stack.match(stackNameRegex)) { + throw new AmplifyUserError('InvalidCommandInputError', { + message: `Invalid --stack name provided: ${argv.stack}`, + resolution: + 'Check the value of the stack name provided and try again.', + }); + } + } + return true; + }) .middleware([this.commandMiddleware.ensureAwsCredentialAndRegion]) ); }; diff --git a/packages/cli/src/commands/generate/generate_command_factory.test.ts b/packages/cli/src/commands/generate/generate_command_factory.test.ts index 4b7108ed844..fe340fb4eed 100644 --- a/packages/cli/src/commands/generate/generate_command_factory.test.ts +++ b/packages/cli/src/commands/generate/generate_command_factory.test.ts @@ -1,7 +1,10 @@ import yargs from 'yargs'; import { describe, it } from 'node:test'; import assert from 'node:assert'; -import { TestCommandRunner } from '../../test-utils/command_runner.js'; +import { + TestCommandError, + TestCommandRunner, +} from '../../test-utils/command_runner.js'; import { createGenerateCommand } from './generate_command_factory.js'; /** @@ -31,6 +34,20 @@ void describe('top level generate command', () => { assert.match(output, /Not enough non-option arguments/); }); + void it('fails if stack argument provided is invalid', async () => { + await assert.rejects( + () => commandRunner.runCommand('generate outputs --stack 3-Invalid'), + (error: TestCommandError) => { + assert.strictEqual(error.error.name, 'InvalidCommandInputError'); + assert.strictEqual( + error.error.message, + 'Invalid --stack name provided: 3-Invalid' + ); + return true; + } + ); + }); + void it('should throw if top level command handler is ever called', () => { assert.throws( () => generateCommand.handler({ $0: '', _: [] }), 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 287f3deb37e..ec8e4a6ed7e 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 80333f072ca..84b4a0f8e6c 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 2feb82e7eea..0edb427a00e 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 1144459a6f7..6fbb0ffdf8a 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 56c79d3cbda..a0896a7b30e 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/pipeline-deploy/pipeline_deploy_command.test.ts b/packages/cli/src/commands/pipeline-deploy/pipeline_deploy_command.test.ts index cfc591858f5..823c3bb6600 100644 --- a/packages/cli/src/commands/pipeline-deploy/pipeline_deploy_command.test.ts +++ b/packages/cli/src/commands/pipeline-deploy/pipeline_deploy_command.test.ts @@ -238,4 +238,32 @@ void describe('deploy command', () => { ClientConfigFormat.DART, ]); }); + + void it('throws when --branch argument has no input', async () => { + await assert.rejects( + async () => + await getCommandRunner(true).runCommand( + 'pipeline-deploy --app-id abc --branch' + ), + (error: TestCommandError) => { + assert.strictEqual(error.error.name, 'InvalidCommandInputError'); + assert.strictEqual(error.error.message, 'Invalid --branch or --app-id'); + return true; + } + ); + }); + + void it('throws when --app-id argument has no input', async () => { + await assert.rejects( + async () => + await getCommandRunner(true).runCommand( + 'pipeline-deploy --app-id --branch testBranch' + ), + (error: TestCommandError) => { + assert.strictEqual(error.error.name, 'InvalidCommandInputError'); + assert.strictEqual(error.error.message, 'Invalid --branch or --app-id'); + return true; + } + ); + }); }); diff --git a/packages/cli/src/commands/pipeline-deploy/pipeline_deploy_command.ts b/packages/cli/src/commands/pipeline-deploy/pipeline_deploy_command.ts index 503006f3c74..413b94f54c1 100644 --- a/packages/cli/src/commands/pipeline-deploy/pipeline_deploy_command.ts +++ b/packages/cli/src/commands/pipeline-deploy/pipeline_deploy_command.ts @@ -119,6 +119,15 @@ export class PipelineDeployCommand type: 'string', array: false, choices: Object.values(ClientConfigFormat), + }) + .check(async (argv) => { + if (argv['branch'].length === 0 || argv['app-id'].length === 0) { + throw new AmplifyUserError('InvalidCommandInputError', { + message: 'Invalid --branch or --app-id', + resolution: '--branch and --app-id must be at least 1 character', + }); + } + return true; }); }; } 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 62e80f946c1..c919cd61413 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-secret/sandbox_secret_set_command.test.ts b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_set_command.test.ts index cabae98bf0c..73e1969623b 100644 --- a/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_set_command.test.ts +++ b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_set_command.test.ts @@ -1,7 +1,10 @@ import { beforeEach, describe, it, mock } from 'node:test'; import { AmplifyPrompter, printer } from '@aws-amplify/cli-core'; import yargs, { CommandModule } from 'yargs'; -import { TestCommandRunner } from '../../../test-utils/command_runner.js'; +import { + TestCommandError, + TestCommandRunner, +} from '../../../test-utils/command_runner.js'; import assert from 'node:assert'; import { SandboxBackendIdResolver } from '../sandbox_id_resolver.js'; import { @@ -11,6 +14,7 @@ import { import { SandboxSecretSetCommand } from './sandbox_secret_set_command.js'; import { ReadStream } from 'node:tty'; import { PassThrough } from 'node:stream'; +import { AmplifyError } from '@aws-amplify/platform-core'; const testSecretName = 'testSecretName'; const testSecretValue = 'testSecretValue'; @@ -145,6 +149,23 @@ void describe('sandbox secret set command', () => { ]); }); + void it('throws AmplifyUserError if invalid secret name is provided', async () => { + const invalidSecretName = 'invalid@'; + await assert.rejects( + () => commandRunner.runCommand(`set ${invalidSecretName}`), + (err: TestCommandError) => { + assert.ok(AmplifyError.isAmplifyError(err.error)); + assert.strictEqual( + err.error.message, + 'Invalid secret name provided: invalid@' + ); + assert.strictEqual(err.error.name, 'InvalidCommandInputError'); + return true; + } + ); + assert.equal(secretSetMock.mock.callCount(), 0); + }); + void it('show --help', async () => { const output = await commandRunner.runCommand('set --help'); assert.match(output, /Set a sandbox secret/); diff --git a/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_set_command.ts b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_set_command.ts index 5d14a06ffe1..5b4c63abf59 100644 --- a/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_set_command.ts +++ b/packages/cli/src/commands/sandbox/sandbox-secret/sandbox_secret_set_command.ts @@ -6,6 +6,7 @@ import { ArgumentsKebabCase } from '../../../kebab_case.js'; import { SandboxCommandGlobalOptions } from '../option_types.js'; import { once } from 'events'; import { ReadStream } from 'node:tty'; +import { AmplifyUserError } from '@aws-amplify/platform-core'; /** * Command to set sandbox secret. @@ -57,11 +58,24 @@ export class SandboxSecretSetCommand * @inheritDoc */ builder = (yargs: Argv): Argv => { - return yargs.positional('secret-name', { - describe: 'Name of the secret to set', - type: 'string', - demandOption: true, - }); + return yargs + .positional('secret-name', { + describe: 'Name of the secret to set', + type: 'string', + demandOption: true, + }) + .check(async (argv) => { + if (argv['secret-name']) { + const secretNameRegex = /^[a-zA-Z0-9_.-]+$/; + if (!argv['secret-name'].match(secretNameRegex)) { + throw new AmplifyUserError('InvalidCommandInputError', { + message: `Invalid secret name provided: ${argv['secret-name']}`, + resolution: 'Use a secret name that matches [a-zA-Z0-9_.-]+', + }); + } + } + return true; + }); }; /** diff --git a/packages/cli/src/commands/sandbox/sandbox_command.test.ts b/packages/cli/src/commands/sandbox/sandbox_command.test.ts index 7b653d877cc..a05bfa3168b 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 356d0a8eb66..37c069286b6 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 07afc979d06..eace366cd20 100644 --- a/packages/cli/src/commands/sandbox/sandbox_command_factory.ts +++ b/packages/cli/src/commands/sandbox/sandbox_command_factory.ts @@ -61,7 +61,15 @@ export const createSandboxCommand = (): CommandModule< const eventHandlerFactory = new SandboxEventHandlerFactory( sandboxBackendIdPartsResolver.resolve, - async () => await new UsageDataEmitterFactory().getInstance(libraryVersion) + async () => { + const dependencies = await new PackageManagerControllerFactory() + .getPackageManagerController() + .tryGetDependencies(); + return await new UsageDataEmitterFactory().getInstance( + libraryVersion, + dependencies + ); + } ); const commandMiddleWare = new CommandMiddleware(printer); @@ -70,7 +78,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 2a3366612ca..e977b00f51a 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 858008c76e8..5d03b11c2c7 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 522f2cb9ec1..806020e9861 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 5a54ffa4a52..e957baa9515 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 5189c7de3f2..78fd56a5501 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 40b7882c3eb..cccafacd46b 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 9e35ebdc83d..d88e8c839c6 100644 --- a/packages/client-config/CHANGELOG.md +++ b/packages/client-config/CHANGELOG.md @@ -1,5 +1,98 @@ # @aws-amplify/client-config +## 1.5.5 + +### Patch Changes + +- a7506f9: wraps no outputs found error from backend output client +- Updated dependencies [a7506f9] +- Updated dependencies [a7506f9] +- Updated dependencies [a7506f9] + - @aws-amplify/model-generator@1.0.12 + - @aws-amplify/platform-core@1.5.0 + - @aws-amplify/plugin-types@1.7.0 + +## 1.5.4 + +### Patch Changes + +- 3cf0738: update detection of BackendOutputClientErrors +- Updated dependencies [95942c5] +- Updated dependencies [3cf0738] +- Updated dependencies [f679cf6] +- Updated dependencies [f193105] + - @aws-amplify/platform-core@1.4.0 + - @aws-amplify/deployed-backend-client@1.5.0 + - @aws-amplify/model-generator@1.0.10 + +## 1.5.3 + +### Patch Changes + +- 72b2fe0: update aws-cdk lib to ^2.168.0 +- Updated dependencies [cfdc854] +- Updated dependencies [72b2fe0] +- Updated dependencies [65abf6a] +- Updated dependencies [f6ba240] + - @aws-amplify/platform-core@1.3.0 + - @aws-amplify/plugin-types@1.6.0 + +## 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 74affd69dee..7566183f769 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.5", "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/platform-core": "^1.0.7", - "@aws-amplify/plugin-types": "^1.2.1", + "@aws-amplify/backend-output-schemas": "^1.4.0", + "@aws-amplify/deployed-backend-client": "^1.5.0", + "@aws-amplify/model-generator": "^1.0.12", + "@aws-amplify/platform-core": "^1.5.0", + "@aws-amplify/plugin-types": "^1.7.0", "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 0c269c82855..50d07f4d0ad 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 dfab479b0ee..8a00d4ebb4f 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 30ce2a06492..4bd0879e0b4 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 00000000000..f9aa397d970 --- /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 00000000000..560b0773ec5 --- /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 00000000000..b85a10ff30c --- /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 00000000000..bf89de55040 --- /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 d9cc71e1aac..501d52792b7 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 72023851a45..33a6e57ff0e 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 e05e68704ae..54994f0096d 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 479c542efa4..bfe343250d2 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 153b6add97d..fdb13552598 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 1d564740a09..ce5ce1b1105 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 db61d1b1de5..c3b89dcf6e0 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 69697ebe3e6..e181d3deb28 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 2552a1309ac..21abe85a034 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 039cf781c4f..b2563330b1a 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 9f1734ca2a2..b82c9620963 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,72 @@ void describe('UnifiedClientConfigGenerator', () => { ); }); + void it('throws user error if the stack outputs are undefined', async () => { + const outputRetrieval = mock.fn(() => { + throw new BackendOutputClientError( + BackendOutputClientErrorType.NO_OUTPUTS_FOUND, + 'stack outputs are undefined' + ); + }); + const modelSchemaAdapter = new ModelIntrospectionSchemaAdapter( + stubClientProvider + ); + + const configContributors = new ClientConfigContributorFactory( + modelSchemaAdapter + ).getContributors('1.3'); + + const clientConfigGenerator = new UnifiedClientConfigGenerator( + outputRetrieval, + configContributors + ); + + await assert.rejects( + () => clientConfigGenerator.generateClientConfig(), + (error: AmplifyUserError) => { + assert.strictEqual( + error.message, + 'Amplify outputs not found in stack metadata' + ); + assert.ok(error.resolution); + return true; + } + ); + }); + + 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 +771,7 @@ void describe('UnifiedClientConfigGenerator', () => { const configContributors = new ClientConfigContributorFactory( modelSchemaAdapter - ).getContributors('1.1'); + ).getContributors('1.3'); const clientConfigGenerator = new UnifiedClientConfigGenerator( outputRetrieval, @@ -495,7 +804,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 eb8397b01b1..068b2c03493 100644 --- a/packages/client-config/src/unified_client_config_generator.ts +++ b/packages/client-config/src/unified_client_config_generator.ts @@ -39,64 +39,72 @@ export class UnifiedClientConfigGenerator implements ClientConfigGenerator { try { output = await this.fetchOutput(); } catch (error) { - if ( - error instanceof BackendOutputClientError && - error.code === BackendOutputClientErrorType.DEPLOYMENT_IN_PROGRESS - ) { - throw new AmplifyUserError( - 'DeploymentInProgressError', - { - message: 'Deployment is currently in progress.', - resolution: 'Re-run this command once the deployment completes.', - }, - error - ); - } - if ( - error instanceof BackendOutputClientError && - error.code === BackendOutputClientErrorType.NO_STACK_FOUND - ) { - throw new AmplifyUserError( - 'StackDoesNotExistError', - { - message: 'Stack does not exist.', - resolution: - 'Ensure the CloudFormation stack ID or Amplify App ID and branch specified are correct and exists, then re-run this command.', - }, - error - ); - } - if ( - error instanceof BackendOutputClientError && - error.code === BackendOutputClientErrorType.CREDENTIALS_ERROR - ) { - throw new AmplifyUserError( - 'CredentialsError', - { - message: - 'Unable to get backend outputs due to invalid credentials.', - resolution: - 'Ensure your AWS credentials are correctly set and refreshed.', - }, - error - ); - } - if ( - error instanceof BackendOutputClientError && - error.code === BackendOutputClientErrorType.ACCESS_DENIED - ) { - throw new AmplifyUserError( - 'AccessDeniedError', - { - message: - 'Unable to get backend outputs due to insufficient permissions.', - resolution: - 'Ensure you have permissions to call cloudformation:GetTemplateSummary.', - }, - error - ); + if (BackendOutputClientError.isBackendOutputClientError(error)) { + switch (error.code) { + case BackendOutputClientErrorType.DEPLOYMENT_IN_PROGRESS: + throw new AmplifyUserError( + 'DeploymentInProgressError', + { + message: 'Deployment is currently in progress.', + resolution: + 'Re-run this command once the deployment completes.', + }, + error + ); + case BackendOutputClientErrorType.NO_STACK_FOUND: + throw new AmplifyUserError( + 'StackDoesNotExistError', + { + message: 'Stack does not exist.', + resolution: + 'Ensure the CloudFormation stack ID or Amplify App ID and branch specified are correct and exists, then re-run this command.', + }, + error + ); + case 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 + ); + case BackendOutputClientErrorType.NO_OUTPUTS_FOUND: + throw new AmplifyUserError( + 'AmplifyOutputsNotFoundError', + { + message: 'Amplify outputs not found in stack metadata', + resolution: `Ensure the CloudFormation stack ID or Amplify App ID and branch specified are correct and exists. + If this is a new sandbox or branch deployment, wait for the deployment to be successfully finished and try again.`, + }, + error + ); + case BackendOutputClientErrorType.CREDENTIALS_ERROR: + throw new AmplifyUserError( + 'CredentialsError', + { + message: + 'Unable to get backend outputs due to invalid credentials.', + resolution: + 'Ensure your AWS credentials are correctly set and refreshed.', + }, + error + ); + case BackendOutputClientErrorType.ACCESS_DENIED: + throw new AmplifyUserError( + 'AccessDeniedError', + { + message: + 'Unable to get backend outputs due to insufficient permissions.', + resolution: + 'Ensure you have permissions to call cloudformation:GetTemplateSummary.', + }, + error + ); + } } - throw error; } const backendOutput = unifiedBackendOutputSchema.parse(output); diff --git a/packages/create-amplify/CHANGELOG.md b/packages/create-amplify/CHANGELOG.md index aaf0868ebcb..89f428585f3 100644 --- a/packages/create-amplify/CHANGELOG.md +++ b/packages/create-amplify/CHANGELOG.md @@ -1,5 +1,28 @@ # create-amplify +## 1.0.7 + +### Patch Changes + +- f6ba240: Upgrade execa +- Updated dependencies [cfdc854] +- Updated dependencies [72b2fe0] +- Updated dependencies [65abf6a] +- Updated dependencies [0cf5c26] +- Updated dependencies [f6ba240] + - @aws-amplify/platform-core@1.3.0 + - @aws-amplify/plugin-types@1.6.0 + - @aws-amplify/cli-core@1.2.1 + +## 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 53c9a4f5c83..2bfad8b81c8 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.7", "type": "module", "main": "lib/index.js", "publishConfig": { @@ -17,10 +17,10 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-amplify/cli-core": "^1.1.1", - "@aws-amplify/platform-core": "^1.0.3", - "@aws-amplify/plugin-types": "^1.1.0", - "execa": "^8.0.1", + "@aws-amplify/cli-core": "^1.2.1", + "@aws-amplify/platform-core": "^1.3.0", + "@aws-amplify/plugin-types": "^1.6.0", + "execa": "^9.5.1", "kleur": "^4.1.5", "yargs": "^17.7.2" } diff --git a/packages/create-amplify/src/amplify_project_creator.test.ts b/packages/create-amplify/src/amplify_project_creator.test.ts index ea9407ebfb8..cf4675f9b14 100644 --- a/packages/create-amplify/src/amplify_project_creator.test.ts +++ b/packages/create-amplify/src/amplify_project_creator.test.ts @@ -109,6 +109,7 @@ void describe('AmplifyProjectCreator', () => { runWithPackageManager: mock.fn(() => Promise.resolve() as never), getCommand: (args: string[]) => `'npx ${args.join(' ')}'`, allowsSignalPropagation: () => true, + tryGetDependencies: mock.fn(() => Promise.resolve([])), }; const projectRootValidatorMock = { validate: mock.fn() }; const gitIgnoreInitializerMock = { ensureInitialized: mock.fn() }; diff --git a/packages/create-amplify/src/initial_project_file_generator.test.ts b/packages/create-amplify/src/initial_project_file_generator.test.ts index 8fcfb9190ac..39bed0b4889 100644 --- a/packages/create-amplify/src/initial_project_file_generator.test.ts +++ b/packages/create-amplify/src/initial_project_file_generator.test.ts @@ -17,6 +17,7 @@ void describe('InitialProjectFileGenerator', () => { runWithPackageManager: mock.fn(() => Promise.resolve() as never), getCommand: (args: string[]) => `'npx ${args.join(' ')}'`, allowsSignalPropagation: () => true, + tryGetDependencies: mock.fn(() => Promise.resolve([])), }; beforeEach(() => { executeWithDebugLoggerMock.mock.resetCalls(); diff --git a/packages/deployed-backend-client/API.md b/packages/deployed-backend-client/API.md index c0dfc594729..3c5353103ac 100644 --- a/packages/deployed-backend-client/API.md +++ b/packages/deployed-backend-client/API.md @@ -89,6 +89,7 @@ export class BackendOutputClientError extends Error { constructor(code: BackendOutputClientErrorType, message: string, options?: ErrorOptions); // (undocumented) code: BackendOutputClientErrorType; + static isBackendOutputClientError: (error: unknown) => error is BackendOutputClientError; } // @public (undocumented) diff --git a/packages/deployed-backend-client/CHANGELOG.md b/packages/deployed-backend-client/CHANGELOG.md index 3df442dc355..86d4dfd0444 100644 --- a/packages/deployed-backend-client/CHANGELOG.md +++ b/packages/deployed-backend-client/CHANGELOG.md @@ -1,5 +1,34 @@ # @aws-amplify/deployed-backend-client +## 1.5.0 + +### Minor Changes + +- 3cf0738: update detection of BackendOutputClientErrors + +### Patch Changes + +- Updated dependencies [95942c5] +- Updated dependencies [f679cf6] +- Updated dependencies [f193105] + - @aws-amplify/platform-core@1.4.0 + +## 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 6f631b038b4..440388678ca 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.5.0", "type": "module", "publishConfig": { "access": "public" @@ -20,8 +20,8 @@ "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.1", + "@aws-amplify/platform-core": "^1.4.0", + "@aws-amplify/plugin-types": "^1.2.2", "zod": "^3.22.2" }, "peerDependencies": { diff --git a/packages/deployed-backend-client/src/backend_output_client_factory.ts b/packages/deployed-backend-client/src/backend_output_client_factory.ts index 2dca755d66b..f9e553f369d 100644 --- a/packages/deployed-backend-client/src/backend_output_client_factory.ts +++ b/packages/deployed-backend-client/src/backend_output_client_factory.ts @@ -31,6 +31,30 @@ export class BackendOutputClientError extends Error { super(message, options); this.code = code; } + + /** + * This function is a type predicate for BackendOutputClientError. + * See https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates. + * + * Checks if error is an BackendOutputClientError by inspecting if required properties are set. + * This is recommended instead of instanceof operator. + * The instance of operator does not work as expected if BackendOutputClientError 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 isBackendOutputClientError = ( + error: unknown + ): error is BackendOutputClientError => { + return ( + error instanceof Error && + 'code' in error && + typeof error.code === 'string' && + (Object.values(BackendOutputClientErrorType) as unknown[]).includes( + error.code + ) && + typeof error.message === 'string' + ); + }; } /** diff --git a/packages/deployed-backend-client/src/deployed_backend_client.ts b/packages/deployed-backend-client/src/deployed_backend_client.ts index 94b18def4ce..2ff69efe31b 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 96afbc73c2b..116042ad1c1 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 12a9e024f94..fb8d6900375 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 11cc1b7ab16..a32e2ca3afa 100644 --- a/packages/eslint-rules/src/index.ts +++ b/packages/eslint-rules/src/index.ts @@ -2,9 +2,14 @@ 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'; +import { backendOutputClientErrorNoInstanceOf } from './rules/backent_output_client_error_no_instance_of.js'; export const rules: Record = { 'amplify-error-name': amplifyErrorNameRule, + 'amplify-error-no-instanceof': amplifyErrorNoInstanceOf, + 'backend-output-client-error-no-instanceof': + backendOutputClientErrorNoInstanceOf, 'no-empty-catch': noEmptyCatchRule, 'prefer-amplify-errors': preferAmplifyErrorsRule, 'no-amplify-errors': noAmplifyErrors, @@ -15,6 +20,9 @@ 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/backend-output-client-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 +35,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 +50,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 00000000000..c805e88fbdd --- /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 00000000000..bd040d2134b --- /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/eslint-rules/src/rules/backend_output_client_error_no_instance_of.test.ts b/packages/eslint-rules/src/rules/backend_output_client_error_no_instance_of.test.ts new file mode 100644 index 00000000000..fec54d38412 --- /dev/null +++ b/packages/eslint-rules/src/rules/backend_output_client_error_no_instance_of.test.ts @@ -0,0 +1,32 @@ +import * as nodeTest from 'node:test'; +import { RuleTester } from '@typescript-eslint/rule-tester'; +import { backendOutputClientErrorNoInstanceOf } from './backent_output_client_error_no_instance_of'; + +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( + 'backend-output-client-error-no-instanceof', + backendOutputClientErrorNoInstanceOf, + { + valid: ['e instanceof Error'], + invalid: [ + { + code: 'e instanceof BackendOutputClientError', + errors: [ + { + messageId: 'noInstanceOfWithBackendOutputClientError', + }, + ], + }, + ], + } +); diff --git a/packages/eslint-rules/src/rules/backent_output_client_error_no_instance_of.ts b/packages/eslint-rules/src/rules/backent_output_client_error_no_instance_of.ts new file mode 100644 index 00000000000..11e0c6ae4ec --- /dev/null +++ b/packages/eslint-rules/src/rules/backent_output_client_error_no_instance_of.ts @@ -0,0 +1,43 @@ +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 backendOutputClientErrorNoInstanceOf = + 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 === 'BackendOutputClientError' + ) { + context.report({ + messageId: 'noInstanceOfWithBackendOutputClientError', + node, + }); + } + }, + }; + }, + meta: { + docs: { + description: + 'Instanceof operator must not be used with BackendOutputClientError.', + }, + messages: { + noInstanceOfWithBackendOutputClientError: + 'Do not use instanceof with BackendOutputClientError. Use BackendOutputClientError.isBackendOutputClientError instead.', + }, + type: 'problem', + schema: [], + }, + defaultOptions: [], + }); diff --git a/packages/form-generator/CHANGELOG.md b/packages/form-generator/CHANGELOG.md index a2b4d28246d..b09cd2d4edc 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 37703ba852a..31a56527203 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 9f98f919436..1d268581bf7 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 0b29d0e6c28..3ae37595923 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 528477bdbca..e49fedd0c34 100644 --- a/packages/integration-tests/CHANGELOG.md +++ b/packages/integration-tests/CHANGELOG.md @@ -1,5 +1,33 @@ # @aws-amplify/integration-tests +## 0.6.1 + +### Patch Changes + +- 72b2fe0: update aws-cdk lib to ^2.168.0 + +## 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 780c9a1929d..f5e3f4b709e 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 60073eac05a..2a60ce15446 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.1", "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/data-schema": "^1.0.0", - "@aws-amplify/deployed-backend-client": "^1.3.0", - "@aws-amplify/platform-core": "^1.1.0", - "@aws-amplify/plugin-types": "^1.2.1", + "@aws-amplify/ai-constructs": "^1.1.0", + "@aws-amplify/auth-construct": "^1.5.1", + "@aws-amplify/backend": "^1.9.0", + "@aws-amplify/backend-ai": "^1.1.0", + "@aws-amplify/backend-secret": "^1.1.4", + "@aws-amplify/client-config": "^1.5.3", + "@aws-amplify/data-schema": "^1.13.4", + "@aws-amplify/deployed-backend-client": "^1.4.1", + "@aws-amplify/platform-core": "^1.3.0", + "@aws-amplify/plugin-types": "^1.6.0", "@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,11 +30,12 @@ "@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.168.0", "constructs": "^10.0.0", - "execa": "^8.0.1", + "execa": "^9.5.1", "fs-extra": "^11.1.1", "glob": "^10.2.7", "graphql-tag": "^2.12.6", @@ -41,6 +43,7 @@ "node-fetch": "^3.3.2", "semver": "^7.6.3", "ssh2": "^1.15.0", + "strip-ansi": "^6.0.1", "uuid": "^9.0.1" }, "license": "Apache-2.0" diff --git a/packages/integration-tests/src/amplify_app_pool.ts b/packages/integration-tests/src/amplify_app_pool.ts index f3cf955e502..026a4625d0a 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 a08b1455bd4..d21f9f8d86c 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 455c8b0cf64..ae484e4b9ef 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 ef8830e680d..933415ddd30 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 cc90bcd05d0..3375aee6e16 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 0f55e04c6fc..90c9e9d5898 100644 --- a/packages/integration-tests/src/process-controller/execa_process_killer.ts +++ b/packages/integration-tests/src/process-controller/execa_process_killer.ts @@ -1,10 +1,12 @@ -import { ExecaChildProcess, execa } from 'execa'; +import { ExecaMethod, execa } from 'execa'; /** * Kills the given process (equivalent of sending CTRL-C) * @param processInstance an instance of execa child process */ -export const killExecaProcess = async (processInstance: ExecaChildProcess) => { +export const killExecaProcess = async ( + processInstance: ReturnType +) => { if (process.platform.startsWith('win')) { if (typeof processInstance.pid !== 'number') { throw new Error('Cannot kill the process that does not have pid'); @@ -14,8 +16,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.ts b/packages/integration-tests/src/process-controller/predicated_action.ts index c63316970cf..82fe61f73b0 100644 --- a/packages/integration-tests/src/process-controller/predicated_action.ts +++ b/packages/integration-tests/src/process-controller/predicated_action.ts @@ -1,4 +1,4 @@ -import { ExecaChildProcess } from 'execa'; +import { ExecaMethod } from 'execa'; /** * Type of actions a user can take with their app. @@ -12,11 +12,11 @@ export enum ActionType { type SendInputToProcessAction = { actionType: ActionType.SEND_INPUT_TO_PROCESS; - action: (execaProcess: ExecaChildProcess) => Promise; + action: (execaProcess: ReturnType) => Promise; }; type KillProcess = { actionType: ActionType.KILL_PROCESS; - action: (execaProcess: ExecaChildProcess) => Promise; + action: (execaProcess: ReturnType) => Promise; }; type UpdateFileContentAction = { actionType: ActionType.UPDATE_FILE_CONTENT; 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 7f38d4789b6..efa6bbc178b 100644 --- a/packages/integration-tests/src/process-controller/predicated_action_macros.ts +++ b/packages/integration-tests/src/process-controller/predicated_action_macros.ts @@ -30,6 +30,12 @@ export const waitForSandboxToBecomeIdle = () => 'Watching for file changes...' ); +/** + * Reusable predicates: Wait for sandbox to indicate that it's executing hotswap deployment, i.e. "hotswapping resources:" + */ +export const waitForSandboxToBeginHotswappingResources = () => + new PredicatedActionBuilder().waitForLineIncludes('hotswapping resources:'); + /** * Reusable predicated action: Wait for sandbox delete to prompt to delete all the resource and respond with yes */ @@ -40,16 +46,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 +55,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/process-controller/predicated_action_queue_builder.ts b/packages/integration-tests/src/process-controller/predicated_action_queue_builder.ts index 0b5506db6a7..06429df8290 100644 --- a/packages/integration-tests/src/process-controller/predicated_action_queue_builder.ts +++ b/packages/integration-tests/src/process-controller/predicated_action_queue_builder.ts @@ -5,9 +5,10 @@ import { } from './predicated_action.js'; import os from 'os'; import fs from 'fs/promises'; +import stripANSI from 'strip-ansi'; import { killExecaProcess } from './execa_process_killer.js'; -import { ExecaChildProcess } from 'execa'; +import { ExecaMethod } from 'execa'; import { CopyDefinition } from './types.js'; export const CONTROL_C = '\x03'; @@ -54,7 +55,7 @@ export class PredicatedActionBuilder { str === CONTROL_C ? ActionType.KILL_PROCESS : ActionType.SEND_INPUT_TO_PROCESS, - action: async (execaProcess: ExecaChildProcess) => { + action: async (execaProcess: ReturnType) => { if (str === CONTROL_C) { await killExecaProcess(execaProcess); } else { @@ -92,6 +93,7 @@ export class PredicatedActionBuilder { action: (strWithDeploymentTime: string) => { // the time can be in fractional or whole seconds. 24.3, 24, 24.22 etc. const regex = /^✨ {2}Total time: (\d*\.*\d*)s.*$/; + strWithDeploymentTime = stripANSI(strWithDeploymentTime); const deploymentTime = strWithDeploymentTime.match(regex); if ( deploymentTime && diff --git a/packages/integration-tests/src/process-controller/process_controller.ts b/packages/integration-tests/src/process-controller/process_controller.ts index 6df33bf7669..f98d628a806 100644 --- a/packages/integration-tests/src/process-controller/process_controller.ts +++ b/packages/integration-tests/src/process-controller/process_controller.ts @@ -58,11 +58,11 @@ export class ProcessController { } if (process.stdout) { - void execaProcess.pipeStdout?.(process.stdout); + execaProcess.stdout.pipe(process.stdout); } if (process.stderr) { - void execaProcess.pipeStderr?.(process.stderr); + execaProcess.stderr.pipe(process.stderr); } if (!execaProcess.stdout) { 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 00000000000..33f0b23b50b --- /dev/null +++ b/packages/integration-tests/src/resource-creation/auth_resource_creator.ts @@ -0,0 +1,377 @@ +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, + permissionBoundaryArn: string + ) => { + const groupRole = await this.createRoleBase({ + RoleName: 'ref-auth-group-role', + AssumeRolePolicyDocument: this.getIdentityPoolAssumeRolePolicyDocument( + identityPoolId, + 'authenticated' + ), + PermissionsBoundary: permissionBoundaryArn, + }); + 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, + permissionBoundaryArn: string + ) => { + const authRole = await this.createRoleBase({ + RoleName: `ref-auth-role`, + AssumeRolePolicyDocument: this.getIdentityPoolAssumeRolePolicyDocument( + identityPoolId, + 'authenticated' + ), + PermissionsBoundary: permissionBoundaryArn, + }); + const unauthRole = await this.createRoleBase({ + RoleName: `ref-unauth-role`, + AssumeRolePolicyDocument: this.getIdentityPoolAssumeRolePolicyDocument( + identityPoolId, + 'unauthenticated' + ), + PermissionsBoundary: permissionBoundaryArn, + }); + 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 00000000000..ba527862910 --- /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')) || + /npm create ['"]?amplify/.test(message) || + /pnpm create ['"]?amplify/.test(message); + return didProcessExitWithError && isKnownProcess; + }; +} diff --git a/packages/integration-tests/src/setup_package_manager.ts b/packages/integration-tests/src/setup_package_manager.ts index e1b51622f0c..ad72f3270fa 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 0939e5709cd..8cddf8b2cf1 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 00000000000..bdbedc29887 --- /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 88397deba46..8f8d5c6f2dc 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 189405ef6f0..b7b3c1333a1 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 c9ea7500de4..00000000000 --- 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 00000000000..6730123191a --- /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 00000000000..b988eb72c22 --- /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 00000000000..2b6f3246246 --- /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 00000000000..1bfa79ce14c --- /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 00000000000..88f36000bf5 --- /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 00000000000..8725ac07c61 --- /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 00000000000..b26f10daeae --- /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 00000000000..4654bac4f4e --- /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_access_from_function_project.deployment.test.ts b/packages/integration-tests/src/test-e2e/deployment/data_access_from_function_project.deployment.test.ts new file mode 100644 index 00000000000..14bd0fe025f --- /dev/null +++ b/packages/integration-tests/src/test-e2e/deployment/data_access_from_function_project.deployment.test.ts @@ -0,0 +1,4 @@ +import { defineDeploymentTest } from './deployment.test.template.js'; +import { DataAccessFromFunctionTestProjectCreator } from '../../test-project-setup/data_access_from_function_project.js'; + +defineDeploymentTest(new DataAccessFromFunctionTestProjectCreator()); 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 00000000000..0fd2ea5062d --- /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 00000000000..4a17afb9b05 --- /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 00000000000..af3e619d9de --- /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 00000000000..2a30ffa54e3 --- /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 394b3c8b3ea..00000000000 --- 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 00000000000..42fc2460d16 --- /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 00000000000..16118e48ea7 --- /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 00000000000..16def745992 --- /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 00000000000..d024c5f764a --- /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 00000000000..b4ee374c491 --- /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 00000000000..17f47a7efb3 --- /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_access_from_function_project.sandbox.test.ts b/packages/integration-tests/src/test-e2e/sandbox/data_access_from_function_project.sandbox.test.ts new file mode 100644 index 00000000000..c71b261b0ef --- /dev/null +++ b/packages/integration-tests/src/test-e2e/sandbox/data_access_from_function_project.sandbox.test.ts @@ -0,0 +1,4 @@ +import { defineSandboxTest } from './sandbox.test.template.js'; +import { DataAccessFromFunctionTestProjectCreator } from '../../test-project-setup/data_access_from_function_project.js'; + +defineSandboxTest(new DataAccessFromFunctionTestProjectCreator()); 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 00000000000..b4132444451 --- /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 00000000000..3f19b529d56 --- /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 00000000000..c7c19bdcf02 --- /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 00000000000..8685f1382c1 --- /dev/null +++ b/packages/integration-tests/src/test-e2e/sandbox/sandbox.test.template.ts @@ -0,0 +1,126 @@ +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, + waitForSandboxToBeginHotswappingResources, +} 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'; +import { runWithRetry } from '../../retry.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) { + // retry hotswapping resources if deployment time is higher than the threshold + await runWithRetry( + async (attempt) => { + if (attempt > 1) { + // reset test project to pre-update state to retry hotswap + await testProject.reset(); + } + + const processController = ampxCli( + ['sandbox', '--dirToWatch', 'amplify'], + testProject.projectDirPath, + { + env: sharedSecretsEnv, + } + ); + + for (const update of updates) { + processController + .do(replaceFiles(update.replacements)) + .do(waitForSandboxToBeginHotswappingResources()); + if (update.deployThresholdSec) { + processController.do( + ensureDeploymentTimeLessThan(update.deployThresholdSec) + ); + } + } + + // Execute the process. + await processController.do(interruptSandbox()).run(); + }, + (error) => error.message.includes('Deployment time') + ); + + 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 7294be74830..688261096f3 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,17 @@ 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, + maxRetries: 3, + }); }); 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 8aae8bd8e65..998e2a4afa8 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 @@ -1,7 +1,7 @@ import { afterEach, before, beforeEach, describe, it } from 'node:test'; import fs from 'fs/promises'; import path from 'path'; -import os from 'os'; +import os, { userInfo } from 'os'; import { execa } from 'execa'; import { ampxCli } from '../process-controller/process_controller.js'; import { TestBranch, amplifyAppPool } from '../amplify_app_pool.js'; @@ -14,12 +14,15 @@ import { import { confirmDeleteSandbox, interruptSandbox, - rejectCleanupSandbox, + replaceFiles, waitForSandboxDeploymentToPrintTotalTime, + waitForSandboxToBeginHotswappingResources, } from '../process-controller/predicated_action_macros.js'; import { BackendIdentifierConversions } from '@aws-amplify/platform-core'; import { e2eToolingClientConfig } from '../e2e_tooling_client_config.js'; import { amplifyAtTag } from '../constants.js'; +import { FunctionCodeHotswapTestProjectCreator } from '../test-project-setup/live-dependency-health-checks-projects/function_code_hotswap.js'; +import { BackendIdentifier } from '@aws-amplify/plugin-types'; const cfnClient = new CloudFormationClient(e2eToolingClientConfig); @@ -123,7 +126,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( @@ -136,4 +138,47 @@ void describe('Live dependency health checks', { concurrency: true }, () => { .run(); }); }); + + void describe('sandbox hotswap', () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'test-amplify')); + }); + + afterEach(async () => { + await fs.rm(tempDir, { recursive: true }); + }); + + void it('can hotswap function code', async () => { + const projectCreator = new FunctionCodeHotswapTestProjectCreator(); + const testProject = await projectCreator.createProject(tempDir); + + const sandboxBackendIdentifier: BackendIdentifier = { + type: 'sandbox', + namespace: testProject.name, + name: userInfo().username, + }; + + await testProject.deploy(sandboxBackendIdentifier); + + const processController = ampxCli( + ['sandbox', '--dirToWatch', 'amplify'], + testProject.projectDirPath + ); + const updates = await testProject.getUpdates(); + for (const update of updates) { + processController + .do(replaceFiles(update.replacements)) + .do(waitForSandboxToBeginHotswappingResources()) + .do(waitForSandboxDeploymentToPrintTotalTime()); + } + + // Execute the process. + await processController.do(interruptSandbox()).run(); + + // Clean up + await testProject.tearDown(sandboxBackendIdentifier); + }); + }); }); 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 b41d4bd1b12..4430a49397b 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 00000000000..eb8056db5a4 --- /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 2beb33dff06..97a3692efed 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 67d3b677585..72db39e0feb 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 6f0e94efe2e..c1b6d2ef6e0 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 00000000000..b75882fb144 --- /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 00000000000..d00c86facd2 --- /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 9c1c52b5a78..4418a324b83 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 97afcf198b8..b86f280f32f 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 7e6b367e5a5..0a20230e25a 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_access_from_function_project.ts b/packages/integration-tests/src/test-project-setup/data_access_from_function_project.ts new file mode 100644 index 00000000000..b85a67f026e --- /dev/null +++ b/packages/integration-tests/src/test-project-setup/data_access_from_function_project.ts @@ -0,0 +1,164 @@ +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 { BackendIdentifier } from '@aws-amplify/plugin-types'; +import { generateClientConfig } from '@aws-amplify/client-config'; +import { + ApolloClient, + ApolloLink, + HttpLink, + InMemoryCache, +} from '@apollo/client/core'; +import { AUTH_TYPE, createAuthLink } from 'aws-appsync-auth-link'; +import { gql } from 'graphql-tag'; +import assert from 'assert'; +import { NormalizedCacheObject } from '@apollo/client'; +import { e2eToolingClientConfig } from '../e2e_tooling_client_config.js'; + +/** + * Creates the data and function test project. + */ +export class DataAccessFromFunctionTestProjectCreator + implements TestProjectCreator +{ + readonly name = 'data-access-from-function'; + + /** + * 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 DataAccessFromFunctionTestProject( + projectName, + projectRoot, + projectAmplifyDir, + this.cfnClient, + this.amplifyClient + ); + await fs.cp( + project.sourceProjectAmplifyDirURL, + project.projectAmplifyDirPath, + { + recursive: true, + } + ); + return project; + }; +} + +/** + * The data and function test project. + */ +class DataAccessFromFunctionTestProject extends TestProjectBase { + readonly sourceProjectDirPath = + '../../src/test-projects/data_access_from_function'; + + 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 + ); + } + + override async assertPostDeployment( + backendId: BackendIdentifier + ): Promise { + await super.assertPostDeployment(backendId); + + const clientConfig = await generateClientConfig(backendId, '1.1'); + if (!clientConfig.data?.url) { + throw new Error('Data and function project must include data'); + } + if (!clientConfig.data.api_key) { + throw new Error('Data and function project must include api_key'); + } + + const httpLink = new HttpLink({ uri: clientConfig.data.url }); + const link = ApolloLink.from([ + createAuthLink({ + url: clientConfig.data.url, + region: clientConfig.data.aws_region, + auth: { + type: AUTH_TYPE.API_KEY, + apiKey: clientConfig.data.api_key, + }, + }), + // see https://github.com/awslabs/aws-mobile-appsync-sdk-js/issues/473#issuecomment-543029072 + httpLink, + ]); + const apolloClient = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); + + await this.assertDataFunctionCallSucceeds(apolloClient); + await this.assertNoopWithImportCallSucceeds(apolloClient); + } + + private assertDataFunctionCallSucceeds = async ( + apolloClient: ApolloClient + ): Promise => { + // The todoCount query calls the todoCount lambda + const response = await apolloClient.query({ + query: gql` + query todoCount { + todoCount + } + `, + variables: {}, + }); + + // Assert the expected lambda call return + assert.deepEqual(response.data, { todoCount: 0 }); + }; + + private assertNoopWithImportCallSucceeds = async ( + apolloClient: ApolloClient + ): Promise => { + // The noopImport query calls the noopImport lambda + const response = await apolloClient.query({ + query: gql` + query noopImport { + noopImport + } + `, + variables: {}, + }); + + // Assert the expected lambda call return + assert.deepEqual(response.data, { noopImport: 'STATIC TEST RESPONSE' }); + }; +} 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 588f5c7f56c..5567a5958ef 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/live-dependency-health-checks-projects/README.md b/packages/integration-tests/src/test-project-setup/live-dependency-health-checks-projects/README.md new file mode 100644 index 00000000000..03e80634532 --- /dev/null +++ b/packages/integration-tests/src/test-project-setup/live-dependency-health-checks-projects/README.md @@ -0,0 +1,5 @@ +Projects in this directory are meant for `live-dependency-health-checks` (aka canaries). + +1. These projects must not be used in e2e tests to provide deep functional coverage. +2. These projects must be lightweight to provide fast runtime and stability. +3. These projects must cover only P0 scenarios we care most. (That are not covered by "getting started" flow, aka `create-amplify`). diff --git a/packages/integration-tests/src/test-project-setup/live-dependency-health-checks-projects/function_code_hotswap.ts b/packages/integration-tests/src/test-project-setup/live-dependency-health-checks-projects/function_code_hotswap.ts new file mode 100644 index 00000000000..7da4b3ffaec --- /dev/null +++ b/packages/integration-tests/src/test-project-setup/live-dependency-health-checks-projects/function_code_hotswap.ts @@ -0,0 +1,143 @@ +import fs from 'fs/promises'; +import { createEmptyAmplifyProject } from '../create_empty_amplify_project.js'; +import { CloudFormationClient } from '@aws-sdk/client-cloudformation'; +import { TestProjectBase, TestProjectUpdate } from '../test_project_base.js'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import path from 'path'; +import { TestProjectCreator } from '../test_project_creator.js'; +import { AmplifyClient } from '@aws-sdk/client-amplify'; +import { e2eToolingClientConfig } from '../../e2e_tooling_client_config.js'; +import { execa } from 'execa'; + +/** + * Creates test projects with function hotswap. + */ +export class FunctionCodeHotswapTestProjectCreator + implements TestProjectCreator +{ + readonly name = 'function-code-hotswap'; + + /** + * 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 FunctionCodeHotswapTestTestProject( + projectName, + projectRoot, + projectAmplifyDir, + this.cfnClient, + this.amplifyClient + ); + await fs.cp( + project.sourceProjectAmplifyDirURL, + project.projectAmplifyDirPath, + { + recursive: true, + } + ); + + // we're not starting from create flow. install latest versions of dependencies. + await execa( + 'npm', + [ + 'install', + '@aws-amplify/backend', + '@aws-amplify/backend-cli', + 'aws-cdk@^2', + 'aws-cdk-lib@^2', + 'constructs@^10.0.0', + 'typescript@^5.0.0', + 'tsx', + 'esbuild', + ], + { + cwd: projectRoot, + stdio: 'inherit', + } + ); + + return project; + }; +} + +/** + * Test project with function hotswap. + */ +class FunctionCodeHotswapTestTestProject 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 + readonly sourceProjectRootPath = + '../../../src/test-projects/live-dependency-health-checks-projects/function-code-hotswap'; + + readonly sourceProjectRootURL: URL = new URL( + this.sourceProjectRootPath, + import.meta.url + ); + + readonly sourceProjectAmplifyDirURL: URL = new URL( + `${this.sourceProjectRootPath}/amplify`, + import.meta.url + ); + + private readonly sourceProjectUpdateDirURL: URL = new URL( + `${this.sourceProjectRootPath}/hotswap-update-files`, + 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 + ); + } + + /** + * @inheritdoc + */ + override async getUpdates(): Promise { + return [ + { + replacements: [ + this.getUpdateReplacementDefinition('func-src/handler.ts'), + ], + }, + ]; + } + + private getUpdateReplacementDefinition = (suffix: string) => ({ + source: this.getSourceProjectUpdatePath(suffix), + destination: this.getTestProjectPath(suffix), + }); + + private getSourceProjectUpdatePath = (suffix: string) => + pathToFileURL( + path.join(fileURLToPath(this.sourceProjectUpdateDirURL), suffix) + ); + + private getTestProjectPath = (suffix: string) => + pathToFileURL(path.join(this.projectAmplifyDirPath, suffix)); +} 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 983f11a211d..b7c1886df18 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 00000000000..8932cc83b26 --- /dev/null +++ b/packages/integration-tests/src/test-project-setup/reference_auth_project.ts @@ -0,0 +1,345 @@ +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 accountId = userPool.Arn!.split(':')[4]; // arn:aws:cognito-idp:::userpool/ + const permissionBoundaryArn = `arn:aws:iam::${accountId}:policy/CreateRolePermissionBoundaryPolicy`; + + 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, + permissionBoundaryArn + ); + + const adminGroup = await this.authResourceCreator.setupUserPoolGroup( + 'ADMINS', + userPool.Id!, + identityPool.IdentityPoolId, + permissionBoundaryArn + ); + 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 618ec72b26a..66704f60cfc 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 c6ab0284fef..c56209a6d4c 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'; @@ -25,6 +26,7 @@ import path from 'path'; import { AmplifyClient } from '@aws-sdk/client-amplify'; import { pathToFileURL } from 'url'; import isMatch from 'lodash.ismatch'; +import { setupDirAsEsmModule } from './setup_dir_as_esm_module.js'; export type PlatformDeploymentThresholds = { onWindows: number; @@ -44,7 +46,7 @@ export type TestProjectUpdate = { * Windows has a separate threshold because it is consistently slower than other platforms * https://github.com/microsoft/Windows-Dev-Performance/issues/17 */ - deployThresholdSec: PlatformDeploymentThresholds; + deployThresholdSec?: PlatformDeploymentThresholds; }; /** @@ -77,7 +79,6 @@ export abstract class TestProjectBase { }) .do(waitForSandboxDeploymentToPrintTotalTime()) .do(interruptSandbox()) - .do(rejectCleanupSandbox()) .run(); } else { await ampxCli( @@ -102,21 +103,82 @@ 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; + } + /** * Gets all project update cases. Override this method if the update (hotswap) test is relevant. */ @@ -186,4 +248,15 @@ export abstract class TestProjectBase { assert.ok(isMatch(currentCodebaseOutputs, npmOutputs)); } + + /** + * Resets the project to its initial state + */ + async reset() { + await fsp.rm(this.projectAmplifyDirPath, { recursive: true, force: true }); + await fsp.cp(this.sourceProjectAmplifyDirURL, this.projectAmplifyDirPath, { + recursive: true, + }); + await setupDirAsEsmModule(this.projectAmplifyDirPath); + } } 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 4f8ad607a23..73271567590 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 00000000000..d4676a53901 --- /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 00000000000..406c9390e83 --- /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 00000000000..e52be2ea0a1 --- /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 00000000000..f5a3fff4558 --- /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/advanced-auth-and-functions/amplify/func-src/handler_provider.ts b/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/func-src/handler_provider.ts new file mode 100644 index 00000000000..f7c71c983ca --- /dev/null +++ b/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/func-src/handler_provider.ts @@ -0,0 +1,7 @@ +/** + * This function is a simple hello world function. + * It's for not direct defineFunction, it's provided function + */ +export const handler = async () => { + return 'Hello from NodeJS Function!'; +}; 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 00000000000..8784959dffc --- /dev/null +++ b/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/function.ts @@ -0,0 +1,46 @@ +import { defineFunction } from '@aws-amplify/backend'; +import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { Runtime } from 'aws-cdk-lib/aws-lambda'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +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 funcProvided = defineFunction((scope) => { + return new NodejsFunction(scope, 'funcProvided', { + entry: path.resolve( + fileURLToPath(import.meta.url), + '..', + 'func-src', + 'handler_provider.ts' + ), + runtime: Runtime.NODEJS_18_X, + }); +}); + +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 00000000000..4841663dc03 --- /dev/null +++ b/packages/integration-tests/src/test-projects/advanced-auth-and-functions/amplify/test_factories.ts @@ -0,0 +1,19 @@ +import { + funcCustomEmailSender, + funcNoMinify, + funcWithAwsSdk, + funcWithSchedule, + funcWithSsm, + funcProvided, +} from './function.js'; +import { auth } from './auth/resource.js'; + +export const authAndFunctions = { + auth, + funcWithSsm, + funcWithAwsSdk, + funcWithSchedule, + funcNoMinify, + funcCustomEmailSender, + funcProvided, +}; 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 00000000000..284ced66562 --- /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 00000000000..57a0b4da492 --- /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 00000000000..fdee70ae7dc --- /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 00000000000..7bf6de291a0 --- /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 00000000000..ad5a6a9ead9 --- /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 00000000000..d62f7c08be6 --- /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 00000000000..cd2d8595084 --- /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 00000000000..68068eaff19 --- /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 00000000000..7d121dbf405 --- /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 00000000000..7bf6de291a0 --- /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 00000000000..ad5a6a9ead9 --- /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 00000000000..dba0d24f036 --- /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 3c6fa81c2e8..f6914ace4eb 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 af652ce8901..8a2256051e8 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 53d2944f5b7..2382dc82d9c 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 2ef65c955ba..07c19400c2d 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 2ae7efb16f7..ceed80b636b 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 e5ff3baa417..92733f12ec8 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 4cd85ed1e3e..8fdf38f0d28 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 2405878c75a..cfaf6da0a42 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 cfd30953e2a..3af6c5fecf8 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 49227af6158..fa024e13fc9 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 61a9171a16d..6032d8c60f1 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/data_access_from_function/amplify/backend.ts b/packages/integration-tests/src/test-projects/data_access_from_function/amplify/backend.ts new file mode 100644 index 00000000000..bd208c8043f --- /dev/null +++ b/packages/integration-tests/src/test-projects/data_access_from_function/amplify/backend.ts @@ -0,0 +1,6 @@ +import { defineBackend } from '@aws-amplify/backend'; +import { data } from './data/resource.js'; +import { todoCount } from './functions/todo-count/resource.js'; +import { customerS3Import } from './functions/customer-s3-import/resource.js'; + +const backend = defineBackend({ data, todoCount, customerS3Import }); diff --git a/packages/integration-tests/src/test-projects/data_access_from_function/amplify/data/resource.ts b/packages/integration-tests/src/test-projects/data_access_from_function/amplify/data/resource.ts new file mode 100644 index 00000000000..2bb4831790b --- /dev/null +++ b/packages/integration-tests/src/test-projects/data_access_from_function/amplify/data/resource.ts @@ -0,0 +1,43 @@ +import { a, ClientSchema, defineData } from '@aws-amplify/backend'; +import { todoCount } from '../functions/todo-count/resource.js'; +import { customerS3Import } from '../functions/customer-s3-import/resource.js'; + +const schema = a + .schema({ + Todo: a + .model({ + title: a.string().required(), + done: a.boolean().default(false), // default value is false + }) + .authorization((allow) => [allow.publicApiKey()]), + todoCount: a + .query() + .arguments({}) + .returns(a.integer()) + .handler(a.handler.function(todoCount)) + .authorization((allow) => [allow.publicApiKey()]), + noopImport: a + .query() + .arguments({}) + .returns(a.string()) + .handler(a.handler.function(customerS3Import)) + .authorization((allow) => [allow.publicApiKey()]), + }) + .authorization((allow) => [ + allow.resource(todoCount), + allow.resource(customerS3Import), + ]); + +export type Schema = ClientSchema; + +export const data = defineData({ + name: 'DATATEST', + 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/data_access_from_function/amplify/functions/customer-s3-import/handler.ts b/packages/integration-tests/src/test-projects/data_access_from_function/amplify/functions/customer-s3-import/handler.ts new file mode 100644 index 00000000000..fc4b448637f --- /dev/null +++ b/packages/integration-tests/src/test-projects/data_access_from_function/amplify/functions/customer-s3-import/handler.ts @@ -0,0 +1,22 @@ +import type { Handler } from 'aws-lambda'; +import { Amplify } from 'aws-amplify'; +import { generateClient } from 'aws-amplify/data'; +import type { Schema } from '../../data/resource.js'; +import { getAmplifyDataClientConfig } from '@aws-amplify/backend/function/runtime'; +// @ts-ignore +import { env } from '$amplify/env/customer-s3-import.js'; +import { S3Client } from '@aws-sdk/client-s3'; + +const { resourceConfig, libraryOptions } = await getAmplifyDataClientConfig( + env +); + +Amplify.configure(resourceConfig, libraryOptions); + +const client = generateClient(); + +export const handler: Handler = async () => { + const _s3Client = new S3Client(); + const _todos = await client.models.Todo.list(); + return 'STATIC TEST RESPONSE'; +}; diff --git a/packages/integration-tests/src/test-projects/data_access_from_function/amplify/functions/customer-s3-import/resource.ts b/packages/integration-tests/src/test-projects/data_access_from_function/amplify/functions/customer-s3-import/resource.ts new file mode 100644 index 00000000000..c985d40794d --- /dev/null +++ b/packages/integration-tests/src/test-projects/data_access_from_function/amplify/functions/customer-s3-import/resource.ts @@ -0,0 +1,7 @@ +import { defineFunction } from '@aws-amplify/backend'; + +export const customerS3Import = defineFunction({ + name: 'customer-s3-import', + entry: './handler.ts', + timeoutSeconds: 30, +}); diff --git a/packages/integration-tests/src/test-projects/data_access_from_function/amplify/functions/todo-count/handler.ts b/packages/integration-tests/src/test-projects/data_access_from_function/amplify/functions/todo-count/handler.ts new file mode 100644 index 00000000000..0383ebf092d --- /dev/null +++ b/packages/integration-tests/src/test-projects/data_access_from_function/amplify/functions/todo-count/handler.ts @@ -0,0 +1,20 @@ +import type { Handler } from 'aws-lambda'; +import { Amplify } from 'aws-amplify'; +import { generateClient } from 'aws-amplify/data'; +import type { Schema } from '../../data/resource.js'; +import { getAmplifyDataClientConfig } from '@aws-amplify/backend/function/runtime'; +// @ts-ignore +import { env } from '$amplify/env/todo-count.js'; + +const { resourceConfig, libraryOptions } = await getAmplifyDataClientConfig( + env +); + +Amplify.configure(resourceConfig, libraryOptions); + +const client = generateClient(); + +export const handler: Handler = async () => { + const todos = await client.models.Todo.list(); + return todos.data.length; +}; diff --git a/packages/integration-tests/src/test-projects/data_access_from_function/amplify/functions/todo-count/resource.ts b/packages/integration-tests/src/test-projects/data_access_from_function/amplify/functions/todo-count/resource.ts new file mode 100644 index 00000000000..6fa45f4b81f --- /dev/null +++ b/packages/integration-tests/src/test-projects/data_access_from_function/amplify/functions/todo-count/resource.ts @@ -0,0 +1,7 @@ +import { defineFunction } from '@aws-amplify/backend'; + +export const todoCount = defineFunction({ + name: 'todo-count', + entry: './handler.ts', + timeoutSeconds: 30, +}); diff --git a/packages/integration-tests/src/test-projects/live-dependency-health-checks-projects/README.md b/packages/integration-tests/src/test-projects/live-dependency-health-checks-projects/README.md new file mode 100644 index 00000000000..03e80634532 --- /dev/null +++ b/packages/integration-tests/src/test-projects/live-dependency-health-checks-projects/README.md @@ -0,0 +1,5 @@ +Projects in this directory are meant for `live-dependency-health-checks` (aka canaries). + +1. These projects must not be used in e2e tests to provide deep functional coverage. +2. These projects must be lightweight to provide fast runtime and stability. +3. These projects must cover only P0 scenarios we care most. (That are not covered by "getting started" flow, aka `create-amplify`). diff --git a/packages/integration-tests/src/test-projects/live-dependency-health-checks-projects/function-code-hotswap/amplify/backend.ts b/packages/integration-tests/src/test-projects/live-dependency-health-checks-projects/function-code-hotswap/amplify/backend.ts new file mode 100644 index 00000000000..f4e88845bec --- /dev/null +++ b/packages/integration-tests/src/test-projects/live-dependency-health-checks-projects/function-code-hotswap/amplify/backend.ts @@ -0,0 +1,4 @@ +import { defineBackend } from '@aws-amplify/backend'; +import { nodeFunc } from './function.js'; + +defineBackend({ nodeFunc }); diff --git a/packages/integration-tests/src/test-projects/live-dependency-health-checks-projects/function-code-hotswap/amplify/func-src/handler.ts b/packages/integration-tests/src/test-projects/live-dependency-health-checks-projects/function-code-hotswap/amplify/func-src/handler.ts new file mode 100644 index 00000000000..7f011bcc588 --- /dev/null +++ b/packages/integration-tests/src/test-projects/live-dependency-health-checks-projects/function-code-hotswap/amplify/func-src/handler.ts @@ -0,0 +1,6 @@ +/** + * Dummy lambda handler. + */ +export const handler = async () => { + return 'Hello'; +}; diff --git a/packages/integration-tests/src/test-projects/live-dependency-health-checks-projects/function-code-hotswap/amplify/function.ts b/packages/integration-tests/src/test-projects/live-dependency-health-checks-projects/function-code-hotswap/amplify/function.ts new file mode 100644 index 00000000000..d05b942f896 --- /dev/null +++ b/packages/integration-tests/src/test-projects/live-dependency-health-checks-projects/function-code-hotswap/amplify/function.ts @@ -0,0 +1,7 @@ +import { defineFunction } from '@aws-amplify/backend'; + +export const nodeFunc = defineFunction({ + name: 'nodeFunction', + entry: './func-src/handler.ts', + timeoutSeconds: 5, +}); diff --git a/packages/integration-tests/src/test-projects/live-dependency-health-checks-projects/function-code-hotswap/hotswap-update-files/func-src/handler.ts b/packages/integration-tests/src/test-projects/live-dependency-health-checks-projects/function-code-hotswap/hotswap-update-files/func-src/handler.ts new file mode 100644 index 00000000000..2acb92f1a19 --- /dev/null +++ b/packages/integration-tests/src/test-projects/live-dependency-health-checks-projects/function-code-hotswap/hotswap-update-files/func-src/handler.ts @@ -0,0 +1,6 @@ +/** + * Non-functional change to the lambda, but it triggers a sandbox hotswap + */ +export const handler = async () => { + return 'Hello V2'; +}; 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 00000000000..bb04424328f --- /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 00000000000..8aac23b5436 --- /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 00000000000..48a0db7cb61 --- /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 00000000000..dd1d930b074 --- /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 00000000000..d6842ab5d0f --- /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 00000000000..9404344b2bf --- /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 00000000000..f58ac10994f --- /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 56dd0a8f932..fbd1823cf7f 100644 --- a/packages/model-generator/CHANGELOG.md +++ b/packages/model-generator/CHANGELOG.md @@ -1,5 +1,62 @@ # @aws-amplify/model-generator +## 1.0.12 + +### Patch Changes + +- a7506f9: wraps no outputs found error from backend output client +- Updated dependencies [a7506f9] +- Updated dependencies [a7506f9] + - @aws-amplify/platform-core@1.5.0 + - @aws-amplify/plugin-types@1.7.0 + +## 1.0.11 + +### Patch Changes + +- 107600b: Updated error handling with S3 Client + +## 1.0.10 + +### Patch Changes + +- 3cf0738: update detection of BackendOutputClientErrors +- Updated dependencies [95942c5] +- Updated dependencies [3cf0738] +- Updated dependencies [f679cf6] +- Updated dependencies [f193105] + - @aws-amplify/platform-core@1.4.0 + - @aws-amplify/deployed-backend-client@1.5.0 + +## 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 2f6efcbd503..f5bf48036b0 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.12", "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.5.0", + "@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/platform-core": "^1.5.0", + "@aws-amplify/plugin-types": "^1.7.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.test.ts b/packages/model-generator/src/create_graphql_document_generator.test.ts index d3071f0d027..5df2c3a43c3 100644 --- a/packages/model-generator/src/create_graphql_document_generator.test.ts +++ b/packages/model-generator/src/create_graphql_document_generator.test.ts @@ -100,6 +100,37 @@ void describe('model generator factory', () => { ); }); + void it('throws an error if outputs do not exist', async () => { + const fakeBackendOutputClient = { + getOutput: mock.fn(() => { + throw new BackendOutputClientError( + BackendOutputClientErrorType.NO_OUTPUTS_FOUND, + 'stack outputs are undefined' + ); + }), + }; + mock.method( + BackendOutputClientFactory, + 'getInstance', + () => fakeBackendOutputClient + ); + const generator = createGraphqlDocumentGenerator({ + backendIdentifier: { stackName: 'stackThatDoesNotHaveOutputs' }, + awsClientProvider, + }); + await assert.rejects( + () => generator.generateModels({ targetFormat: 'javascript' }), + (error: AmplifyUserError) => { + assert.strictEqual( + error.message, + 'Amplify outputs not found in stack metadata' + ); + assert.ok(error.resolution); + return true; + } + ); + }); + void it('throws an error if credentials are expired when getting backend outputs', async () => { const fakeBackendOutputClient = { getOutput: mock.fn(() => { diff --git a/packages/model-generator/src/create_graphql_document_generator.ts b/packages/model-generator/src/create_graphql_document_generator.ts index 2de7652a8f2..b2a6b3aef4d 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.test.ts b/packages/model-generator/src/create_graphql_models_generator.test.ts index 615ac7e655c..86aeb3deb25 100644 --- a/packages/model-generator/src/create_graphql_models_generator.test.ts +++ b/packages/model-generator/src/create_graphql_models_generator.test.ts @@ -105,6 +105,37 @@ void describe('models generator factory', () => { ); }); + void it('throws an error if stack outputs are undefined', async () => { + const fakeBackendOutputClient = { + getOutput: mock.fn(() => { + throw new BackendOutputClientError( + BackendOutputClientErrorType.NO_OUTPUTS_FOUND, + 'stack outputs are undefined' + ); + }), + }; + mock.method( + BackendOutputClientFactory, + 'getInstance', + () => fakeBackendOutputClient + ); + const generator = createGraphqlModelsGenerator({ + backendIdentifier: { stackName: 'stackThatDoesNotHaveOutputs' }, + awsClientProvider, + }); + await assert.rejects( + () => generator.generateModels({ target: 'javascript' }), + (error: AmplifyUserError) => { + assert.strictEqual( + error.message, + 'Amplify outputs not found in stack metadata' + ); + assert.ok(error.resolution); + return true; + } + ); + }); + void it('throws an error if credentials are expired when getting backend outputs', async () => { const fakeBackendOutputClient = { getOutput: mock.fn(() => { diff --git a/packages/model-generator/src/create_graphql_models_generator.ts b/packages/model-generator/src/create_graphql_models_generator.ts index b0f8d0cd085..c1a2920242a 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.test.ts b/packages/model-generator/src/create_graphql_types_generator.test.ts index 50981b498d1..ab889cff597 100644 --- a/packages/model-generator/src/create_graphql_types_generator.test.ts +++ b/packages/model-generator/src/create_graphql_types_generator.test.ts @@ -99,6 +99,37 @@ void describe('types generator factory', () => { ); }); + void it('throws an AmplifyUserError if stack outputs are undefined', async () => { + const fakeBackendOutputClient = { + getOutput: mock.fn(() => { + throw new BackendOutputClientError( + BackendOutputClientErrorType.NO_OUTPUTS_FOUND, + 'stack outputs are undefined' + ); + }), + }; + mock.method( + BackendOutputClientFactory, + 'getInstance', + () => fakeBackendOutputClient + ); + const generator = createGraphqlTypesGenerator({ + backendIdentifier: { stackName: 'stackThatDoesNotHaveOutputs' }, + awsClientProvider, + }); + await assert.rejects( + () => generator.generateTypes({ target: 'json' }), + (error: AmplifyUserError) => { + assert.strictEqual( + error.message, + 'Amplify outputs not found in stack metadata' + ); + assert.ok(error.resolution); + return true; + } + ); + }); + void it('throws an AmplifyUserError if credentials are expired when getting backend outputs', async () => { const fakeBackendOutputClient = { getOutput: mock.fn(() => { diff --git a/packages/model-generator/src/create_graphql_types_generator.ts b/packages/model-generator/src/create_graphql_types_generator.ts index a6c72479709..17f0e799e20 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 29ad2fa7664..43ca3864334 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 1c5e9feb846..6dba118eae8 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 @@ -16,67 +16,63 @@ export const getBackendOutputWithErrorHandling = async ( try { return await backendOutputClient.getOutput(backendIdentifier); } catch (error) { - if ( - error instanceof BackendOutputClientError && - error.code === BackendOutputClientErrorType.DEPLOYMENT_IN_PROGRESS - ) { - // eslint-disable-next-line amplify-backend-rules/no-amplify-errors - throw new AmplifyUserError( - 'DeploymentInProgressError', - { - message: 'Deployment is currently in progress.', - resolution: 'Re-run this command once the deployment completes.', - }, - error - ); + if (BackendOutputClientError.isBackendOutputClientError(error)) { + switch (error.code) { + case BackendOutputClientErrorType.DEPLOYMENT_IN_PROGRESS: + throw new AmplifyUserError( + 'DeploymentInProgressError', + { + message: 'Deployment is currently in progress.', + resolution: 'Re-run this command once the deployment completes.', + }, + error + ); + case BackendOutputClientErrorType.NO_STACK_FOUND: + throw new AmplifyUserError( + 'StackDoesNotExistError', + { + message: 'Stack does not exist.', + resolution: + 'Ensure the CloudFormation stack ID or Amplify App ID and branch specified are correct and exists, then re-run this command.', + }, + error + ); + case BackendOutputClientErrorType.NO_OUTPUTS_FOUND: + throw new AmplifyUserError( + 'AmplifyOutputsNotFoundError', + { + message: 'Amplify outputs not found in stack metadata', + resolution: `Ensure the CloudFormation stack ID or Amplify App ID and branch specified are correct and exists. + If this is a new sandbox or branch deployment, wait for the deployment to be successfully finished and try again.`, + }, + error + ); + case BackendOutputClientErrorType.CREDENTIALS_ERROR: + throw new AmplifyUserError( + 'CredentialsError', + { + message: + 'Unable to get backend outputs due to invalid credentials.', + resolution: + 'Ensure your AWS credentials are correctly set and refreshed.', + }, + error + ); + case BackendOutputClientErrorType.ACCESS_DENIED: + throw new AmplifyUserError( + 'AccessDeniedError', + { + message: + 'Unable to get backend outputs due to insufficient permissions.', + resolution: + 'Ensure you have permissions to call cloudformation:GetTemplateSummary.', + }, + error + ); + default: + throw error; + } } - if ( - error instanceof BackendOutputClientError && - error.code === BackendOutputClientErrorType.NO_STACK_FOUND - ) { - // eslint-disable-next-line amplify-backend-rules/no-amplify-errors - throw new AmplifyUserError( - 'StackDoesNotExistError', - { - message: 'Stack does not exist.', - resolution: - 'Ensure the CloudFormation stack ID or Amplify App ID and branch specified are correct and exists, then re-run this command.', - }, - error - ); - } - if ( - error instanceof BackendOutputClientError && - error.code === BackendOutputClientErrorType.CREDENTIALS_ERROR - ) { - // eslint-disable-next-line amplify-backend-rules/no-amplify-errors - throw new AmplifyUserError( - 'CredentialsError', - { - message: 'Unable to get backend outputs due to invalid credentials.', - resolution: - 'Ensure your AWS credentials are correctly set and refreshed.', - }, - error - ); - } - if ( - error instanceof BackendOutputClientError && - error.code === BackendOutputClientErrorType.ACCESS_DENIED - ) { - // eslint-disable-next-line amplify-backend-rules/no-amplify-errors - throw new AmplifyUserError( - 'AccessDeniedError', - { - message: - 'Unable to get backend outputs due to insufficient permissions.', - resolution: - 'Ensure you have permissions to call cloudformation:GetTemplateSummary.', - }, - error - ); - } - throw error; } }; diff --git a/packages/model-generator/src/graphql_document_generator.ts b/packages/model-generator/src/graphql_document_generator.ts index 82ee5c33c08..8ac6dbff5cc 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 534c2af8c02..d116e2b33f8 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 33c9285b5d9..38a9c7e6715 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 0b29d0e6c28..d3483be70eb 100644 --- a/packages/model-generator/src/s3_string_object_fetcher.ts +++ b/packages/model-generator/src/s3_string_object_fetcher.ts @@ -1,4 +1,5 @@ -import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'; +import { AmplifyFault } from '@aws-amplify/platform-core'; +import { GetObjectCommand, NoSuchBucket, S3Client } from '@aws-sdk/client-s3'; /** * Handles fetching an object from an s3 bucket and parsing the object contents to a string @@ -14,14 +15,30 @@ export class S3StringObjectFetcher { */ fetch = async (uri: string) => { const { bucket, key } = this.parseS3Uri(uri); - const getSchemaCommandResult = await this.s3Client.send( - new GetObjectCommand({ Bucket: bucket, Key: key }) - ); - const schema = await getSchemaCommandResult.Body?.transformToString(); - if (!schema) { - throw new Error('Error on parsing output schema'); + try { + const getSchemaCommandResult = await this.s3Client.send( + new GetObjectCommand({ Bucket: bucket, Key: key }) + ); + 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; + } catch (caught) { + if (caught instanceof NoSuchBucket) { + throw new AmplifyFault( + 'NoSuchBucketFault', + { + message: `${bucket} does not exist. \n + Try redeploying your changes again, if the error persists, create a bug report here: https://github.com/aws-amplify/amplify-backend/issues/new/choose`, + }, + caught + ); + } else { + throw caught; + } } - return schema; }; private parseS3Uri = (uri: string): { bucket: string; key: string } => { diff --git a/packages/platform-core/API.md b/packages/platform-core/API.md index 928d4ada67a..4ef316c3494 100644 --- a/packages/platform-core/API.md +++ b/packages/platform-core/API.md @@ -5,10 +5,24 @@ ```ts import { AppId } from '@aws-amplify/plugin-types'; +import { ApplicationLogLevel } from 'aws-cdk-lib/aws-lambda'; import { BackendIdentifier } from '@aws-amplify/plugin-types'; import { DeepPartialAmplifyGeneratedConfigs } from '@aws-amplify/plugin-types'; +import { Dependency } from '@aws-amplify/plugin-types'; +import { FieldLogLevel } from 'aws-cdk-lib/aws-appsync'; +import { LogLevel } from '@aws-amplify/plugin-types'; +import { LogRetention } from '@aws-amplify/plugin-types'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; import z from 'zod'; +declare namespace __export__cdk { + export { + LogLevelConverter, + LogRetentionConverter + } +} +export { __export__cdk } + // @public export abstract class AmplifyError extends Error { constructor(name: T, classification: AmplifyErrorClassification, options: AmplifyErrorOptions, cause?: Error | undefined); @@ -21,9 +35,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; // (undocumented) static fromStderr: (_stderr: string) => AmplifyError | undefined; + static isAmplifyError: (error: unknown) => error is AmplifyError; // (undocumented) readonly link?: string; // (undocumented) @@ -120,6 +135,25 @@ export class FilePathExtractor { // @public (undocumented) export type LocalConfigurationFileName = 'usage_data_preferences.json'; +// @public +class LogLevelConverter { + // (undocumented) + toCDKAppsyncFieldLogLevel: (logLevel: LogLevel | undefined) => FieldLogLevel | undefined; + // (undocumented) + toCDKLambdaApplicationLogLevel: (logLevel: LogLevel | undefined) => ApplicationLogLevel | undefined; +} + +// @public +class LogRetentionConverter { + // (undocumented) + toCDKRetentionDays: (retention: LogRetention | undefined) => RetentionDays | undefined; +} + +// @public +export class NamingConverter { + toScreamingSnakeCase(input: string): string; +} + // @public export class ObjectAccumulator { constructor(accumulator: DeepPartialAmplifyGeneratedConfigs, versionKey?: string); @@ -197,7 +231,7 @@ export type UsageDataEmitter = { // @public export class UsageDataEmitterFactory { - getInstance: (libraryVersion: string) => Promise; + getInstance: (libraryVersion: string, dependencies?: Array) => Promise; } // (No @packageDocumentation comment for this package) diff --git a/packages/platform-core/CHANGELOG.md b/packages/platform-core/CHANGELOG.md index 51c7f3c8f49..95f5ed46126 100644 --- a/packages/platform-core/CHANGELOG.md +++ b/packages/platform-core/CHANGELOG.md @@ -1,5 +1,67 @@ # @aws-amplify/platform-core +## 1.5.1 + +### Patch Changes + +- a712983: Base64 encode serialized Amplify Errors + +## 1.5.0 + +### Minor Changes + +- a7506f9: added data logging api to defineData + +### Patch Changes + +- a7506f9: add InsufficientMemorySpaceError wrapping +- Updated dependencies [a7506f9] + - @aws-amplify/plugin-types@1.7.0 + +## 1.4.0 + +### Minor Changes + +- f193105: Update getAmplifyDataClientConfig to work with named data backend + +### Patch Changes + +- 95942c5: expand wrapping of credentials related errors +- f679cf6: expand handling of getaddrinfo ENOTFOUND errors + +## 1.3.0 + +### Minor Changes + +- 65abf6a: Add options to control log settings + +### Patch Changes + +- cfdc854: return amplify user error as it is from `AmplifyError.fromError` +- Updated dependencies [72b2fe0] +- Updated dependencies [f6ba240] + - @aws-amplify/plugin-types@1.6.0 + +## 1.2.2 + +### Patch Changes + +- 249c0e5: Handle insufficient disk space errors + +## 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/api-extractor.json b/packages/platform-core/api-extractor.json index 0f56de03f66..cc2ebea8cf9 100644 --- a/packages/platform-core/api-extractor.json +++ b/packages/platform-core/api-extractor.json @@ -1,3 +1,4 @@ { - "extends": "../../api-extractor.base.json" + "extends": "../../api-extractor.base.json", + "mainEntryPointFilePath": "/lib/index.internal.d.ts" } diff --git a/packages/platform-core/package.json b/packages/platform-core/package.json index d62d258aa92..47b2b28d90d 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.5.1", "type": "commonjs", "publishConfig": { "access": "public" @@ -10,6 +10,11 @@ "types": "./lib/index.d.ts", "import": "./lib/index.js", "require": "./lib/index.js" + }, + "./cdk": { + "types": "./lib/cdk/index.d.ts", + "import": "./lib/cdk/index.js", + "require": "./lib/cdk/index.js" } }, "main": "lib/index.js", @@ -24,12 +29,17 @@ "@types/uuid": "9.0.7" }, "dependencies": { - "@aws-amplify/plugin-types": "^1.2.1", + "@aws-amplify/plugin-types": "^1.7.0", "@aws-sdk/client-sts": "^3.624.0", "is-ci": "^3.0.1", "lodash.mergewith": "^4.6.2", "semver": "^7.6.3", + "lodash.snakecase": "^4.1.1", "uuid": "^9.0.1", "zod": "^3.22.2" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.168.0", + "constructs": "^10.0.0" } } diff --git a/packages/platform-core/src/.eslintrc.json b/packages/platform-core/src/.eslintrc.json new file mode 100644 index 00000000000..0da8b97bbb6 --- /dev/null +++ b/packages/platform-core/src/.eslintrc.json @@ -0,0 +1,15 @@ +{ + "rules": { + "no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["aws-cdk-lib", "aws-cdk-lib/*", "constructs"], + "message": "Usage of CDK lib is not allowed in platform-core. Except /cdk entry point. This is to ensure that we don't load CDK eagerly from package root." + } + ] + } + ] + } +} diff --git a/packages/platform-core/src/cdk/.eslintrc.json b/packages/platform-core/src/cdk/.eslintrc.json new file mode 100644 index 00000000000..c3220a910c1 --- /dev/null +++ b/packages/platform-core/src/cdk/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-restricted-imports": "off" + } +} diff --git a/packages/platform-core/src/cdk/enum_converters.test.ts b/packages/platform-core/src/cdk/enum_converters.test.ts new file mode 100644 index 00000000000..bc260d28da8 --- /dev/null +++ b/packages/platform-core/src/cdk/enum_converters.test.ts @@ -0,0 +1,201 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { LogLevel, LogRetention } from '@aws-amplify/plugin-types'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { LogLevelConverter, LogRetentionConverter } from './enum_converters'; +import { ApplicationLogLevel } from 'aws-cdk-lib/aws-lambda'; +import { FieldLogLevel } from 'aws-cdk-lib/aws-appsync'; + +type TestCase = { + input: TSource | undefined; + expectedOutput: TTarget | undefined; +}; + +void describe('LogRetentionConverter', () => { + const testCases: Array> = [ + { + input: undefined, + expectedOutput: undefined, + }, + { + input: '1 day', + expectedOutput: RetentionDays.ONE_DAY, + }, + { + input: '3 days', + expectedOutput: RetentionDays.THREE_DAYS, + }, + { + input: '5 days', + expectedOutput: RetentionDays.FIVE_DAYS, + }, + { + input: '1 week', + expectedOutput: RetentionDays.ONE_WEEK, + }, + { + input: '2 weeks', + expectedOutput: RetentionDays.TWO_WEEKS, + }, + { + input: '1 month', + expectedOutput: RetentionDays.ONE_MONTH, + }, + { + input: '2 months', + expectedOutput: RetentionDays.TWO_MONTHS, + }, + { + input: '3 months', + expectedOutput: RetentionDays.THREE_MONTHS, + }, + { + input: '4 months', + expectedOutput: RetentionDays.FOUR_MONTHS, + }, + { + input: '5 months', + expectedOutput: RetentionDays.FIVE_MONTHS, + }, + { + input: '6 months', + expectedOutput: RetentionDays.SIX_MONTHS, + }, + { + input: '13 months', + expectedOutput: RetentionDays.THIRTEEN_MONTHS, + }, + { + input: '18 months', + expectedOutput: RetentionDays.EIGHTEEN_MONTHS, + }, + { + input: '1 year', + expectedOutput: RetentionDays.ONE_YEAR, + }, + { + input: '2 years', + expectedOutput: RetentionDays.TWO_YEARS, + }, + { + input: '3 years', + expectedOutput: RetentionDays.THREE_YEARS, + }, + { + input: '5 years', + expectedOutput: RetentionDays.FIVE_YEARS, + }, + { + input: '6 years', + expectedOutput: RetentionDays.SIX_YEARS, + }, + { + input: '7 years', + expectedOutput: RetentionDays.SEVEN_YEARS, + }, + { + input: '8 years', + expectedOutput: RetentionDays.EIGHT_YEARS, + }, + { + input: '9 years', + expectedOutput: RetentionDays.NINE_YEARS, + }, + { + input: '10 years', + expectedOutput: RetentionDays.TEN_YEARS, + }, + { + input: 'infinite', + expectedOutput: RetentionDays.INFINITE, + }, + ]; + + testCases.forEach((testCase, index) => { + void it(`converts log retention[${index}]`, () => { + const convertedValue = new LogRetentionConverter().toCDKRetentionDays( + testCase.input + ); + assert.strictEqual(convertedValue, testCase.expectedOutput); + }); + }); +}); + +void describe('Lambda ApplicationLogLevelConverter', () => { + const testCases: Array> = [ + { + input: undefined, + expectedOutput: undefined, + }, + { + input: 'info', + expectedOutput: ApplicationLogLevel.INFO, + }, + { + input: 'debug', + expectedOutput: ApplicationLogLevel.DEBUG, + }, + { + input: 'error', + expectedOutput: ApplicationLogLevel.ERROR, + }, + { + input: 'warn', + expectedOutput: ApplicationLogLevel.WARN, + }, + { + input: 'trace', + expectedOutput: ApplicationLogLevel.TRACE, + }, + { + input: 'fatal', + expectedOutput: ApplicationLogLevel.FATAL, + }, + ]; + + testCases.forEach((testCase, index) => { + void it(`converts log retention[${index}]`, () => { + const convertedValue = + new LogLevelConverter().toCDKLambdaApplicationLogLevel(testCase.input); + assert.strictEqual(convertedValue, testCase.expectedOutput); + }); + }); +}); + +void describe('Appsync FieldLogLevelConverter', () => { + const testCases: Array> = [ + { + input: undefined, + expectedOutput: undefined, + }, + { + input: 'none', + expectedOutput: FieldLogLevel.NONE, + }, + { + input: 'error', + expectedOutput: FieldLogLevel.ERROR, + }, + { + input: 'info', + expectedOutput: FieldLogLevel.INFO, + }, + { + input: 'debug', + expectedOutput: FieldLogLevel.DEBUG, + }, + { + input: 'all', + expectedOutput: FieldLogLevel.ALL, + }, + ]; + + testCases.forEach((testCase, index) => { + void it(`converts data log level[${index}]`, () => { + const convertedValue = new LogLevelConverter().toCDKAppsyncFieldLogLevel( + testCase.input + ); + assert.strictEqual(convertedValue, testCase.expectedOutput); + }); + }); +}); diff --git a/packages/platform-core/src/cdk/enum_converters.ts b/packages/platform-core/src/cdk/enum_converters.ts new file mode 100644 index 00000000000..96f9652f5b7 --- /dev/null +++ b/packages/platform-core/src/cdk/enum_converters.ts @@ -0,0 +1,121 @@ +import { LogLevel, LogRetention } from '@aws-amplify/plugin-types'; +import { ApplicationLogLevel } from 'aws-cdk-lib/aws-lambda'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { FieldLogLevel } from 'aws-cdk-lib/aws-appsync'; + +/** + * Converts LogRetention to CDK types. + */ +export class LogRetentionConverter { + toCDKRetentionDays = ( + retention: LogRetention | undefined + ): RetentionDays | undefined => { + switch (retention) { + case undefined: + return undefined; + + case '1 day': + return RetentionDays.ONE_DAY; + case '3 days': + return RetentionDays.THREE_DAYS; + case '5 days': + return RetentionDays.FIVE_DAYS; + case '1 week': + return RetentionDays.ONE_WEEK; + case '2 weeks': + return RetentionDays.TWO_WEEKS; + case '1 month': + return RetentionDays.ONE_MONTH; + case '2 months': + return RetentionDays.TWO_MONTHS; + case '3 months': + return RetentionDays.THREE_MONTHS; + case '4 months': + return RetentionDays.FOUR_MONTHS; + case '5 months': + return RetentionDays.FIVE_MONTHS; + case '6 months': + return RetentionDays.SIX_MONTHS; + case '1 year': + return RetentionDays.ONE_YEAR; + case '13 months': + return RetentionDays.THIRTEEN_MONTHS; + case '18 months': + return RetentionDays.EIGHTEEN_MONTHS; + case '2 years': + return RetentionDays.TWO_YEARS; + case '3 years': + return RetentionDays.THREE_YEARS; + case '5 years': + return RetentionDays.FIVE_YEARS; + case '6 years': + return RetentionDays.SIX_YEARS; + case '7 years': + return RetentionDays.SEVEN_YEARS; + case '8 years': + return RetentionDays.EIGHT_YEARS; + case '9 years': + return RetentionDays.NINE_YEARS; + case '10 years': + return RetentionDays.TEN_YEARS; + case 'infinite': + return RetentionDays.INFINITE; + } + }; +} + +/** + * Converts LogLevel to CDK types. + */ +export class LogLevelConverter { + toCDKLambdaApplicationLogLevel = ( + logLevel: LogLevel | undefined + ): ApplicationLogLevel | undefined => { + switch (logLevel) { + case undefined: { + return undefined; + } + case 'info': { + return ApplicationLogLevel.INFO; + } + case 'debug': { + return ApplicationLogLevel.DEBUG; + } + case 'warn': { + return ApplicationLogLevel.WARN; + } + case 'error': { + return ApplicationLogLevel.ERROR; + } + case 'fatal': { + return ApplicationLogLevel.FATAL; + } + case 'trace': { + return ApplicationLogLevel.TRACE; + } + default: + throw new Error(`Invalid Lambda application log level: ${logLevel}`); + } + }; + + toCDKAppsyncFieldLogLevel = ( + logLevel: LogLevel | undefined + ): FieldLogLevel | undefined => { + switch (logLevel) { + case undefined: + return undefined; + case 'none': + return FieldLogLevel.NONE; + case 'error': + return FieldLogLevel.ERROR; + case 'info': + return FieldLogLevel.INFO; + case 'debug': + return FieldLogLevel.DEBUG; + case 'all': + return FieldLogLevel.ALL; + default: + throw new Error(`Invalid Appsync field log level: ${logLevel}`); + } + }; +} diff --git a/packages/platform-core/src/cdk/index.ts b/packages/platform-core/src/cdk/index.ts new file mode 100644 index 00000000000..d5c9d704c97 --- /dev/null +++ b/packages/platform-core/src/cdk/index.ts @@ -0,0 +1,3 @@ +import { LogLevelConverter, LogRetentionConverter } from './enum_converters.js'; + +export { LogLevelConverter, LogRetentionConverter }; diff --git a/packages/platform-core/src/errors/amplify_error.test.ts b/packages/platform-core/src/errors/amplify_error.test.ts index 1f19ac057d5..295da2c5186 100644 --- a/packages/platform-core/src/errors/amplify_error.test.ts +++ b/packages/platform-core/src/errors/amplify_error.test.ts @@ -85,67 +85,113 @@ and some after the error message assert.deepStrictEqual(actual?.cause?.message, testError.cause?.message); }); - void it('deserialize when string is encoded with single quote and has double quotes in it', () => { - const sampleStderr = `some random stderr + void describe('V1 deserialization', () => { + void it('deserialize when string is encoded with single quote and has double quotes in it', () => { + const sampleStderr = `some random stderr ${util.inspect({ serializedError: '{"name":"SyntaxError","classification":"ERROR","options":{"message":"test error message","resolution":"test resolution"}}', })} and some after the error message `; - const actual = AmplifyError.fromStderr(sampleStderr); - assert.deepStrictEqual(actual?.name, 'SyntaxError'); - assert.deepStrictEqual(actual?.classification, 'ERROR'); - assert.deepStrictEqual(actual?.message, 'test error message'); - assert.deepStrictEqual(actual?.resolution, 'test resolution'); - }); + const actual = AmplifyError.fromStderr(sampleStderr); + assert.deepStrictEqual(actual?.name, 'SyntaxError'); + assert.deepStrictEqual(actual?.classification, 'ERROR'); + assert.deepStrictEqual(actual?.message, 'test error message'); + assert.deepStrictEqual(actual?.resolution, 'test resolution'); + }); - void it('deserialize when string is encoded with single quote and has double quotes escaped in between', () => { - const sampleStderr = `some random stderr + void it('deserialize when string is encoded with single quote and has double quotes escaped in between', () => { + const sampleStderr = `some random stderr ${util.inspect({ serializedError: '{"name":"SyntaxError","classification":"ERROR","options":{"message":"paths must start with \\"/\\" and end with \\"/*","resolution":"test resolution"}}', })} and some after the error message `; - const actual = AmplifyError.fromStderr(sampleStderr); - assert.deepStrictEqual(actual?.name, 'SyntaxError'); - assert.deepStrictEqual(actual?.classification, 'ERROR'); - assert.deepStrictEqual( - actual?.message, - 'paths must start with "/" and end with "/*' - ); - assert.deepStrictEqual(actual?.resolution, 'test resolution'); - }); + const actual = AmplifyError.fromStderr(sampleStderr); + assert.deepStrictEqual(actual?.name, 'SyntaxError'); + assert.deepStrictEqual(actual?.classification, 'ERROR'); + assert.deepStrictEqual( + actual?.message, + 'paths must start with "/" and end with "/*' + ); + assert.deepStrictEqual(actual?.resolution, 'test resolution'); + }); - void it('deserialize when string is encoded with double quote and has double quotes string in it', () => { - const sampleStderr = `some random stderr + void it('deserialize when string is encoded with double quote and has double quotes string in it', () => { + const sampleStderr = `some random stderr serializedError: "{\\"name\\":\\"SyntaxError\\",\\"classification\\":\\"ERROR\\",\\"options\\":{\\"message\\":\\"test error message\\",\\"resolution\\":\\"test resolution\\"}}" and some after the error message `; - const actual = AmplifyError.fromStderr(sampleStderr); - assert.deepStrictEqual(actual?.name, 'SyntaxError'); - assert.deepStrictEqual(actual?.classification, 'ERROR'); - assert.deepStrictEqual(actual?.message, 'test error message'); - assert.deepStrictEqual(actual?.resolution, 'test resolution'); - }); + const actual = AmplifyError.fromStderr(sampleStderr); + assert.deepStrictEqual(actual?.name, 'SyntaxError'); + assert.deepStrictEqual(actual?.classification, 'ERROR'); + assert.deepStrictEqual(actual?.message, 'test error message'); + assert.deepStrictEqual(actual?.resolution, 'test resolution'); + }); - void it('deserialize when string has single quotes in between', () => { - const sampleStderr = `some random stderr + void it('deserialize when string has single quotes in between', () => { + const sampleStderr = `some random stderr ${util.inspect({ serializedError: '{"name":"SyntaxError","classification":"ERROR","options":{"message":"Cannot read properties of undefined (reading \'data\')","resolution":"test resolution"}}', })} and some after the error message `; - const actual = AmplifyError.fromStderr(sampleStderr); - assert.deepStrictEqual(actual?.name, 'SyntaxError'); - assert.deepStrictEqual(actual?.classification, 'ERROR'); - assert.deepStrictEqual( - actual?.message, - `Cannot read properties of undefined (reading 'data')` - ); - assert.deepStrictEqual(actual?.resolution, 'test resolution'); + const actual = AmplifyError.fromStderr(sampleStderr); + assert.deepStrictEqual(actual?.name, 'SyntaxError'); + assert.deepStrictEqual(actual?.classification, 'ERROR'); + assert.deepStrictEqual( + actual?.message, + `Cannot read properties of undefined (reading 'data')` + ); + assert.deepStrictEqual(actual?.resolution, 'test resolution'); + }); + }); + + void describe('V2 deserialization', () => { + void it('deserialize when string is encoded with single quote', () => { + const sampleStderr = `some random stderr + serializedError: '${Buffer.from( + '{"name":"SyntaxError","classification":"ERROR","options":{"message":"test error message","resolution":"test resolution"}}' + ).toString('base64')}', +and some after the error message + `; + const actual = AmplifyError.fromStderr(sampleStderr); + assert.deepStrictEqual(actual?.name, 'SyntaxError'); + assert.deepStrictEqual(actual?.classification, 'ERROR'); + assert.deepStrictEqual(actual?.message, 'test error message'); + assert.deepStrictEqual(actual?.resolution, 'test resolution'); + }); + + void it('deserialize when string is encoded with double quote', () => { + const sampleStderr = `some random stderr + serializedError: "${Buffer.from( + '{"name":"SyntaxError","classification":"ERROR","options":{"message":"test error message","resolution":"test resolution"}}' + ).toString('base64')}", +and some after the error message + `; + const actual = AmplifyError.fromStderr(sampleStderr); + assert.deepStrictEqual(actual?.name, 'SyntaxError'); + assert.deepStrictEqual(actual?.classification, 'ERROR'); + assert.deepStrictEqual(actual?.message, 'test error message'); + assert.deepStrictEqual(actual?.resolution, 'test resolution'); + }); + + void it('deserialize when string is encoded with back ticks', () => { + const sampleStderr = `some random stderr + serializedError: \`${Buffer.from( + '{"name":"SyntaxError","classification":"ERROR","options":{"message":"test error message","resolution":"test resolution"}}' + ).toString('base64')}\`, +and some after the error message + `; + const actual = AmplifyError.fromStderr(sampleStderr); + assert.deepStrictEqual(actual?.name, 'SyntaxError'); + assert.deepStrictEqual(actual?.classification, 'ERROR'); + assert.deepStrictEqual(actual?.message, 'test error message'); + assert.deepStrictEqual(actual?.resolution, 'test resolution'); + }); }); }); @@ -165,7 +211,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 +221,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,7 +231,63 @@ 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 credentials related errors in AmplifyUserError', () => { + const error = new Error( + 'The security token included in the request is expired' + ); + [ + 'ExpiredToken', + 'ExpiredTokenException', + 'CredentialsProviderError', + 'InvalidClientTokenId', + 'CredentialsError', + ].forEach((name) => { + error.name = name; + const actual = AmplifyError.fromError(error); + assert.ok( + AmplifyError.isAmplifyError(actual) && + actual.name === 'CredentialsError', + `Failed the test while wrapping error ${name}` + ); + }); + }); + 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}` + ); + }); + }); + void it('return amplify user errors as it is', () => { + const error = new AmplifyUserError('DeploymentInProgressError', { + message: 'Deployment already in progress', + resolution: 'wait for it', + }); + const actual = AmplifyError.fromError(error); + assert.deepStrictEqual(error, actual); + assert.strictEqual(actual.resolution, error.resolution); + }); + void it('wraps InsufficientMemorySpaceError in AmplifyUserError', () => { + const error = new Error( + 'FATAL ERROR: Zone Allocation failed - process out of memory.' + ); + const actual = AmplifyError.fromError(error); + assert.ok( + AmplifyError.isAmplifyError(actual) && + actual.name === 'InsufficientMemorySpaceError', `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 e9c524d5726..981d18a1ccf 100644 --- a/packages/platform-core/src/errors/amplify_error.ts +++ b/packages/platform-core/src/errors/amplify_error.ts @@ -44,69 +44,71 @@ 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( - { - name, - classification, - options, - cause, - }, - errorSerializer - ); + this.serializedError = Buffer.from( + JSON.stringify( + { + name, + classification, + options, + cause, + }, + errorSerializer + ) + ).toString('base64'); } static fromStderr = (_stderr: string): AmplifyError | undefined => { - /** - * `["']?serializedError["']?:[ ]?` captures the start of the serialized error. The quotes depend on which OS is being used - * `(?:`(.+?)`|'(.+?)'|"((?:\\"|[^"])*?)")` captures the rest of the serialized string enclosed in either single quote, - * double quotes or back-ticks. - */ - const extractionRegex = - /["']?serializedError["']?:[ ]?(?:`(.+?)`|'(.+?)'|"((?:\\"|[^"])*?)")/; - const serialized = _stderr.match(extractionRegex); - if (serialized && serialized.length === 4) { - // 4 because 1 match and 3 capturing groups - try { - const serializedString = serialized - .slice(1) - .find((item) => item && item.length > 0) - ?.replaceAll('\\"', '"') - .replaceAll("\\'", "'"); + try { + const serializedString = tryFindSerializedErrorJSONString(_stderr); - if (!serializedString) { - return undefined; - } + if (!serializedString) { + return undefined; + } - const { name, classification, options, cause } = - JSON.parse(serializedString); + const { name, classification, options, cause } = + JSON.parse(serializedString); - let serializedCause = cause; - if (cause && ErrorSerializerDeserializer.isSerializedErrorType(cause)) { - serializedCause = ErrorSerializerDeserializer.deserialize(cause); - } - return classification === 'ERROR' - ? new AmplifyUserError(name, options, serializedCause) - : new AmplifyFault(name, options, serializedCause); - } catch (error) { - // cannot deserialize - return undefined; + let serializedCause = cause; + if (cause && ErrorSerializerDeserializer.isSerializedErrorType(cause)) { + serializedCause = ErrorSerializerDeserializer.deserialize(cause); } + return classification === 'ERROR' + ? new AmplifyUserError(name, options, serializedCause) + : new AmplifyFault(name, options, serializedCause); + } catch (error) { + // cannot deserialize + return undefined; } - return undefined; }; - static fromError = ( - error: unknown - ): AmplifyError< - | 'UnknownFault' - | 'CredentialsError' - | 'InvalidCommandInputError' - | 'DomainNotFoundError' - | 'SyntaxError' - > => { + /** + * 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 => { + if (AmplifyError.isAmplifyError(error)) { + return error; + } + const errorMessage = error instanceof Error ? `${error.name}: ${error.message}` @@ -159,6 +161,28 @@ 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 + ); + } + if (error instanceof Error && isOutOfMemoryError(error)) { + return new AmplifyUserError( + 'InsufficientMemorySpaceError', + { + message: error.message, + resolution: + 'There appears to be insufficient memory on your system to finish. Close other applications or restart your system and try again.', + }, + error + ); + } return new AmplifyFault( 'UnknownFault', { @@ -169,8 +193,79 @@ export abstract class AmplifyError extends Error { }; } +const tryFindSerializedErrorJSONString = ( + _stderr: string +): string | undefined => { + let errorJSONString = tryFindSerializedErrorJSONStringV2(_stderr); + if (!errorJSONString) { + errorJSONString = tryFindSerializedErrorJSONStringV1(_stderr); + } + return errorJSONString; +}; + +/** + * Tries to find serialized string assuming that it is in a form of serialized JSON encoded with base64. + */ +const tryFindSerializedErrorJSONStringV2 = ( + _stderr: string +): string | undefined => { + /** + * `["']?serializedError["']?:[ ]?` captures the start of the serialized error. The quotes depend on which OS is being used + * `(?:`([a-zA-Z0-9+/=]+?)`|'([a-zA-Z0-9+/=]+?)'|"([a-zA-Z0-9+/=]+?)")` captures the rest of the serialized string enclosed in either single quote, + * double quotes or back-ticks. + */ + const extractionRegex = + /["']?serializedError["']?:[ ]?(?:`([a-zA-Z0-9+/=]+?)`|'([a-zA-Z0-9+/=]+?)'|"([a-zA-Z0-9+/=]+?)")/; + const serialized = _stderr.match(extractionRegex); + if (serialized && serialized.length === 4) { + // 4 because 1 match and 3 capturing groups + const base64SerializedString = serialized + .slice(1) + .find((item) => item && item.length > 0); + if (base64SerializedString) { + return Buffer.from(base64SerializedString, 'base64').toString('utf-8'); + } + } + return undefined; +}; + +/** + * Tries to find serialized string assuming that it is in a form of serialized JSON. + * @deprecated This is old format left for backwards compatibility in case that synth-time components are using older version of platform-core. + */ +const tryFindSerializedErrorJSONStringV1 = ( + _stderr: string +): string | undefined => { + /** + * `["']?serializedError["']?:[ ]?` captures the start of the serialized error. The quotes depend on which OS is being used + * `(?:`(.+?)`|'(.+?)'|"((?:\\"|[^"])*?)")` captures the rest of the serialized string enclosed in either single quote, + * double quotes or back-ticks. + */ + const extractionRegex = + /["']?serializedError["']?:[ ]?(?:`(.+?)`|'(.+?)'|"((?:\\"|[^"])*?)")/; + const serialized = _stderr.match(extractionRegex); + if (serialized && serialized.length === 4) { + // 4 because 1 match and 3 capturing groups + return serialized + .slice(1) + .find((item) => item && item.length > 0) + ?.replaceAll('\\"', '"') + .replaceAll("\\'", "'"); + } + return undefined; +}; + const isCredentialsError = (err?: Error): boolean => { - return !!err && err?.name === 'CredentialsProviderError'; + return ( + !!err && + [ + 'ExpiredToken', + 'ExpiredTokenException', + 'CredentialsProviderError', + 'InvalidClientTokenId', + 'CredentialsError', + ].includes(err.name) + ); }; // These validation messages are taken from https://github.com/yargs/yargs/blob/0c95f9c79e1810cf9c8964fbf7d139009412f7e7/lib/validation.ts @@ -193,13 +288,26 @@ const isYargsValidationError = (err?: Error): boolean => { }; const isENotFoundError = (err?: Error): boolean => { - return !!err && err.message.startsWith('getaddrinfo ENOTFOUND'); + return !!err && err.message.includes('getaddrinfo ENOTFOUND'); }; 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) + ) + ); +}; + +const isOutOfMemoryError = (err?: Error): boolean => { + return !!err && err.message.includes('process out of memory'); +}; + /** * Amplify exception classifications */ diff --git a/packages/platform-core/src/index.internal.ts b/packages/platform-core/src/index.internal.ts new file mode 100644 index 00000000000..336a22d74e2 --- /dev/null +++ b/packages/platform-core/src/index.internal.ts @@ -0,0 +1,12 @@ +// Suppressing to allow special prefix __export__ that is recognized by API checks. +// eslint-disable-next-line @typescript-eslint/naming-convention +import * as __export__cdk from './cdk/index.js'; + +export * from './index.js'; + +/* + Api-extractor does not ([yet](https://github.com/microsoft/rushstack/issues/1596)) support multiple package entry points + Because this package has a submodule export, we are working around this issue by including that export here and directing api-extract to this entry point instead + This allows api-extractor to pick up the submodule exports in its analysis + */ +export { __export__cdk }; diff --git a/packages/platform-core/src/index.ts b/packages/platform-core/src/index.ts index 7e05a510ea4..97987eade81 100644 --- a/packages/platform-core/src/index.ts +++ b/packages/platform-core/src/index.ts @@ -11,3 +11,4 @@ export { CDKContextKey } from './cdk_context_key.js'; export * from './parameter_path_conversions.js'; export * from './object_accumulator.js'; export { TagName } from './tag_name.js'; +export * from './naming_convention_conversions.js'; diff --git a/packages/backend/src/engine/naming_convention_conversions.test.ts b/packages/platform-core/src/naming_convention_conversions.test.ts similarity index 80% rename from packages/backend/src/engine/naming_convention_conversions.test.ts rename to packages/platform-core/src/naming_convention_conversions.test.ts index d6e9e2d4964..f2092bc4f3c 100644 --- a/packages/backend/src/engine/naming_convention_conversions.test.ts +++ b/packages/platform-core/src/naming_convention_conversions.test.ts @@ -1,5 +1,5 @@ import { describe, it } from 'node:test'; -import { toScreamingSnakeCase } from './naming_convention_conversions.js'; +import { NamingConverter } from './naming_convention_conversions.js'; import assert from 'node:assert'; void describe('screaming snake conversions', () => { @@ -16,7 +16,10 @@ void describe('screaming snake conversions', () => { ]; testCases.forEach((testCase) => { void it(`should successfully convert ${testCase.input} to ${testCase.expected}`, () => { - assert.equal(toScreamingSnakeCase(testCase.input), testCase.expected); + assert.equal( + new NamingConverter().toScreamingSnakeCase(testCase.input), + testCase.expected + ); }); }); }); diff --git a/packages/platform-core/src/naming_convention_conversions.ts b/packages/platform-core/src/naming_convention_conversions.ts new file mode 100644 index 00000000000..7829db94962 --- /dev/null +++ b/packages/platform-core/src/naming_convention_conversions.ts @@ -0,0 +1,16 @@ +import snakeCase from 'lodash.snakecase'; + +/** + * Naming Converter + * @example + * new NamingConverter().toScreamingSnakeCase('myInputString') + */ +export class NamingConverter { + /** + * Converts input string to SCREAMING_SNAKE_CASE + * @param input Input string to convert + */ + public toScreamingSnakeCase(input: string): string { + return snakeCase(input).toUpperCase(); + } +} diff --git a/packages/platform-core/src/usage-data/get_usage_data_url.test.ts b/packages/platform-core/src/usage-data/get_usage_data_url.test.ts index 9c55985dcc2..e7c44a13b77 100644 --- a/packages/platform-core/src/usage-data/get_usage_data_url.test.ts +++ b/packages/platform-core/src/usage-data/get_usage_data_url.test.ts @@ -1,18 +1,26 @@ import { afterEach, describe, test } from 'node:test'; import assert from 'node:assert'; -import { getUrl } from './get_usage_data_url'; +import url from 'node:url'; void describe('getUrl', () => { afterEach(() => { delete process.env.AMPLIFY_BACKEND_USAGE_TRACKING_ENDPOINT; + delete require.cache[require.resolve('./get_usage_data_url')]; }); void test('that prod URL is returned when the env for beta URL is not set', () => { - assert(getUrl(), 'https://api.cli.amplify.aws/v1.0/metrics'); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { getUrl } = require('./get_usage_data_url'); + assert.equal( + url.format(getUrl()), + 'https://api.cli.amplify.aws/v1.0/metrics' + ); }); void test('that BETA URL is returned when the env for beta URL is set', () => { process.env.AMPLIFY_BACKEND_USAGE_TRACKING_ENDPOINT = 'https://aws.amazon.com/amplify/'; - assert(getUrl(), 'https://aws.amazon.com/amplify/'); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { getUrl } = require('./get_usage_data_url'); + assert.equal(url.format(getUrl()), 'https://aws.amazon.com/amplify/'); }); }); diff --git a/packages/platform-core/src/usage-data/usage_data.ts b/packages/platform-core/src/usage-data/usage_data.ts index 31190a5ffa2..41b7a8b995b 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; details?: 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 00542327021..189c9b57d71 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,10 +12,31 @@ 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; const testLibraryVersion = '1.2.3'; + const testDependencies = [ + { + name: 'aws-cdk', + version: '1.2.3', + }, + { + name: 'aws-cdk-lib', + version: '12.13.14', + }, + { + name: 'test-dep', + version: '1.2.4', + }, + { + name: 'some_other_dep', + version: '12.12.14', + }, + ]; const testURL = url.parse('https://aws.amazon.com/amplify/'); const onReqEndMock = mock.fn(); const onReqWriteMock = mock.fn(); @@ -39,6 +60,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,10 +101,27 @@ 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); assert.ok(usageDataSent.downstreamException == undefined); + assert.deepStrictEqual( + usageDataSent.projectSetting.details, + JSON.stringify([ + { + name: 'aws-cdk', + version: '1.2.3', + }, + { + name: 'aws-cdk-lib', + version: '12.13.14', + }, + ]) + ); }); void test('happy case, emitFailure generates and send correct usage data', async () => { @@ -105,6 +151,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'); @@ -136,6 +186,7 @@ void describe('UsageDataEmitter', () => { usageDataEmitter = new DefaultUsageDataEmitter( testLibraryVersion, + testDependencies, v4(), testURL, accountIdFetcherMock 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 d108dc9fc75..82616760aac 100644 --- a/packages/platform-core/src/usage-data/usage_data_emitter.ts +++ b/packages/platform-core/src/usage-data/usage_data_emitter.ts @@ -10,20 +10,29 @@ import isCI from 'is-ci'; import { SerializableError } from './serializable_error.js'; import { UsageDataEmitter } from './usage_data_emitter_factory.js'; import { AmplifyError } from '../index.js'; +import { Dependency } from '@aws-amplify/plugin-types'; /** * Entry point for sending usage data metrics */ export class DefaultUsageDataEmitter implements UsageDataEmitter { + private dependenciesToReport?: Array; /** * Constructor for UsageDataEmitter */ constructor( private readonly libraryVersion: string, + private readonly dependencies?: Array, private readonly sessionUuid = uuid(), private readonly url = getUrl(), private readonly accountIdFetcher = new AccountIdFetcher() - ) {} + ) { + const targetDependencies = ['aws-cdk', 'aws-cdk-lib']; + + this.dependenciesToReport = this.dependencies?.filter((dependency) => + targetDependencies.includes(dependency.name) + ); + } emitSuccess = async ( metrics?: Record, @@ -64,7 +73,7 @@ export class DefaultUsageDataEmitter implements UsageDataEmitter { metrics?: Record; dimensions?: Record; error?: AmplifyError; - }) => { + }): Promise => { return { accountId: await this.accountIdFetcher.fetch(), sessionUuid: this.sessionUuid, @@ -86,6 +95,10 @@ 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, + details: JSON.stringify(this.dependenciesToReport), + }, }; }; diff --git a/packages/platform-core/src/usage-data/usage_data_emitter_factory.test.ts b/packages/platform-core/src/usage-data/usage_data_emitter_factory.test.ts index a447ad3604e..443a0227e99 100644 --- a/packages/platform-core/src/usage-data/usage_data_emitter_factory.test.ts +++ b/packages/platform-core/src/usage-data/usage_data_emitter_factory.test.ts @@ -1,5 +1,5 @@ import assert from 'node:assert'; -import { beforeEach, describe, it, mock } from 'node:test'; +import { after, before, beforeEach, describe, it, mock } from 'node:test'; import { UsageDataEmitterFactory } from './usage_data_emitter_factory'; import { DefaultUsageDataEmitter } from './usage_data_emitter'; @@ -21,6 +21,19 @@ void describe('UsageDataEmitterFactory', () => { () => mockedConfigController ); + const originalAmplifyDisableTelemetry = + process.env['AMPLIFY_DISABLE_TELEMETRY']; + + before(() => { + // Unset AMPLIFY_DISABLE_TELEMETRY. We may be setting this variable in GitHub workflows. + delete process.env['AMPLIFY_DISABLE_TELEMETRY']; + }); + + after(() => { + // Restore original value after tests. + process.env['AMPLIFY_DISABLE_TELEMETRY'] = originalAmplifyDisableTelemetry; + }); + beforeEach(() => { configControllerGet.mock.resetCalls(); }); @@ -28,7 +41,8 @@ void describe('UsageDataEmitterFactory', () => { void it('returns DefaultUsageDataEmitter by default', async () => { configControllerGet.mock.mockImplementationOnce(() => undefined); const dataEmitter = await new UsageDataEmitterFactory().getInstance( - '0.0.0' + '0.0.0', + [] ); assert.strictEqual(configControllerGet.mock.callCount(), 1); assert.strictEqual(dataEmitter instanceof DefaultUsageDataEmitter, true); @@ -38,7 +52,8 @@ void describe('UsageDataEmitterFactory', () => { configControllerGet.mock.mockImplementationOnce(() => undefined); process.env['AMPLIFY_DISABLE_TELEMETRY'] = '1'; const dataEmitter = await new UsageDataEmitterFactory().getInstance( - '0.0.0' + '0.0.0', + [] ); assert.strictEqual(dataEmitter instanceof NoOpUsageDataEmitter, true); assert.strictEqual(configControllerGet.mock.callCount(), 1); @@ -48,7 +63,8 @@ void describe('UsageDataEmitterFactory', () => { void it('returns NoOpUsageDataEmitter if local config file exists and reads true', async () => { configControllerGet.mock.mockImplementationOnce(() => false); const dataEmitter = await new UsageDataEmitterFactory().getInstance( - '0.0.0' + '0.0.0', + [] ); assert.strictEqual(configControllerGet.mock.callCount(), 1); assert.strictEqual(dataEmitter instanceof NoOpUsageDataEmitter, true); diff --git a/packages/platform-core/src/usage-data/usage_data_emitter_factory.ts b/packages/platform-core/src/usage-data/usage_data_emitter_factory.ts index 6ee9a2fb110..c8134c0e097 100644 --- a/packages/platform-core/src/usage-data/usage_data_emitter_factory.ts +++ b/packages/platform-core/src/usage-data/usage_data_emitter_factory.ts @@ -3,6 +3,7 @@ import { NoOpUsageDataEmitter } from './noop_usage_data_emitter.js'; import { DefaultUsageDataEmitter } from './usage_data_emitter.js'; import { USAGE_DATA_TRACKING_ENABLED } from './constants.js'; import { AmplifyError } from '../index.js'; +import { Dependency } from '@aws-amplify/plugin-types'; export type UsageDataEmitter = { emitSuccess: ( @@ -22,7 +23,10 @@ export class UsageDataEmitterFactory { /** * Creates UsageDataEmitter for a given library version, usage data tracking preferences */ - getInstance = async (libraryVersion: string): Promise => { + getInstance = async ( + libraryVersion: string, + dependencies?: Array + ): Promise => { const configController = configControllerFactory.getInstance( 'usage_data_preferences.json' ); @@ -37,6 +41,6 @@ export class UsageDataEmitterFactory { ) { return new NoOpUsageDataEmitter(); } - return new DefaultUsageDataEmitter(libraryVersion); + return new DefaultUsageDataEmitter(libraryVersion, dependencies); }; } diff --git a/packages/plugin-types/API.md b/packages/plugin-types/API.md index 58b1302c884..b289307f054 100644 --- a/packages/plugin-types/API.md +++ b/packages/plugin-types/API.md @@ -4,6 +4,8 @@ ```ts +/// + import { CfnFunction } from 'aws-cdk-lib/aws-lambda'; import { CfnIdentityPool } from 'aws-cdk-lib/aws-cognito'; import { CfnIdentityPoolRoleAttachment } from 'aws-cdk-lib/aws-cognito'; @@ -12,20 +14,24 @@ import { CfnUserPoolClient } from 'aws-cdk-lib/aws-cognito'; import { CfnUserPoolGroup } from 'aws-cdk-lib/aws-cognito'; import { Client } from '@aws-sdk/types'; import { Construct } from 'constructs'; -import { ExecaChildProcess } from 'execa'; import { IFunction } from 'aws-cdk-lib/aws-lambda'; import { IRole } from 'aws-cdk-lib/aws-iam'; import { IUserPool } from 'aws-cdk-lib/aws-cognito'; import { IUserPoolClient } from 'aws-cdk-lib/aws-cognito'; import { MetadataBearer } from '@aws-sdk/types'; -import { Options } from 'execa'; import { Policy } from 'aws-cdk-lib/aws-iam'; +import { Readable } from 'node:stream'; import { SecretValue } from 'aws-cdk-lib'; 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 +49,7 @@ export type AuthResources = { userPoolClient: IUserPoolClient; authenticatedUserIamRole: IRole; unauthenticatedUserIamRole: IRole; + identityPoolId: string; cfnResources: AuthCfnResources; groups: { [groupName: string]: { @@ -145,9 +152,35 @@ export type DeepPartialAmplifyGeneratedConfigs = { [P in keyof T]?: P extends 'auth' | 'data' | 'storage' ? T[P] extends object ? DeepPartialAmplifyGeneratedConfigs : Partial : T[P]; }; +// @public (undocumented) +export type Dependency = { + name: string; + version: string; +}; + // @public export type DeploymentType = 'branch' | 'sandbox'; +// @public (undocumented) +export type ExecaChildProcess = { + stdout: Readable | null; + stderr: Readable | null; +} & Promise; + +// @public (undocumented) +export type ExecaChildProcessResult = { + exitCode?: number | undefined; +}; + +// @public (undocumented) +export type ExecaOptions = { + stdin?: 'inherit'; + stdout?: 'pipe'; + stderr?: 'pipe'; + extendEnv?: boolean; + env?: Record; +}; + // @public (undocumented) export type FunctionResources = { lambda: IFunction; @@ -169,6 +202,12 @@ export type ImportPathVerifier = { verify: (importStack: string | undefined, expectedImportingFile: string, errorMessage: string) => void; }; +// @public (undocumented) +export type LogLevel = 'all' | 'debug' | 'error' | 'fatal' | 'info' | 'none' | 'trace' | 'warn'; + +// @public (undocumented) +export type LogRetention = '1 day' | '3 days' | '5 days' | '1 week' | '2 weeks' | '1 month' | '2 months' | '3 months' | '4 months' | '5 months' | '6 months' | '1 year' | '13 months' | '18 months' | '2 years' | '3 years' | '5 years' | '6 years' | '7 years' | '8 years' | '9 years' | '10 years' | 'infinite'; + // @public export type MainStackCreator = { getOrCreateMainStack: () => Stack; @@ -184,14 +223,29 @@ export type PackageManagerController = { initializeProject: () => Promise; initializeTsConfig: (targetDir: string) => Promise; installDependencies: (packageNames: string[], type: 'dev' | 'prod') => Promise; - runWithPackageManager: (args: string[] | undefined, dir: string, options?: Options<'utf8'>) => ExecaChildProcess; + runWithPackageManager: (args: string[] | undefined, dir: string, options?: ExecaOptions) => ExecaChildProcess; getCommand: (args: string[]) => string; allowsSignalPropagation: () => boolean; + tryGetDependencies: () => Promise | undefined>; }; // @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 +292,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 1fe3d4c0272..2ff01de154c 100644 --- a/packages/plugin-types/CHANGELOG.md +++ b/packages/plugin-types/CHANGELOG.md @@ -1,5 +1,51 @@ # @aws-amplify/plugin-types +## 1.7.0 + +### Minor Changes + +- a7506f9: added data logging api to defineData + +## 1.6.0 + +### Minor Changes + +- f6ba240: Upgrade execa + +### Patch Changes + +- 72b2fe0: update aws-cdk lib to ^2.168.0 + +## 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 81a4624688c..fd424073e42 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.7.0", "types": "lib/index.d.ts", "type": "commonjs", "publishConfig": { @@ -11,14 +11,11 @@ }, "license": "Apache-2.0", "peerDependencies": { - "aws-cdk-lib": "^2.152.0", + "aws-cdk-lib": "^2.168.0", "constructs": "^10.0.0", "@aws-sdk/types": "^3.609.0" }, "imports": { "#package.json": "./package.json" - }, - "devDependencies": { - "execa": "^5.1.1" } } 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 00000000000..45bfbabcd93 --- /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 3b571a497c6..0112e06a317 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 3b8c7bf18e8..8d738f7b2ff 100644 --- a/packages/plugin-types/src/index.ts +++ b/packages/plugin-types/src/index.ts @@ -20,3 +20,7 @@ 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'; +export * from './log_level.js'; +export * from './log_retention.js'; diff --git a/packages/plugin-types/src/log_level.ts b/packages/plugin-types/src/log_level.ts new file mode 100644 index 00000000000..398bfb9be95 --- /dev/null +++ b/packages/plugin-types/src/log_level.ts @@ -0,0 +1,9 @@ +export type LogLevel = + | 'all' + | 'debug' + | 'error' + | 'fatal' + | 'info' + | 'none' + | 'trace' + | 'warn'; diff --git a/packages/plugin-types/src/log_retention.ts b/packages/plugin-types/src/log_retention.ts new file mode 100644 index 00000000000..60210c721ba --- /dev/null +++ b/packages/plugin-types/src/log_retention.ts @@ -0,0 +1,24 @@ +export type LogRetention = + | '1 day' + | '3 days' + | '5 days' + | '1 week' + | '2 weeks' + | '1 month' + | '2 months' + | '3 months' + | '4 months' + | '5 months' + | '6 months' + | '1 year' + | '13 months' + | '18 months' + | '2 years' + | '3 years' + | '5 years' + | '6 years' + | '7 years' + | '8 years' + | '9 years' + | '10 years' + | 'infinite'; diff --git a/packages/plugin-types/src/package_manager_controller.ts b/packages/plugin-types/src/package_manager_controller.ts index 67189f185fb..d583960c434 100644 --- a/packages/plugin-types/src/package_manager_controller.ts +++ b/packages/plugin-types/src/package_manager_controller.ts @@ -1,10 +1,29 @@ -/** - * TODO: use the latest execa. - * Issue: https://github.com/aws-amplify/amplify-backend/issues/962 - * execa v8 doesn't support commonjs, so we need to use the types from v5 +import { Readable } from 'node:stream'; + +/* + * Execa v6 and onwards doesn't support commonjs, so we need to define our own types + * to match execa functionalities we use. * https://github.com/sindresorhus/execa/issues/489#issuecomment-1109983390 */ -import { type ExecaChildProcess, type Options } from 'execa'; + +export type ExecaOptions = { + stdin?: 'inherit'; + stdout?: 'pipe'; + stderr?: 'pipe'; + extendEnv?: boolean; + env?: Record; +}; + +export type ExecaChildProcessResult = { + exitCode?: number | undefined; +}; + +export type ExecaChildProcess = { + stdout: Readable | null; + stderr: Readable | null; +} & Promise; + +export type Dependency = { name: string; version: string }; export type PackageManagerController = { initializeProject: () => Promise; @@ -16,8 +35,9 @@ export type PackageManagerController = { runWithPackageManager: ( args: string[] | undefined, dir: string, - options?: Options<'utf8'> + options?: ExecaOptions ) => ExecaChildProcess; getCommand: (args: string[]) => string; allowsSignalPropagation: () => boolean; + tryGetDependencies: () => Promise | undefined>; }; diff --git a/packages/plugin-types/src/stack_provider.ts b/packages/plugin-types/src/stack_provider.ts new file mode 100644 index 00000000000..1c88482f7b2 --- /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 a129089dab3..90271901a69 100644 --- a/packages/sandbox/CHANGELOG.md +++ b/packages/sandbox/CHANGELOG.md @@ -1,5 +1,117 @@ # @aws-amplify/sandbox +## 1.2.9 + +### Patch Changes + +- a7506f9: wraps no outputs found error from backend output client +- Updated dependencies [a7506f9] +- Updated dependencies [a7506f9] +- Updated dependencies [a7506f9] +- Updated dependencies [a7506f9] + - @aws-amplify/client-config@1.5.5 + - @aws-amplify/backend-deployer@1.1.13 + - @aws-amplify/platform-core@1.5.0 + - @aws-amplify/plugin-types@1.7.0 + +## 1.2.8 + +### Patch Changes + +- 3cf0738: do not stream function logs if stack does not exist +- Updated dependencies [dedcc27] +- Updated dependencies [95942c5] +- Updated dependencies [1eced2c] +- Updated dependencies [3cf0738] +- Updated dependencies [f679cf6] +- Updated dependencies [f193105] + - @aws-amplify/backend-deployer@1.1.12 + - @aws-amplify/platform-core@1.4.0 + - @aws-amplify/deployed-backend-client@1.5.0 + - @aws-amplify/client-config@1.5.4 + +## 1.2.7 + +### Patch Changes + +- 72b2fe0: update aws-cdk lib to ^2.168.0 +- Updated dependencies [1593ce8] +- Updated dependencies [a406263] +- Updated dependencies [37d8564] +- Updated dependencies [cfdc854] +- Updated dependencies [d66ab17] +- Updated dependencies [5a47d21] +- Updated dependencies [72b2fe0] +- Updated dependencies [65abf6a] +- Updated dependencies [0a360fb] +- Updated dependencies [6015595] +- Updated dependencies [daaedb6] +- Updated dependencies [0cf5c26] +- Updated dependencies [f6ba240] + - @aws-amplify/backend-deployer@1.1.11 + - @aws-amplify/platform-core@1.3.0 + - @aws-amplify/client-config@1.5.3 + - @aws-amplify/plugin-types@1.6.0 + - @aws-amplify/cli-core@1.2.1 + +## 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 36bfaf4c412..b04e734283f 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.9", "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.13", + "@aws-amplify/backend-secret": "^1.1.2", + "@aws-amplify/cli-core": "^1.2.1", + "@aws-amplify/client-config": "^1.5.5", + "@aws-amplify/deployed-backend-client": "^1.5.0", + "@aws-amplify/platform-core": "^1.5.0", + "@aws-amplify/plugin-types": "^1.7.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.168.0" } } diff --git a/packages/sandbox/src/file_watching_sandbox.test.ts b/packages/sandbox/src/file_watching_sandbox.test.ts index 390d917426a..68e410a56f3 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 39891c1347c..aa1bb1c6a1e 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 7ba897fea28..a6e24b1a4f8 100644 --- a/packages/sandbox/src/lambda_function_log_streamer.test.ts +++ b/packages/sandbox/src/lambda_function_log_streamer.test.ts @@ -1,23 +1,22 @@ import { beforeEach, describe, it, mock } from 'node:test'; 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'; + BackendOutputClient, + BackendOutputClientError, + BackendOutputClientErrorType, +} from '@aws-amplify/deployed-backend-client'; + 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 +24,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 +37,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 +74,12 @@ void describe('LambdaFunctionLogStreamer', () => { }, version: '1', }, + ['AWS::Amplify::AI::Conversation']: { + payload: { + definedConversationHandlers: definedConversationHandlers, + }, + version: '1', + }, } as BackendOutput); }), }; @@ -91,14 +97,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 +151,68 @@ 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('return early if backend output client throws with stack does not exist', async () => { + backendOutputClientMock.getOutput.mock.mockImplementationOnce(() => { + return Promise.reject( + new BackendOutputClientError( + BackendOutputClientErrorType.NO_STACK_FOUND, + 'Stack with id test-stack does not exist' + ) + ); + }); + await classUnderTest.startStreamingLogs(testSandboxBackendId, { + enabled: true, + }); + + // No lambda calls to retrieve tags + assert.strictEqual(lambdaClientSendMock.mock.callCount(), 0); + }); + + void it('return early if backend output client throws with outputs do not exist', async () => { + backendOutputClientMock.getOutput.mock.mockImplementationOnce(() => { + return Promise.reject( + new BackendOutputClientError( + BackendOutputClientErrorType.NO_OUTPUTS_FOUND, + 'Stack outputs are undefined' + ) + ); + }); + await classUnderTest.startStreamingLogs(testSandboxBackendId, { + enabled: true, + }); + + // No lambda calls to retrieve tags + assert.strictEqual(lambdaClientSendMock.mock.callCount(), 0); + }); + + 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.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 and was then called activate assert.strictEqual( cloudWatchLogMonitorMock.addLogGroups.mock.callCount(), - 2 + 4 ); assert.strictEqual( cloudWatchLogMonitorMock.addLogGroups.mock.calls[0].arguments[0], @@ -184,6 +230,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 +259,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 @@ -231,14 +301,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.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 for only filtered functions and was then called activate @@ -273,14 +351,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 74688495963..aac9f2a8334 100644 --- a/packages/sandbox/src/lambda_function_log_streamer.ts +++ b/packages/sandbox/src/lambda_function_log_streamer.ts @@ -1,17 +1,14 @@ import { LogLevel, Printer } from '@aws-amplify/cli-core'; -import { BackendOutputClient } from '@aws-amplify/deployed-backend-client'; import { - BackendIdentifierConversions, - TagName, -} from '@aws-amplify/platform-core'; + BackendOutputClient, + BackendOutputClientError, + BackendOutputClientErrorType, +} from '@aws-amplify/deployed-backend-client'; +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 +21,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 @@ -45,42 +41,59 @@ export class LambdaFunctionLogStreamer { return; } - const backendOutput: BackendOutput = - await this.backendOutputClient.getOutput(sandboxBackendId); + let backendOutput: BackendOutput = {}; + try { + backendOutput = await this.backendOutputClient.getOutput( + sandboxBackendId + ); + } catch (error) { + // If stack does not exist or hasn't deployed successfully, we do not want to go further to start streaming logs + if ( + BackendOutputClientError.isBackendOutputClientError(error) && + [ + BackendOutputClientErrorType.NO_STACK_FOUND, + BackendOutputClientErrorType.NO_OUTPUTS_FOUND, + ].some((code) => (error as BackendOutputClientError).code === code) + ) { + this.enabled = false; + return; + } + } 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 +122,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 +145,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 b4d872ab272..f1f36344e87 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 767c6a2687d..a8cefeccb36 100644 --- a/packages/schema-generator/CHANGELOG.md +++ b/packages/schema-generator/CHANGELOG.md @@ -1,5 +1,35 @@ # @aws-amplify/schema-generator +## 1.2.6 + +### Patch Changes + +- 72b2fe0: update aws-cdk lib to ^2.168.0 +- Updated dependencies [cfdc854] +- Updated dependencies [65abf6a] + - @aws-amplify/platform-core@1.3.0 + +## 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 160643bd6f8..7600a7f32ad 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.6", "type": "module", "publishConfig": { "access": "public" @@ -18,8 +18,8 @@ "update:api": "api-extractor run --local" }, "dependencies": { - "@aws-amplify/graphql-schema-generator": "^0.9.4", - "@aws-amplify/platform-core": "^1.0.5" + "@aws-amplify/graphql-schema-generator": "^0.11.0", + "@aws-amplify/platform-core": "^1.3.0" }, "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 e281560cddb..625cdb40771 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 bfe5e28ea8f..49fc857b862 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 e80682f4fdb..b574e8d23ca 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 0dbd34d7f9f..28cc61a3be2 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_dependencies.ts b/scripts/check_dependencies.ts index 80d26889d4b..52828f4419f 100644 --- a/scripts/check_dependencies.ts +++ b/scripts/check_dependencies.ts @@ -22,19 +22,5 @@ await new DependenciesValidator( }, }, [['aws-cdk', 'aws-cdk-lib']], - [ - { - // @aws-amplify/plugin-types can depend on execa@^5.1.1 as a workaround for https://github.com/aws-amplify/amplify-backend/issues/962 - // all other packages must depend on execa@^8.0.1 - // this can be removed once execa is patched - dependencyName: 'execa', - globalDependencyVersion: '^8.0.1', - exceptions: [ - { - packageName: '@aws-amplify/plugin-types', - dependencyVersion: '^5.1.1', - }, - ], - }, - ] + [] ).validate(); diff --git a/scripts/check_package_versions.ts b/scripts/check_package_versions.ts index a35822819af..149cf3fd724 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 536b7b41d28..8a2771bc164 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, @@ -20,6 +27,8 @@ import { import { CognitoIdentityProviderClient, DeleteUserPoolCommand, + DeleteUserPoolDomainCommand, + DescribeUserPoolCommand, ListUserPoolsCommand, ListUserPoolsCommandOutput, UserPoolDescriptionType, @@ -70,6 +79,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 +103,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 +113,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 +127,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; @@ -273,6 +298,19 @@ const staleUserPools = await listStaleCognitoUserPools(); for (const staleUserPool of staleUserPools) { if (staleUserPool.Name) { try { + const describeUserPoolResponse = await cognitoClient.send( + new DescribeUserPoolCommand({ + UserPoolId: staleUserPool.Id, + }) + ); + if (describeUserPoolResponse.UserPool?.Domain) { + await cognitoClient.send( + new DeleteUserPoolDomainCommand({ + UserPoolId: describeUserPoolResponse.UserPool.Id, + Domain: describeUserPoolResponse.UserPool?.Domain, + }) + ); + } await cognitoClient.send( new DeleteUserPoolCommand({ UserPoolId: staleUserPool.Id, @@ -546,3 +584,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 1e8e02f40a4..f6aa9e19c30 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 e024aeec5aa..9fc94d6031a 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 d48de411c74..de70ba1a81d 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 3e69d7c7bfa..ffb9b207831 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 ad6088f0987..d4e94526a90 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 6d8f581f00f..f01fbae9acb 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 24be0c1e89c..0b65dbb60d6 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 b84fb63b5a0..aec702b4f83 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 4de03c28f05..1243ed5af25 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 a232c4ba6f6..ba36d058be5 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/dependabot_version_update_handler.test.ts b/scripts/components/dependabot_version_update_handler.test.ts new file mode 100644 index 00000000000..929d00d95b1 --- /dev/null +++ b/scripts/components/dependabot_version_update_handler.test.ts @@ -0,0 +1,203 @@ +import { randomUUID } from 'crypto'; +import { $ as chainableExeca } from 'execa'; +import fsp from 'fs/promises'; +import { after, before, beforeEach, describe, it, mock } from 'node:test'; +import { EOL, tmpdir } from 'os'; +import path from 'path'; +import { GitClient } from './git_client.js'; +import { GithubClient } from './github_client.js'; +import { NpmClient } from './npm_client.js'; +import { + readPackageJson, + writePackageJson, +} from './package-json/package_json.js'; +import { DependabotVersionUpdateHandler } from './dependabot_version_update_handler.js'; +import assert from 'assert'; + +const originalEnv = process.env; + +/** + * This test suite is more of an integration test than a unit test. + * It uses the real file system and git repo but mocks the GitHub API client and GitHub context + */ +void describe('dependabot version update handler', async () => { + let testWorkingDir: string; + let gitClient: GitClient; + let npmClient: NpmClient; + + let cantaloupePackageName: string; + let cantaloupePackagePath: string; + let platypusPackageName: string; + let platypusPackagePath: string; + + let baseRef: string; + + const pullRequestBody = 'Bumps testDep from 1.0.0 to 1.1.0.'; + + before(async () => { + process.env.GITHUB_TOKEN = 'testToken'; + }); + + after(async () => { + process.env = originalEnv; + }); + + beforeEach(async ({ name: testName }) => { + // create temp dir + const shortId = randomUUID().split('-')[0]; + const testNameNormalized = testName.slice(0, 15).replaceAll(/\s/g, ''); + testWorkingDir = await fsp.mkdtemp( + path.join(tmpdir(), `${testNameNormalized}-${shortId}`) + ); + console.log(testWorkingDir); + + gitClient = new GitClient(testWorkingDir); + npmClient = new NpmClient(null, testWorkingDir); + + const $ = chainableExeca({ stdio: 'inherit', cwd: testWorkingDir }); + + // converting to lowercase because npm init creates packages with all lowercase + cantaloupePackageName = + `${testNameNormalized}-cantaloupe-${shortId}`.toLocaleLowerCase(); + platypusPackageName = + `${testNameNormalized}-platypus-${shortId}`.toLocaleLowerCase(); + + cantaloupePackagePath = path.join( + testWorkingDir, + 'packages', + cantaloupePackageName + ); + platypusPackagePath = path.join( + testWorkingDir, + 'packages', + platypusPackageName + ); + + await gitClient.init(); + await gitClient.switchToBranch('main'); + await npmClient.init(); + + await npmClient.initWorkspacePackage(cantaloupePackageName); + await setPackageToPublic(cantaloupePackagePath); + + await npmClient.initWorkspacePackage(platypusPackageName); + await setPackageToPublic(platypusPackagePath); + + await npmClient.install(['@changesets/cli']); + await setPackageDependencies(cantaloupePackagePath, { testDep: '^1.0.0' }); + await setPackageDependencies(platypusPackagePath, { testDep: '^1.0.0' }); + + await $`npx changeset init`; + await gitClient.commitAllChanges('Initial setup'); + baseRef = await gitClient.getHashForCurrentCommit(); + }); + + void it('can generate changeset with version updates', async () => { + const githubClient = new GithubClient('garbage'); + const labelPullRequestMocked = mock.method( + githubClient, + 'labelPullRequest', + async () => {} + ); + const gitPushMocked = mock.method(gitClient, 'push', async () => {}); + const ghContextMocked = { + eventName: '', + sha: '', + ref: '', + workflow: '', + action: '', + actor: '', + job: '', + runNumber: 0, + runId: 0, + apiUrl: '', + serverUrl: '', + graphqlUrl: '', + payload: { + pull_request: { + number: 1, + body: pullRequestBody, + head: { + ref: 'dependabot/test_version_update_branch', + // eslint-disable-next-line spellcheck/spell-checker + sha: 'abcd1234', // used for naming the changeset file + }, + }, + }, + issue: { + owner: '', + repo: '', + number: 0, + }, + repo: { + owner: '', + repo: '', + }, + }; + + // Update package.json files for both packages and commit to match what Dependabot will do for a version update PR + await gitClient.switchToBranch('dependabot/test_version_update_branch'); + await setPackageDependencies(cantaloupePackagePath, { testDep: '^1.1.0' }); + await setPackageDependencies(platypusPackagePath, { testDep: '^1.1.0' }); + await gitClient.commitAllChanges('Bump dependencies'); + + const dependabotVersionUpdateHandler = new DependabotVersionUpdateHandler( + baseRef, + gitClient, + githubClient, + testWorkingDir, + ghContextMocked + ); + + await dependabotVersionUpdateHandler.handleVersionUpdate(); + + const changesetFilePath = path.join( + testWorkingDir, + // eslint-disable-next-line spellcheck/spell-checker + '.changeset/dependabot-abcd1234.md' + ); + + await assertChangesetFile( + changesetFilePath, + [cantaloupePackageName, platypusPackageName], + pullRequestBody + ); + assert.deepEqual(labelPullRequestMocked.mock.calls[0].arguments, [ + 1, + ['run-e2e'], + ]); + assert.deepEqual(gitPushMocked.mock.callCount(), 1); + }); +}); + +const setPackageToPublic = async (packagePath: string) => { + const packageJson = await readPackageJson(packagePath); + packageJson.publishConfig = { + access: 'public', + }; + await writePackageJson(packagePath, packageJson); +}; + +const setPackageDependencies = async ( + packagePath: string, + dependencies: Record +) => { + const packageJson = await readPackageJson(packagePath); + packageJson.dependencies = dependencies; + await writePackageJson(packagePath, packageJson); +}; + +const assertChangesetFile = async ( + filePath: string, + packageNames: string[], + message: string +) => { + const changesetFileContent = await fsp.readFile(filePath, 'utf-8'); + const frontmatterContent = packageNames + .map((name) => `'${name}': patch`) + .join(EOL); + + const expectedContent = `---${EOL}${frontmatterContent}${EOL}---${EOL}${EOL}${message}${EOL}`; + + assert.deepEqual(changesetFileContent, expectedContent); +}; diff --git a/scripts/components/dependabot_version_update_handler.ts b/scripts/components/dependabot_version_update_handler.ts new file mode 100644 index 00000000000..08dd3477246 --- /dev/null +++ b/scripts/components/dependabot_version_update_handler.ts @@ -0,0 +1,135 @@ +import { context as ghContext } from '@actions/github'; +import { Context } from '@actions/github/lib/context.js'; +import fsp from 'fs/promises'; +import { EOL } from 'os'; +import { GitClient } from './git_client.js'; +import { GithubClient } from './github_client.js'; +import { readPackageJson } from './package-json/package_json.js'; +import path from 'path'; + +/** + * Handles the follow up processes of Dependabot opening a version update PR + */ +export class DependabotVersionUpdateHandler { + /** + * Initialize with version update config and necessary clients + */ + constructor( + private readonly baseRef: string, + private readonly gitClient: GitClient, + private readonly ghClient: GithubClient, + private readonly _rootDir: string = process.cwd(), + private readonly _ghContext: Context = ghContext + ) {} + + /** + * This method handles all of the follow up processes we would want run when Dependabot opens a version update PR. + * + * We would want a changeset file generated for Dependabot version update PRs in order for these version updates to be part of our release process. + * We also want to run our E2E tests on these version updates to ensure new dependency updates don't break us. + * + * Running this method when either of the following are true results in a no-op: + * - GitHub context event is not a pull request + * - Branch PR does not start with `dependabot/`, meaning the branch isn't for a Dependabot PR + * - PR already has a changeset in list of files changed + */ + handleVersionUpdate = async () => { + if (!this._ghContext.payload.pull_request) { + // event is not a pull request, return early + return; + } + + const branch = this._ghContext.payload.pull_request.head.ref; + await this.gitClient.switchToBranch(branch); + if (!branch.startsWith('dependabot/')) { + // if branch is not a dependabot branch, return early + return; + } + + const changedFiles = await this.gitClient.getChangedFiles(this.baseRef); + if (changedFiles.find((file) => file.startsWith('.changeset'))) { + // if changeset file already exists, return early + return; + } + + // Get all of the public packages with version updates (where 'package.json' is modified) + const modifiedPackageDirs = new Set(); + const packageJsonFiles = changedFiles.filter( + (changedFile) => + changedFile.startsWith('packages/') && + changedFile.endsWith('package.json') + ); + packageJsonFiles.forEach((changedPackageFile) => { + modifiedPackageDirs.add( + changedPackageFile.split('/').slice(0, 2).join('/') + ); + }); + + const packageNames = []; + for (const modifiedPackageDir of modifiedPackageDirs) { + const packageJson = await readPackageJson( + path.join(this._rootDir, modifiedPackageDir) + ); + if (!packageJson.private) { + packageNames.push(packageJson.name); + } + } + + // Create and commit the changeset file, then add the 'run-e2e' label and force push to the PR + const fileName = path.join( + this._rootDir, + `.changeset/dependabot-${this._ghContext.payload.pull_request.head.sha}.md` + ); + const versionUpdates = await this.getVersionUpdates(); + await this.createChangesetFile(fileName, versionUpdates, packageNames); + await this.gitClient.status(); + await this.gitClient.commitAllChanges('add changeset'); + await this.ghClient.labelPullRequest( + this._ghContext.payload.pull_request.number, + ['run-e2e'] + ); + await this.gitClient.push({ force: true }); + }; + + private createChangesetFile = async ( + fileName: string, + versionUpdates: string[], + packageNames: string[] + ) => { + let message = ''; + let content = ''; + + for (const update of versionUpdates) { + message += `${update}${EOL}`; + } + + const frontmatterContent = packageNames + .map((name) => `'${name}': patch`) + .join(EOL); + + if (packageNames.length === 0 || versionUpdates.length === 0) { + content = `---${EOL}---${EOL}`; + } else { + content = `---${EOL}${frontmatterContent}${EOL}---${EOL}${EOL}${message.trim()}${EOL}`; + } + await fsp.writeFile(fileName, content); + }; + + private getVersionUpdates = async () => { + const updates: string[] = []; + const prBody = this._ghContext.payload.pull_request?.body; + + // Match lines in PR body that are one of the following: + // Updates `` from to + // Bumps []() from to . + const matches = prBody?.match( + /(Updates|Bumps) (.*) from [0-9.]+ to [0-9.]+/g + ); + + for (const match of matches ?? []) { + updates.push(match); + } + + return updates; + }; +} diff --git a/scripts/components/git_client.ts b/scripts/components/git_client.ts index 3183660ec0a..5d90514298d 100644 --- a/scripts/components/git_client.ts +++ b/scripts/components/git_client.ts @@ -65,6 +65,14 @@ export class GitClient { return filenameDiffOutput.toString().split(EOL); }; + /** + * Get changes made in a specific modified file from getChangedFiles + */ + getFileChanges = async (file: string) => { + const { stdout: changes } = await this.exec`git show ${file}`; + return changes; + }; + /** * Switches to branchName. Creates the branch if it does not exist. */ @@ -189,6 +197,14 @@ export class GitClient { return previousReleaseTags; }; + /** + * Get commit hash at HEAD for the current branch + */ + getHashForCurrentCommit = async () => { + const { stdout: currentCommitHash } = await this.exec`git rev-parse HEAD`; + return currentCommitHash; + }; + private validateReleaseCommitHash = async (releaseCommitHash: string) => { // check that the hash points to a valid commit const { stdout: hashType } = await this diff --git a/scripts/components/github_client.ts b/scripts/components/github_client.ts index 56eb7e18e85..7c294c0ec1b 100644 --- a/scripts/components/github_client.ts +++ b/scripts/components/github_client.ts @@ -57,6 +57,14 @@ export class GithubClient { }); return response.data; }; + + labelPullRequest = async (pullRequestNumber: number, labels: string[]) => { + await this.ghClient.issues.addLabels({ + issue_number: pullRequestNumber, + labels, + ...ghContext.repo, + }); + }; } /** 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 00000000000..73a0a5af60e --- /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 00000000000..4daf7ce0f07 --- /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 00000000000..af6cf15235e --- /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 00000000000..af6cf15235e --- /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 00000000000..af6cf15235e --- /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/copy_template.ts b/scripts/copy_template.ts index 40cb9ccf907..edb1a45a082 100644 --- a/scripts/copy_template.ts +++ b/scripts/copy_template.ts @@ -24,14 +24,12 @@ if (!values?.name || !values?.template) { } const sourcePath = path.resolve( - new URL('.', import.meta.url).pathname, - '..', + new URL('.', import.meta.url).host, 'templates', values.template as string ); const destPath = path.resolve( - new URL('.', import.meta.url).pathname, - '..', + new URL('.', import.meta.url).host, 'packages', values.name as string ); diff --git a/scripts/dependabot_handle_version_update.ts b/scripts/dependabot_handle_version_update.ts new file mode 100644 index 00000000000..c24012d51af --- /dev/null +++ b/scripts/dependabot_handle_version_update.ts @@ -0,0 +1,23 @@ +import { GitClient } from './components/git_client.js'; +import { GithubClient } from './components/github_client.js'; +import { DependabotVersionUpdateHandler } from './components/dependabot_version_update_handler.js'; + +const baseRef = process.argv[2]; +if (baseRef === undefined) { + throw new Error( + 'No base ref specified for handle dependabot version update check' + ); +} + +const dependabotVersionUpdateHandler = new DependabotVersionUpdateHandler( + baseRef, + new GitClient(), + new GithubClient() +); + +try { + await dependabotVersionUpdateHandler.handleVersionUpdate(); +} catch (error) { + console.error(error); + process.exitCode = 1; +} diff --git a/scripts/generate_sparse_test_matrix.ts b/scripts/generate_sparse_test_matrix.ts new file mode 100644 index 00000000000..0f2c9ffb541 --- /dev/null +++ b/scripts/generate_sparse_test_matrix.ts @@ -0,0 +1,41 @@ +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 < 5) { + console.log( + "Usage: npx tsx scripts/generate_sparse_test_matrix.ts '' '' '' " + ); +} + +const testGlobPattern = process.argv[2]; +const nodeVersions = JSON.parse(process.argv[3]) as Array; +let os = JSON.parse(process.argv[4]) as Array; +const maxTestsPerJob = process.argv[5] ? parseInt(process.argv[5]) : 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' + ); +} + +os = os.map((entry) => { + if (entry === 'macos-14') { + // replace with large. + return 'macos-14-xlarge'; + } + return entry; +}); + +const matrix = await new SparseTestMatrixGenerator({ + testGlobPattern, + maxTestsPerJob, + dimensions: { + 'node-version': nodeVersions, + os, + }, +}).generate(); + +console.log(JSON.stringify(matrix)); diff --git a/scripts/get_unit_test_dir_list.ts b/scripts/get_unit_test_dir_list.ts index 50b4def5f17..0c962d47d12 100644 --- a/scripts/get_unit_test_dir_list.ts +++ b/scripts/get_unit_test_dir_list.ts @@ -6,4 +6,5 @@ result = result.filter((result) => !result.includes('integration-tests')); result.push( path.join('packages', 'integration-tests', 'lib', 'test-in-memory') ); + console.log(result.join(' ')); diff --git a/scripts/run_tests.ts b/scripts/run_tests.ts new file mode 100644 index 00000000000..47f0c02fdfa --- /dev/null +++ b/scripts/run_tests.ts @@ -0,0 +1,21 @@ +import { execa } from 'execa'; +import fs from 'fs'; +import semver from 'semver'; + +let testPaths = process.argv.slice(2); + +const nodeVersion = semver.parse(process.versions.node); +if (nodeVersion && nodeVersion.major >= 21) { + // Starting from version 21. Node test runner's cli changed how inputs to test CLI work. + // See https://github.com/nodejs/node/issues/50219. + testPaths = testPaths.map((path) => { + if (fs.existsSync(path) && fs.statSync(path).isDirectory()) { + return `${path}/**/*.test.?(c|m)js`; + } + return path; + }); +} + +await execa('tsx', ['--test', '--test-reporter', 'spec'].concat(testPaths), { + stdio: 'inherit', +}); diff --git a/scripts/stop_npm_proxy.ts b/scripts/stop_npm_proxy.ts index 070126177ea..dd8555313db 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 2a879e37685..3494b52996f 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" } }