diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e265aad28..f3010c681 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,9 +1,10 @@ -*Issue #, if available:* +📬 *Issue #, if available:* -*Description of changes:* +✍️ *Description of changes:* +🔏 *By submitting this pull request* -By submitting this pull request - -- [ ] I confirm that my contribution is made under the terms of the Apache 2.0 license. +- [ ] I confirm that I've ran `cargo +nightly fmt`. +- [ ] I confirm that I've ran `cargo clippy --fix`. - [ ] I confirm that I've made a best effort attempt to update all relevant documentation. +- [ ] I confirm that my contribution is made under the terms of the Apache 2.0 license. diff --git a/.github/actions/rust-build/action.yml b/.github/actions/rust-build/action.yml new file mode 100644 index 000000000..3eb857e9b --- /dev/null +++ b/.github/actions/rust-build/action.yml @@ -0,0 +1,33 @@ +name: "Rust builds" +description: "Builds, tests, and formats Rust code" +inputs: + package: + required: true + description: "the Rust package to test" + toolchain: + required: true + description: "the Rust toolchain to use" + run-tests: + required: true + default: true + description: "whether to run tests in addition to building" + +runs: + using: "composite" + steps: + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ inputs.toolchain }} + components: clippy, rustfmt + - uses: Swatinem/rust-cache@v2 + + - name: Build + shell: bash + run: cargo build --all-features --verbose --package ${{ inputs.package }} + + - name: Run tests + if: ${{ inputs.run-tests == 'true' }} + shell: bash + env: + RUSTFLAGS: ${{ inputs.package == 'lambda_runtime' && '--cfg tokio_unstable' || '' }} + run: cargo test --all-features --verbose --package ${{ inputs.package }} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..24a9fc5bf --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,30 @@ +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "." + schedule: + interval: "weekly" + - package-ecosystem: "cargo" + directory: "lambda-events" + schedule: + interval: "weekly" + - package-ecosystem: "cargo" + directory: "lambda-extension" + schedule: + interval: "weekly" + - package-ecosystem: "cargo" + directory: "lambda-http" + schedule: + interval: "weekly" + - package-ecosystem: "cargo" + directory: "lambda-integration-tests" + schedule: + interval: "weekly" + - package-ecosystem: "cargo" + directory: "lambda-runtime-api-client" + schedule: + interval: "weekly" + - package-ecosystem: "cargo" + directory: "lambda-runtime" + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/build-events.yml b/.github/workflows/build-events.yml new file mode 100644 index 000000000..8f0e6d1dc --- /dev/null +++ b/.github/workflows/build-events.yml @@ -0,0 +1,58 @@ +name: Check Lambda Events + +on: + push: + paths: + - "lambda-events/**" + - "Cargo.toml" + pull_request: + paths: + - "lambda-events/**" + - "Cargo.toml" + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + toolchain: + - "1.84.0" # Current MSRV + - stable + env: + RUST_BACKTRACE: 1 + steps: + - uses: actions/checkout@v6 + - name: Build events + uses: ./.github/actions/rust-build + with: + package: aws_lambda_events + toolchain: ${{ matrix.toolchain}} + check-event-features: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Test individual event features + run: make check-event-features + semver: + name: semver + needs: [build, check-event-features] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Check `aws_lambda_events` semver with only default features + uses: obi1kenobi/cargo-semver-checks-action@v2 + with: + rust-toolchain: stable + package: aws_lambda_events + feature-group: default-features + - name: Check `aws_lambda_events` semver with all features + uses: obi1kenobi/cargo-semver-checks-action@v2 + with: + rust-toolchain: stable + package: aws_lambda_events + feature-group: all-features diff --git a/.github/workflows/build-extension.yml b/.github/workflows/build-extension.yml new file mode 100644 index 000000000..90d5bf593 --- /dev/null +++ b/.github/workflows/build-extension.yml @@ -0,0 +1,61 @@ +name: Check Lambda Extension + +on: + push: + paths: + - 'lambda-runtime-api-client/**' + - 'lambda-extension/**' + - 'lambda-runtime/**' + - 'Cargo.toml' + + pull_request: + paths: + - 'lambda-runtime-api-client/**' + - 'lambda-extension/**' + - 'lambda-runtime/**' + - 'Cargo.toml' + + +permissions: + contents: read + +jobs: + build-runtime: + runs-on: ubuntu-latest + strategy: + matrix: + toolchain: + - "1.84.0" # Current MSRV + - stable + env: + RUST_BACKTRACE: 1 + steps: + - uses: actions/checkout@v6 + - name: Build Runtime API Client + uses: ./.github/actions/rust-build + with: + package: lambda_runtime_api_client + toolchain: ${{ matrix.toolchain}} + - name: Build Extensions runtime + uses: ./.github/actions/rust-build + with: + package: lambda-extension + toolchain: ${{ matrix.toolchain}} + semver: + name: semver + needs: build-runtime + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Check `lambda-extension` semver with only default features + uses: obi1kenobi/cargo-semver-checks-action@v2 + with: + rust-toolchain: stable + package: lambda-extension + feature-group: default-features + - name: Check `lambda-extension` semver with all features + uses: obi1kenobi/cargo-semver-checks-action@v2 + with: + rust-toolchain: stable + package: lambda-extension + feature-group: all-features diff --git a/.github/workflows/build-integration-test.yml b/.github/workflows/build-integration-test.yml new file mode 100644 index 000000000..4308c6d62 --- /dev/null +++ b/.github/workflows/build-integration-test.yml @@ -0,0 +1,43 @@ +name: Build integration tests + +on: + push: + paths: + - 'lambda-runtime-api-client/**' + - 'lambda-runtime/**' + - 'lambda-http/**' + - 'lambda-extension/**' + - 'Cargo.toml' + + pull_request: + paths: + - 'lambda-runtime-api-client/**' + - 'lambda-runtime/**' + - 'lambda-http/**' + - 'lambda-extension/**' + - 'Cargo.toml' + +permissions: + contents: read + +jobs: + build-runtime: + runs-on: ubuntu-latest + strategy: + matrix: + toolchain: + - "1.84.0" # Current MSRV + - stable + env: + RUST_BACKTRACE: 1 + steps: + - uses: actions/checkout@v6 + + - name: Build Integration tests + uses: ./.github/actions/rust-build + with: + package: lambda-integration-tests + toolchain: ${{ matrix.toolchain}} + # the tests will generally fail in ci since they make a network call to a real endpoint, + # this step is just designed to make sure they build successfully + run-tests: false diff --git a/.github/workflows/build-runtime.yml b/.github/workflows/build-runtime.yml new file mode 100644 index 000000000..16603df2f --- /dev/null +++ b/.github/workflows/build-runtime.yml @@ -0,0 +1,65 @@ +name: Check Lambda Runtime + +on: + push: + paths: + - 'lambda-runtime-api-client/**' + - 'lambda-runtime/**' + - 'lambda-http/**' + - 'Cargo.toml' + + pull_request: + paths: + - 'lambda-runtime-api-client/**' + - 'lambda-runtime/**' + - 'lambda-http/**' + - 'Cargo.toml' + +permissions: + contents: read + +jobs: + build-runtime: + runs-on: ubuntu-latest + strategy: + matrix: + toolchain: + - "1.84.0" # Current MSRV + - stable + env: + RUST_BACKTRACE: 1 + steps: + - uses: actions/checkout@v6 + - name: Build Runtime API Client + uses: ./.github/actions/rust-build + with: + package: lambda_runtime_api_client + toolchain: ${{ matrix.toolchain}} + - name: Build Functions runtime + uses: ./.github/actions/rust-build + with: + package: lambda_runtime + toolchain: ${{ matrix.toolchain}} + - name: Build HTTP layer + uses: ./.github/actions/rust-build + with: + package: lambda_http + toolchain: ${{ matrix.toolchain}} + semver: + name: semver + needs: build-runtime + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Check `lambda_runtime_api_client`, `lambda_runtime`, lambda_http` semver with only default features + uses: obi1kenobi/cargo-semver-checks-action@v2 + with: + rust-toolchain: stable + package: lambda_runtime_api_client, lambda_runtime, lambda_http + feature-group: default-features + - name: Check `lambda_runtime_api_client`, `lambda_runtime`, lambda_http` semver with all features + uses: obi1kenobi/cargo-semver-checks-action@v2 + with: + rust-toolchain: stable + package: lambda_runtime_api_client, lambda_runtime, lambda_http + feature-group: all-features diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index a33eca21f..000000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,98 +0,0 @@ -name: Rust - -on: [push, pull_request, workflow_dispatch] - -jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: - - ubuntu-latest - - macOS-latest - rust: - - "1.62.0" # Current MSRV - - stable - - beta - - nightly - target: - - "" - - x86_64-unknown-linux-musl - include: - - rust: nightly - allow_failure: true - env: - RUST_BACKTRACE: 1 - steps: - - uses: actions/checkout@v1 - - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ matrix.rust }} - override: true - - name: Build - run: cargo build --all --verbose - env: - TARGET: ${{ matrix.target }} - continue-on-error: ${{ matrix.allow_failure }} - - name: Run tests - run: cargo test --all --verbose - env: - TARGET: ${{ matrix.target }} - continue-on-error: ${{ matrix.allow_failure }} - fmt: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - components: rustfmt - override: true - - name: Run fmt check - run: cargo fmt --all -- --check - clippy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - components: clippy - override: true - - name: Run clippy check - run: cargo clippy - - check-examples: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - components: clippy - override: true - - name: Check examples - working-directory: examples - shell: bash - run: ./check-examples.sh - - # publish rustdoc to a gh-pages branch on pushes to main - # this can be helpful to those depending on the mainline branch - publish-docs: - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - needs: [build] - steps: - - name: Set up Rust - uses: hecrj/setup-rust-action@v1 - - uses: actions/checkout@v2 - - name: Generate Docs - run: | - cargo doc --no-deps - echo "" > target/doc/index.html - - name: Publish - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./target/doc diff --git a/.github/workflows/check-docs.yml b/.github/workflows/check-docs.yml new file mode 100644 index 000000000..252122b5b --- /dev/null +++ b/.github/workflows/check-docs.yml @@ -0,0 +1,41 @@ +name: Check rustdocs +# this is its own workflow since we to to use unstable +# to have the docs.rs display of feature flags + +on: + push: + paths: + - 'lambda-runtime/**' + - 'lambda-runtime-api-client/**' + - 'lambda-http/**' + - 'lambda-events/**' + - 'lambda-extension/**' + - 'Cargo.toml' + + pull_request: + paths: + - 'lambda-runtime/**' + - 'lambda-runtime-api-client/**' + - 'lambda-http/**' + - 'lambda-events/**' + - 'lambda-extension/**' + - 'Cargo.toml' + +permissions: + contents: read + +jobs: + build-runtime: + runs-on: ubuntu-latest + + env: + RUST_BACKTRACE: 1 + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@nightly + + - name: Check documentation + shell: bash + env: + RUSTDOCFLAGS: --cfg docsrs -Dwarnings + run: cargo doc --workspace --no-deps --document-private-items --all-features diff --git a/.github/workflows/check-examples.yml b/.github/workflows/check-examples.yml new file mode 100644 index 000000000..cec9ac1e6 --- /dev/null +++ b/.github/workflows/check-examples.yml @@ -0,0 +1,22 @@ +name: Check examples + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + + - name: Check examples + working-directory: examples + shell: bash + run: ./check-examples.sh \ No newline at end of file diff --git a/.github/workflows/closed-issue-message.yml b/.github/workflows/closed-issue-message.yml index f295d310e..6fbc007a5 100644 --- a/.github/workflows/closed-issue-message.yml +++ b/.github/workflows/closed-issue-message.yml @@ -1,17 +1,18 @@ -name: Closed Issue Message -on: - issues: - types: [closed] -jobs: - auto_comment: - runs-on: ubuntu-latest - steps: - - uses: aws-actions/closed-issue-message@v1 - with: - # These inputs are both required - repo-token: "${{ secrets.GITHUB_TOKEN }}" - message: | - ### ⚠️COMMENT VISIBILITY WARNING⚠️ - Comments on closed issues are hard for the maintainers of this repository to see. - If you need more assistance, please open a new issue that references this one. - If you wish to keep having a conversation with other community members under this issue feel free to do so. +name: Closed Issue Message +on: + issues: + types: [closed] +permissions: + issues: write + +jobs: + auto_comment: + runs-on: ubuntu-latest + steps: + - uses: aws-actions/closed-issue-message@v2 + with: + # These inputs are both required + repo-token: "${{ secrets.GITHUB_TOKEN }}" + message: | + This issue is now closed. Comments on closed issues are hard for our team to see. + If you need more assistance, please either tag a team member or open a new issue that references this one. diff --git a/.github/workflows/dockerized-test.yml b/.github/workflows/dockerized-test.yml new file mode 100644 index 000000000..be86e1470 --- /dev/null +++ b/.github/workflows/dockerized-test.yml @@ -0,0 +1,72 @@ +name: dockerized-test + +permissions: + contents: read + +on: + push: + branches: [ main ] + pull_request: + branches: [ '*' ] + workflow_dispatch: + + +jobs: + dockerized-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Build Lambda artifacts for testing + run: | + mkdir -p test/dockerized/tasks + HANDLERS_TO_BUILD="basic-lambda basic-sqs http-basic-lambda basic-lambda-concurrent" OUTPUT_DIR="$(pwd)/test/dockerized/tasks" ./scripts/build-examples.sh + ls -la test/dockerized/tasks/ + + - name: Build base test image with RIE and custom entrypoint + run: | + docker build . -t local/test-base -f Dockerfile.test + + - name: Run tests + uses: aws/containerized-test-runner-for-aws-lambda@main + with: + suiteFileArray: '["./test/dockerized/suites/*.json"]' + dockerImageName: 'local/test-base' + taskFolder: './test/dockerized/tasks' + + dockerized-test-concurrent: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + + - name: Build Lambda artifacts for testing + run: | + mkdir -p test/dockerized/tasks + HANDLERS_TO_BUILD="basic-lambda-concurrent invocation-id-concurrent" OUTPUT_DIR="$(pwd)/test/dockerized/tasks" ./scripts/build-examples.sh + ls -la test/dockerized/tasks/ + + - name: Build base test image with RIE and custom entrypoint + run: docker build -t local/test-base -f Dockerfile.test . + + - name: Create Docker network for concurrent tests + run: docker network create concurrent-test-net + + - name: Run concurrent scenarios + uses: aws/containerized-test-runner-for-aws-lambda@main + env: + CONTAINER_READY_DELAY_SECS: 5 + with: + suiteFileArray: '[]' + dockerImageName: 'local/test-base' + taskFolder: './test/dockerized/tasks' + scenarioDir: './test/dockerized/scenarios' + dockerSharedNetwork: 'concurrent-test-net' + + - name: Remove Docker network + if: always() + run: docker network rm concurrent-test-net || true diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 000000000..507b3dccf --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,34 @@ +name: Formatting and Linting + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + fmt: + name: Cargo fmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@nightly + with: + components: rustfmt + - uses: Swatinem/rust-cache@v2 + - name: Run fmt check + id: cargoFmt + shell: bash + run: cargo +nightly fmt --all -- --check + clippy: + name: Cargo clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Run clippy check + id: cargoClippy + shell: bash + run: cargo clippy --workspace --all-features -- -D warnings + + \ No newline at end of file diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml new file mode 100644 index 000000000..5bb826495 --- /dev/null +++ b/.github/workflows/release-plz.yml @@ -0,0 +1,54 @@ +name: Publish release + +on: + push: + branches: + - main + +jobs: + release-plz-release: + name: Release-plz release + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'aws' }} + permissions: + contents: write + id-token: write + steps: + - &checkout + name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - &install-rust + name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Authenticate to crates.io + uses: rust-lang/crates-io-auth-action@v1 + id: auth + - name: Run release-plz + uses: release-plz/action@v0.5 + with: + command: release + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} + + release-plz-pr: + name: Release-plz PR + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'aws' }} + permissions: + pull-requests: write + contents: write + concurrency: + group: release-plz-${{ github.ref }} + cancel-in-progress: false + steps: + - *checkout + - *install-rust + - name: Run release-plz + uses: release-plz/action@v0.5 + with: + command: release-pr + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }} diff --git a/.github/workflows/run-integration-test.yml b/.github/workflows/run-integration-test.yml new file mode 100644 index 000000000..d43ed9c71 --- /dev/null +++ b/.github/workflows/run-integration-test.yml @@ -0,0 +1,70 @@ +name: Run integration tests + +permissions: + id-token: write + contents: read + +on: + workflow_dispatch: + push: + +jobs: + run-integration-tests: + # Only run on the main repo, not forks + if: ${{ github.repository_owner == 'aws' }} + runs-on: ubuntu-latest + concurrency: + group: integration-test + cancel-in-progress: false + steps: + - name: install Cargo Lambda + uses: jaxxstorm/action-install-gh-release@v2.1.0 + with: + repo: cargo-lambda/cargo-lambda + platform: linux + arch: x86_64 + - name: install Zig toolchain + uses: mlugg/setup-zig@v2 + with: + version: 0.10.0 + - name: install SAM + uses: aws-actions/setup-sam@v2 + with: + use-installer: true + - uses: actions/checkout@v6 + - name: configure aws credentials + uses: aws-actions/configure-aws-credentials@v6.0.0 + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + role-session-name: ${{ secrets.ROLE_SESSION_NAME }} + aws-region: ${{ secrets.AWS_REGION }} + - name: build stack + run: cd lambda-integration-tests && sam build --beta-features + - name: validate stack + run: cd lambda-integration-tests && sam validate --lint + - name: deploy stack + id: deploy_stack + env: + AWS_REGION: ${{ secrets.AWS_REGION }} + run: | + cd lambda-integration-tests + stackName="aws-lambda-rust-integ-test-$GITHUB_RUN_ID" + echo "STACK_NAME=$stackName" >> "$GITHUB_OUTPUT" + echo "Stack name = $stackName" + sam deploy --stack-name "${stackName}" --parameter-overrides "ParameterKey=SecretToken,ParameterValue=${{ secrets.SECRET_TOKEN }}" "ParameterKey=LambdaRole,ParameterValue=${{ secrets.AWS_LAMBDA_ROLE }}" --no-confirm-changeset --no-progressbar > disable_output + TEST_ENDPOINT=$(sam list stack-outputs --stack-name "${stackName}" --output json | jq -r '.[] | select(.OutputKey=="HelloApiEndpoint") | .OutputValue') + TENANT_ID_TEST_FUNCTION=$(sam list stack-outputs --stack-name "${stackName}" --output json | jq -r '.[] | select(.OutputKey=="TenantIdTestFunction") | .OutputValue') + echo "TEST_ENDPOINT=$TEST_ENDPOINT" >> "$GITHUB_OUTPUT" + echo "TENANT_ID_TEST_FUNCTION=$TENANT_ID_TEST_FUNCTION" >> "$GITHUB_OUTPUT" + - name: run test + env: + SECRET_TOKEN: ${{ secrets.SECRET_TOKEN }} + TEST_ENDPOINT: ${{ steps.deploy_stack.outputs.TEST_ENDPOINT }} + TENANT_ID_TEST_FUNCTION: ${{ steps.deploy_stack.outputs.TENANT_ID_TEST_FUNCTION }} + run: cd lambda-integration-tests && cargo test + - name: cleanup + if: always() && steps.deploy_stack.outputs.STACK_NAME + env: + AWS_REGION: ${{ secrets.AWS_REGION }} + STACK_NAME: ${{ steps.deploy_stack.outputs.STACK_NAME }} + run: sam delete --stack-name "${STACK_NAME}" --no-prompts diff --git a/.github/workflows/test-rie.yml b/.github/workflows/test-rie.yml new file mode 100644 index 000000000..2f3dc9f69 --- /dev/null +++ b/.github/workflows/test-rie.yml @@ -0,0 +1,53 @@ +name: Test with RIE + +on: + pull_request: + branches: [ main ] + push: + branches: [ main ] + +permissions: + contents: read + +jobs: + test-rie: + runs-on: ubuntu-latest + strategy: + matrix: + example: [basic-lambda, basic-sqs] + + steps: + - uses: actions/checkout@v6 + + - name: Build and test ${{ matrix.example }} with RIE + run: | + docker build -f Dockerfile.rie --build-arg EXAMPLE=${{ matrix.example }} -t rust-lambda-rie-test-${{ matrix.example }} . + + # Start container in background + docker run -d -p 9000:8080 --name rie-test-${{ matrix.example }} rust-lambda-rie-test-${{ matrix.example }} + + # Wait for container to be ready + sleep 5 + + # Test the function based on example type + if [ "${{ matrix.example }}" = "basic-lambda" ]; then + PAYLOAD='{"command": "test from CI"}' + elif [ "${{ matrix.example }}" = "basic-sqs" ]; then + PAYLOAD='{"Records": [{"body": "{\"id\": \"123\", \"text\": \"hello from SQS\"}", "messageId": "test-id", "receiptHandle": "test-handle", "attributes": {}, "messageAttributes": {}, "md5OfBody": "test-md5", "eventSource": "aws:sqs", "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:test-queue", "awsRegion": "us-east-1"}]}' + fi + + # Make request and verify response + RESPONSE=$(curl -s -XPOST 'http://localhost:9000/2015-03-31/functions/function/invocations' \ + -d "$PAYLOAD" \ + -H 'Content-Type: application/json') + + echo "Response: $RESPONSE" + + # Basic validation that we got a response (not empty) + if [ -z "$RESPONSE" ]; then + echo "Error: Empty response" + exit 1 + fi + + # Stop container + docker stop rie-test-${{ matrix.example }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index d8feb2449..4fb609825 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,51 @@ -target +# Rust +target/ +**/*.rs.bk +*.pdb + +# Cargo /.cargo -lambda-runtime/libtest.rmeta -lambda-integration-tests/target Cargo.lock -# Built AWS Lambda zipfile -lambda.zip +# IDE +.vscode/ +.idea/ +*.iml +*.ipr +*.iws +.kiro/ + +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# Environment +*.env.local -# output.json from example docs +# AWS / SAM +.aws-sam/ +build/ output.json +lambda.zip + +# CDK +node_modules/ +cdk.out/ + +# Test artifacts +.test-runner/ +test/dockerized/tasks/ +Dockerfile.test-with-tasks +.test/ -.aws-sam -build -.vscode +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +.pytest_cache/ + +# Misc +lambda-runtime/libtest.rmeta +lambda-integration-tests/target/ diff --git a/.rustfmt.toml b/.rustfmt.toml index a40f0edc2..d2f482336 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,6 +1,14 @@ -edition = "2018" -# imports_granularity is unstable -# # https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#merge_imports -# imports_granularity = "Crate" +edition = "2021" + # https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#max_width -max_width = 120 \ No newline at end of file +max_width = 120 + +#https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#reorder_imports +reorder_imports = true + +#https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#unstable_features +unstable_features = true + +# imports_granularity is unstable +# https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#merge_imports +imports_granularity = "Crate" diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 3b6446687..ec98f2b76 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,4 +1,5 @@ ## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact + +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 09a504348..108db9c92 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,12 +6,11 @@ documentation, we greatly value feedback and contributions from our community. Please read through this document before submitting any issues or pull requests to ensure we have all the necessary information to effectively respond to your bug report or contribution. - ## Reporting Bugs/Feature Requests We welcome you to use the GitHub issue tracker to report bugs or suggest features. -When filing an issue, please check [existing open](https://github.com/awslabs/aws-lambda-rust-runtime/issues), or [recently closed](https://github.com/awslabs/aws-lambda-rust-runtime/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already +When filing an issue, please check [existing open](https://github.com/aws/aws-lambda-rust-runtime/issues), or [recently closed](https://github.com/aws/aws-lambda-rust-runtime/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: * A reproducible test case or series of steps @@ -19,8 +18,8 @@ reported the issue. Please try to include as much information as you can. Detail * Any modifications you've made relevant to the bug * Anything unusual about your environment or deployment - ## Contributing via Pull Requests + Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 1. You are working against the latest source on the *main* branch. @@ -39,23 +38,22 @@ To send us a pull request, please: GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). - ## Finding contributions to work on -Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/aws-lambda-rust-runtime/labels/help%20wanted) issues is a great place to start. +Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws/aws-lambda-rust-runtime/labels/help%20wanted) issues is a great place to start. ## Code of Conduct + This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact opensource-codeofconduct@amazon.com with any additional questions or comments. - ## Security issue notifications -If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. +If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. ## Licensing -See the [LICENSE](https://github.com/awslabs/aws-lambda-rust-runtime/blob/main/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. +See the [LICENSE](https://github.com/aws/aws-lambda-rust-runtime/blob/main/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/Cargo.toml b/Cargo.toml index 7361557eb..ce1f66402 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,36 @@ [workspace] +resolver = "3" members = [ "lambda-http", "lambda-integration-tests", "lambda-runtime-api-client", "lambda-runtime", - "lambda-extension" + "lambda-extension", + "lambda-events", ] -exclude = ["examples"] +exclude = ["examples","lambda-events/lambda-events-examples"] + +[workspace.dependencies] +base64 = "0.22" +bytes = "1" +chrono = { version = "0.4.35", default-features = false, features = [ + "clock", + "serde", + "std", +] } +futures = "0.3" +futures-channel = "0.3" +futures-util = "0.3" +http = "1.0" +http-body = "1.0" +http-body-util = "0.1" +http-serde = "2.0" +hyper = "1.0" +hyper-util = "0.1.1" +pin-project-lite = "0.2" +tower = "0.5" +tower-layer = "0.3" +tower-service = "0.3" + + diff --git a/Dockerfile.rie b/Dockerfile.rie new file mode 100644 index 000000000..b55545b22 --- /dev/null +++ b/Dockerfile.rie @@ -0,0 +1,37 @@ +FROM public.ecr.aws/lambda/provided:al2023 + +RUN dnf install -y gcc +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" + +ARG TARGETARCH +ENV RIE_VERSION=1.36 \ + RIE_SHA256_AMD64=ba57f2683260127135ad5ba9bafea141f90492143cbaeb9312cde6dae8d1c08e \ + RIE_SHA256_ARM64=7826415f278663274e279085ff96d7c9da210a30213fa72279e56e59f028ce76 \ + RIE_PATH=/usr/local/bin/aws-lambda-rie + +COPY scripts/download-rie.sh /tmp/download-rie.sh +RUN sh /tmp/download-rie.sh \ + "${TARGETARCH}" \ + "${RIE_VERSION}" \ + "${RIE_SHA256_AMD64}" \ + "${RIE_SHA256_ARM64}" \ + "${RIE_PATH}" \ + && rm /tmp/download-rie.sh + +ARG EXAMPLE=basic-lambda + +COPY Cargo.* /build/ +COPY lambda-runtime /build/lambda-runtime +COPY lambda-runtime-api-client /build/lambda-runtime-api-client +COPY lambda-events /build/lambda-events +COPY lambda-http /build/lambda-http +COPY lambda-extension /build/lambda-extension +COPY examples/${EXAMPLE} /build/examples/${EXAMPLE} + +WORKDIR /build/examples/${EXAMPLE} +RUN cargo build --release +RUN cp target/release/${EXAMPLE} ${LAMBDA_RUNTIME_DIR}/bootstrap + +ENTRYPOINT [] +CMD [ "/usr/local/bin/aws-lambda-rie", "/var/runtime/bootstrap" ] \ No newline at end of file diff --git a/Dockerfile.test b/Dockerfile.test new file mode 100644 index 000000000..ece55bf63 --- /dev/null +++ b/Dockerfile.test @@ -0,0 +1,22 @@ +FROM public.ecr.aws/lambda/provided:al2023 + +ARG TARGETARCH +ENV RIE_VERSION=1.36 \ + RIE_SHA256_AMD64=ba57f2683260127135ad5ba9bafea141f90492143cbaeb9312cde6dae8d1c08e \ + RIE_SHA256_ARM64=7826415f278663274e279085ff96d7c9da210a30213fa72279e56e59f028ce76 \ + RIE_PATH=/usr/local/bin/aws-lambda-rie + +COPY scripts/download-rie.sh /tmp/download-rie.sh +RUN sh /tmp/download-rie.sh \ + "${TARGETARCH}" \ + "${RIE_VERSION}" \ + "${RIE_SHA256_AMD64}" \ + "${RIE_SHA256_ARM64}" \ + "${RIE_PATH}" \ + && rm /tmp/download-rie.sh + +COPY scripts/custom-lambda-entrypoint.sh /usr/local/bin/lambda-entrypoint +RUN chmod +x /usr/local/bin/lambda-entrypoint + +ENTRYPOINT ["/usr/local/bin/lambda-entrypoint"] +CMD ["basic-lambda"] diff --git a/Makefile b/Makefile index cb00545c4..ccc415a72 100644 --- a/Makefile +++ b/Makefile @@ -5,9 +5,19 @@ INTEG_API_INVOKE := RestApiUrl HttpApiUrl INTEG_EXTENSIONS := extension-fn extension-trait logs-trait # Using musl to run extensions on both AL1 and AL2 INTEG_ARCH := x86_64-unknown-linux-musl +RIE_MAX_CONCURRENCY ?= 4 +TEST_RUNNER_BRANCH ?= main +CONTAINER_READY_DELAY_SECS ?= 5 +OUTPUT_DIR ?= test/dockerized/tasks +HANDLERS_TO_BUILD ?= basic-lambda basic-sqs http-basic-lambda basic-lambda-concurrent +HANDLER ?= basic-lambda + +.PHONY: help pr-check integration-tests check-event-features fmt build-examples build-test-runner test-rie test-rie-lmi nuke test-dockerized test-dockerized-concurrent + +.DEFAULT_GOAL := help define uppercase -$(shell sed -r 's/(^|-)(\w)/\U\2/g' <<< $(1)) +$(shell echo '$(1)' | sed -r 's/(^|-)(\w)/\U\2/g') endef pr-check: @@ -60,3 +70,137 @@ invoke-integration-api-%: curl -X POST -d '{"command": "hello"}' $(API_URL)/trait/post curl -X POST -d '{"command": "hello"}' $(API_URL)/al2/post curl -X POST -d '{"command": "hello"}' $(API_URL)/al2-trait/post + +# Test individual event features to ensure optional dependencies +# are correctly loaded when all default features are disabled. +check-event-features: + cargo test --package aws_lambda_events --no-default-features --features activemq + cargo test --package aws_lambda_events --no-default-features --features alb + cargo test --package aws_lambda_events --no-default-features --features apigw + cargo test --package aws_lambda_events --no-default-features --features appsync + cargo test --package aws_lambda_events --no-default-features --features autoscaling + cargo test --package aws_lambda_events --no-default-features --features bedrock_agent_runtime + cargo test --package aws_lambda_events --no-default-features --features chime_bot + cargo test --package aws_lambda_events --no-default-features --features clientvpn + cargo test --package aws_lambda_events --no-default-features --features cloudwatch_alarms + cargo test --package aws_lambda_events --no-default-features --features cloudwatch_events + cargo test --package aws_lambda_events --no-default-features --features cloudwatch_logs + cargo test --package aws_lambda_events --no-default-features --features code_commit + cargo test --package aws_lambda_events --no-default-features --features codebuild + cargo test --package aws_lambda_events --no-default-features --features codedeploy + cargo test --package aws_lambda_events --no-default-features --features codepipeline_cloudwatch + cargo test --package aws_lambda_events --no-default-features --features codepipeline_job + cargo test --package aws_lambda_events --no-default-features --features cognito + cargo test --package aws_lambda_events --no-default-features --features config + cargo test --package aws_lambda_events --no-default-features --features connect + cargo test --package aws_lambda_events --no-default-features --features documentdb + cargo test --package aws_lambda_events --no-default-features --features dynamodb + cargo test --package aws_lambda_events --no-default-features --features ecr_scan + cargo test --package aws_lambda_events --no-default-features --features eventbridge + cargo test --package aws_lambda_events --no-default-features --features firehose + cargo test --package aws_lambda_events --no-default-features --features iam + cargo test --package aws_lambda_events --no-default-features --features iot + cargo test --package aws_lambda_events --no-default-features --features iot_1_click + cargo test --package aws_lambda_events --no-default-features --features iot_button + cargo test --package aws_lambda_events --no-default-features --features iot_deprecated + cargo test --package aws_lambda_events --no-default-features --features kafka + cargo test --package aws_lambda_events --no-default-features --features kinesis + cargo test --package aws_lambda_events --no-default-features --features kinesis_analytics + cargo test --package aws_lambda_events --no-default-features --features lambda_function_urls + cargo test --package aws_lambda_events --no-default-features --features lex + cargo test --package aws_lambda_events --no-default-features --features rabbitmq + cargo test --package aws_lambda_events --no-default-features --features s3 + cargo test --package aws_lambda_events --no-default-features --features s3_batch_job + cargo test --package aws_lambda_events --no-default-features --features secretsmanager + cargo test --package aws_lambda_events --no-default-features --features ses + cargo test --package aws_lambda_events --no-default-features --features sns + cargo test --package aws_lambda_events --no-default-features --features sqs + cargo test --package aws_lambda_events --no-default-features --features streams + cargo test --package aws_lambda_events --no-default-features --features vpc_lattice + +fmt: + cargo +nightly fmt --all + +build-examples: + HANDLERS_TO_BUILD="$(subst ",,$(HANDLERS_TO_BUILD))" OUTPUT_DIR="$(OUTPUT_DIR)" ./scripts/build-examples.sh + +nuke: + docker kill $$(docker ps -q) + +build-test-runner: build-examples + @echo "Building base Docker image with RIE and custom entrypoint..." + docker build \ + -t local/test-base \ + -f Dockerfile.test \ + . + + @echo "Setting up containerized test runner..." + @if [ ! -d ".test-runner" ]; then \ + echo "Cloning containerized-test-runner-for-aws-lambda..."; \ + git clone --quiet --branch $(TEST_RUNNER_BRANCH) https://github.com/aws/containerized-test-runner-for-aws-lambda.git .test-runner; \ + fi + @echo "Building test runner Docker image..." + @docker build -t test-runner:local -f .test-runner/Dockerfile .test-runner + +test-dockerized-concurrent: HANDLERS_TO_BUILD := basic-lambda-concurrent invocation-id-concurrent +test-dockerized-concurrent: build-test-runner + @echo "Running concurrent scenarios in Docker..." + @docker network rm concurrent-test-net 2>/dev/null || true + @docker network create concurrent-test-net + @docker run --rm \ + -e INPUT_SUITE_FILE_ARRAY='[]' \ + -e INPUT_SCENARIO_DIR=/workspace/test/dockerized/scenarios \ + -e DOCKER_IMAGE_NAME=local/test-base \ + -e TASK_FOLDER=./test/dockerized/tasks \ + -e GITHUB_WORKSPACE=/workspace \ + -e DOCKER_SHARED_NETWORK=concurrent-test-net \ + -e CONTAINER_READY_DELAY_SECS=$(CONTAINER_READY_DELAY_SECS) \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$(CURDIR):/workspace" \ + -w /workspace \ + test-runner:local + @docker network rm --force concurrent-test-net 2>/dev/null || true + +test-dockerized: build-test-runner + @echo "Running tests in Docker..." + @docker run --rm \ + -e INPUT_SUITE_FILE_ARRAY='["./test/dockerized/suites/*.json"]' \ + -e DOCKER_IMAGE_NAME=local/test-base \ + -e TASK_FOLDER=./test/dockerized/tasks \ + -e GITHUB_WORKSPACE=/workspace \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$(CURDIR):/workspace" \ + -w /workspace \ + test-runner:local + +test-rie: + HANDLER="$(HANDLER)" ./scripts/test-rie.sh + +# Run RIE in Lambda Managed Instance (LMI) mode with concurrent polling. +test-rie-lmi: + RIE_MAX_CONCURRENCY=$(RIE_MAX_CONCURRENCY) HANDLER="$(HANDLER)" ./scripts/test-rie.sh $(EXAMPLE) + +help: ## Show this help message + @echo 'Usage: make [target]' + @echo '' + @echo 'Available targets:' + @echo ' pr-check Run pre-commit checks (fmt, clippy, tests)' + @echo ' integration-tests Build and run AWS integration tests' + @echo ' check-event-features Test individual event features' + @echo ' fmt Format code with cargo fmt' + @echo ' build-examples Build example Lambda functions' + @echo ' Usage: EXAMPLES="basic-lambda" OUTPUT_DIR=/ make build-examples' + @echo ' test-rie Test Lambda with Runtime Interface Emulator' + @echo ' Usage: HANDLERS_TO_BUILD="basic-lambda basic-sqs" make test-rie' + @echo ' Usage: HANDLERS_TO_BUILD="basic-lambda" HANDLER="basic-lambda" make test-rie' + @echo ' test-rie-lmi Test RIE in Lambda Managed Instance mode' + @echo ' Usage: RIE_MAX_CONCURRENCY=4 HANDLERS_TO_BUILD="basic-lambda-concurrent" make test-rie-lmi' + @echo ' test-dockerized Run dockerized test harness' + @echo ' test-dockerized-concurrent Run concurrent LMI test scenarios' + @echo ' nuke Kill all running Docker containers' + @echo '' + @echo 'Environment variables:' + @echo ' HANDLERS_TO_BUILD Space-separated list of handlers to build for RIE (for test-rie)' + @echo ' HANDLER Specific handler to run (defaults to first in HANDLERS_TO_BUILD)' + @echo ' OUTPUT_DIR Directory for built binaries (default: test/dockerized/tasks)' + @echo ' RIE_MAX_CONCURRENCY Max concurrent Lambda invocations for LMI mode (for test-rie-lmi)' diff --git a/README.md b/README.md index 70573d94b..7b0cc9438 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,18 @@ # Rust Runtime for AWS Lambda -[![Build Status](https://github.com/awslabs/aws-lambda-rust-runtime/workflows/Rust/badge.svg)](https://github.com/awslabs/aws-lambda-rust-runtime/actions) +[![Build Status](https://github.com/aws/aws-lambda-rust-runtime/actions/workflows/check-examples.yml/badge.svg)](https://github.com/aws/aws-lambda-rust-runtime/actions) This package makes it easy to run AWS Lambda Functions written in Rust. This workspace includes multiple crates: - [![Docs](https://docs.rs/lambda_runtime/badge.svg)](https://docs.rs/lambda_runtime) **`lambda-runtime`** is a library that provides a Lambda runtime for applications written in Rust. -- [![Docs](https://docs.rs/lambda_http/badge.svg)](https://docs.rs/lambda_http) **`lambda-http`** is a library that makes it easy to write API Gateway proxy event focused Lambda functions in Rust. +- [![Docs](https://docs.rs/lambda_http/badge.svg)](https://docs.rs/lambda_http) **`lambda-http`** is a library for writing HTTP Lambda functions in Rust, with support for upstream API Gateway, ALB, Lambda Function URLs, and VPC Lattice. - [![Docs](https://docs.rs/lambda-extension/badge.svg)](https://docs.rs/lambda-extension) **`lambda-extension`** is a library that makes it easy to write Lambda Runtime Extensions in Rust. +- [![Docs](https://docs.rs/aws_lambda_events/badge.svg)](https://docs.rs/aws_lambda_events) **`lambda-events`** is a library with strongly-typed Lambda event structs in Rust. - [![Docs](https://docs.rs/lambda_runtime_api_client/badge.svg)](https://docs.rs/lambda_runtime_api_client) **`lambda-runtime-api-client`** is a shared library between the lambda runtime and lambda extension libraries that includes a common API client to talk with the AWS Lambda Runtime API. ## Getting started -The easiest way to start writing Lambda functions with Rust is by using Cargo Lambda. This Cargo subcommand provides several commands to help you in your journey with Rust on AWS Lambda. +The easiest way to start writing Lambda functions with Rust is by using [Cargo Lambda](https://www.cargo-lambda.info/), a related project. Cargo Lambda is a Cargo plugin, or subcommand, that provides several commands to help you in your journey with Rust on AWS Lambda. The preferred way to install Cargo Lambda is by using a package manager. @@ -34,14 +35,19 @@ Or PiP on any system with Python 3 installed: ```bash pip3 install cargo-lambda ``` +Alternatively, you can install the pip package as an executable using [uv](https://docs.astral.sh/uv/) + +```bash +uv tool install cargo-lambda +``` See other installation options in [the Cargo Lambda documentation](https://www.cargo-lambda.info/guide/installation.html). -### Your first function +## Your first function To create your first function, run Cargo Lambda with the [subcommand `new`](https://www.cargo-lambda.info/commands/new.html). This command will generate a Rust package with the initial source code for your function: -``` +```bash cargo lambda new YOUR_FUNCTION_NAME ``` @@ -68,40 +74,144 @@ async fn func(event: LambdaEvent) -> Result { } ``` -## Building and deploying your Lambda functions +## Understanding Lambda errors -If you already have Cargo Lambda installed in your machine, run the next command to build your function: +when a function invocation fails, AWS Lambda expects you to return an object that can be serialized into JSON structure with the error information. This structure is represented in the following example: +```json +{ + "error_type": "the type of error raised", + "error_message": "a string description of the error" +} ``` -cargo lambda build --release + +The Rust Runtime for Lambda uses a struct called `Diagnostic` to represent function errors internally. The runtime implements the conversion of several general error types, like `std::error::Error`, into `Diagnostic`. For these general implementations, the `error_type` is the name of the value type returned by your function. For example, if your function returns `lambda_runtime::Error`, the `error_type` will be something like `alloc::boxed::Box`, which is not very descriptive. + +### Implement your own Diagnostic + +To get more descriptive `error_type` fields, you can implement `From` for your error type. That gives you full control on what the `error_type` is: + +```rust +use lambda_runtime::{Diagnostic, Error, LambdaEvent}; + +#[derive(Debug)] +struct ErrorResponse(&'static str); + +impl From for Diagnostic { + fn from(error: ErrorResponse) -> Diagnostic { + Diagnostic { + error_type: "MyErrorType".into(), + error_message: error.0.to_string(), + } + } +} + +async fn handler(_event: LambdaEvent<()>) -> Result<(), ErrorResponse> { + Err(ErrorResponse("this is an error response")) +} ``` -There are other ways of building your function: manually with the AWS CLI, with [AWS SAM](https://github.com/aws/aws-sam-cli), and with the [Serverless framework](https://serverless.com/framework/). +We recommend you to use the [thiserror crate](https://crates.io/crates/thiserror) to declare your errors. You can see an example on how to integrate `thiserror` with the Runtime's diagnostics in our [example repository](https://github.com/aws/aws-lambda-rust-runtime/tree/main/examples/basic-error-thiserror) +### Anyhow, Eyre, and Miette -### 1. Cross-compiling your Lambda functions +Popular error crates like Anyhow, Eyre, and Miette provide their own error types that encapsulate other errors. There is no direct transformation of those errors into `Diagnostic`, but we provide feature flags for each one of those crates to help you integrate them with your Lambda functions. -By default, Cargo Lambda builds your functions to run on x86_64 architectures. If you'd like to use a different architecture, use the options described below. +If you enable the features `anyhow`, `eyre`, or `miette` in the `lambda_runtime` dependency of your package. The error types provided by those crates can have blanket transformations into `Diagnostic`. These features expose an `From for Diagnostic` implementation that transforms those error types into a `Diagnostic`. This is an example that transforms an `anyhow::Error` into a `Diagnostic`: + +```rust +use lambda_runtime::{Diagnostic, LambdaEvent}; + +async fn handler(_event: LambdaEvent) -> Result<(), Diagnostic> { + Err(anyhow::anyhow!("this is an error").into()) +} +``` + +You can see more examples on how to use these error crates in our [example repository](https://github.com/aws/aws-lambda-rust-runtime/tree/main/examples/basic-error-error-crates-integration). + +### Graceful shutdown + +`lambda_runtime` offers a helper to simplify configuring graceful shutdown signal handling, `spawn_graceful_shutdown_handler()`. This requires the `graceful-shutdown` feature flag and only supports Unix systems. + +You can use it by passing a `FnOnce` closure that returns an async block. That async block will be executed +when the function receives a `SIGTERM` or `SIGKILL`. + +Note that this helper is opinionated in a number of ways. Most notably: +1. It spawns a task to drive your signal handlers +2. It registers a 'no-op' extension in order to enable graceful shutdown signals +3. It panics on unrecoverable errors + +If you prefer to fine-tune the behavior, refer to the implementation of `spawn_graceful_shutdown_handler()` as a starting point for your own. + +For more information on graceful shutdown handling in AWS Lambda, see: [aws-samples/graceful-shutdown-with-aws-lambda](https://github.com/aws-samples/graceful-shutdown-with-aws-lambda). + +Complete example (cleaning up a non-blocking tracing writer): + +```rust,no_run +use lambda_runtime::{service_fn, LambdaEvent, Error}; +use serde_json::{json, Value}; + +#[tokio::main] +async fn main() -> Result<(), Error> { + let func = service_fn(func); + + let (writer, log_guard) = tracing_appender::non_blocking(std::io::stdout()); + lambda_runtime::tracing::init_default_subscriber_with_writer(writer); + + let shutdown_hook = || async move { + std::mem::drop(log_guard); + }; + lambda_runtime::spawn_graceful_shutdown_handler(shutdown_hook).await; + + lambda_runtime::run(func).await?; + Ok(()) +} + +async fn func(event: LambdaEvent) -> Result { + let (event, _context) = event.into_parts(); + let first_name = event["firstName"].as_str().unwrap_or("world"); -#### 1.2. Build your Lambda functions + Ok(json!({ "message": format!("Hello, {}!", first_name) })) +} +``` -__Amazon Linux 2__ +## Building and deploying your Lambda functions -We recommend you to use Amazon Linux 2 runtimes (such as `provided.al2`) as much as possible for building Lambda functions in Rust. To build your Lambda functions for Amazon Linux 2 runtimes, run: +If you already have Cargo Lambda installed in your machine, run the next command to build your function: ```bash -cargo lambda build --release --arm64 +cargo lambda build --release ``` -__Amazon Linux 1__ +There are other ways of building your function: manually with the AWS CLI, with [AWS SAM](https://github.com/aws/aws-sam-cli), and with the [Serverless framework](https://serverless.com/framework/). + +### CPU jitter entropy and cold starts -Amazon Linux 1 uses glibc version 2.17, while Rust binaries need glibc version 2.18 or later by default. However, with Cargo Lambda, you can specify a different version of glibc. +The AWS SDK for Rust commonly uses AWS-LC as its TLS provider. Starting with AWS-LC 1.60 (`aws-lc-rs` 1.14.1), AWS-LC collects CPU jitter as an additional entropy source. This happens once per process and can add anywhere from several milliseconds to about a second of cold-start latency. The effect is most noticeable on smaller functions. -If you are building for Amazon Linux 1, or you want to support both Amazon Linux 2 and 1, run: +If that tradeoff does not suit your workload, you can disable CPU jitter entropy at build time in `.cargo/config.toml`: +```toml +[env] +AWS_LC_SYS_NO_JITTER_ENTROPY = "1" ``` -# Note: replace "aarch64" with "x86_64" if you are building for x86_64 -cargo lambda build --release --target aarch64-unknown-linux-gnu.2.17 + +The setting requires `aws-lc-sys` 0.32.3 or newer and must be present when the function is compiled. It does not change an already-built binary. AWS-LC will continue to use traditional entropy sources such as `RD_RAND` or `/dev/urandom`, but disabling jitter reduces the number of entropy sources. Consider the security and cold-start requirements of your application before opting out. + +See the [AWS SDK announcement](https://github.com/smithy-lang/smithy-rs/discussions/4541) and the [AWS-LC entropy configuration documentation](https://aws.github.io/aws-lc-rs/resources.html#entropy-configuration) for more detail. The setting applies to `aws-lc-sys`, not `aws-lc-fips-sys`. + +### 1. Cross-compiling your Lambda functions + +By default, Cargo Lambda builds your functions to run on x86_64 architectures. If you'd like to use a different architecture, use the options described below. + +#### 1.1. Build your Lambda functions + +__Amazon Linux 2023__ + +We recommend you to use the Amazon Linux 2023 (such as `provided.al2023`) because it includes a newer version of GLIBC, which many Rust programs depend on. To build your Lambda functions for Amazon Linux 2023 runtimes, run: + +```bash +cargo lambda build --release --arm64 ``` ### 2. Deploying the binary to AWS Lambda @@ -110,29 +220,25 @@ For [a custom runtime](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-cus You can find the `bootstrap` binary for your function under the `target/lambda` directory. -#### 2.2. Deploying with Cargo Lambda +#### 2.1. Deploying with Cargo Lambda Once you've built your code with one of the options described earlier, use the `deploy` subcommand to upload your function to AWS: ```bash -cargo lambda deploy \ - --iam-role arn:aws:iam::XXXXXXXXXXXXX:role/your_lambda_execution_role +cargo lambda deploy ``` -> **warning** +> **Warning** > Make sure to replace the execution role with an existing role in your account! This command will create a Lambda function with the same name of your rust package. You can change the name of the function by adding the argument at the end of the command: - ```bash -cargo lambda deploy \ - --iam-role arn:aws:iam::XXXXXXXXXXXXX:role/your_lambda_execution_role \ - my-first-lambda-function +cargo lambda deploy my-first-lambda-function ``` -> **info** +> **Note** > See other deployment options in [the Cargo Lambda documentation](https://www.cargo-lambda.info/commands/deploy.html). You can test the function with the [invoke subcommand](https://www.cargo-lambda.info/commands/invoke.html): @@ -144,6 +250,9 @@ cargo lambda invoke --remote \ my-first-lambda-function ``` +> **Note** +> CLI commands in the examples use Linux/MacOS syntax. For different shells like Windows CMD or PowerShell, modify syntax when using nested quotation marks like `'{"command": "hi"}'`. Escaping with a backslash may be necessary. See [AWS CLI Reference](https://docs.amazonaws.cn/en_us/cli/latest/userguide/cli-usage-parameters-quoting-strings.html#cli-usage-parameters-quoting-strings-containing) for more information. + #### 2.2. Deploying with the AWS CLI You can also use the AWS CLI to deploy your Rust functions. First, you will need to create a ZIP archive of your function. Cargo Lambda can do that for you automatically when it builds your binary if you add the `output-format` flag: @@ -154,18 +263,17 @@ cargo lambda build --release --arm64 --output-format zip You can find the resulting zip file in `target/lambda/YOUR_PACKAGE/bootstrap.zip`. Use that file path to deploy your function with the [AWS CLI](https://aws.amazon.com/cli/): - ```bash $ aws lambda create-function --function-name rustTest \ --handler bootstrap \ --zip-file fileb://./target/lambda/basic/bootstrap.zip \ - --runtime provided.al2 \ # Change this to provided.al if you would like to use Amazon Linux 1. + --runtime provided.al2023 \ # Change this to provided.al2 if you would like to use Amazon Linux 2 --role arn:aws:iam::XXXXXXXXXXXXX:role/your_lambda_execution_role \ --environment Variables={RUST_BACKTRACE=1} \ --tracing-config Mode=Active ``` -> **warning** +> **Warning** > Make sure to replace the execution role with an existing role in your account! You can now test the function using the AWS CLI or the AWS Lambda console @@ -179,8 +287,8 @@ $ aws lambda invoke $ cat output.json # Prints: {"msg": "Command Say Hi! executed."} ``` -**Note:** `--cli-binary-format raw-in-base64-out` is a required - argument when using the AWS CLI version 2. [More Information](https://docs.aws.amazon.com/cli/latest/userguide/cliv2-migration.html#cliv2-migration-binaryparam) +> **Note** +> `--cli-binary-format raw-in-base64-out` is a required argument when using the AWS CLI version 2. [More Information](https://docs.aws.amazon.com/cli/latest/userguide/cliv2-migration.html#cliv2-migration-binaryparam) #### 2.3. AWS Serverless Application Model (SAM) @@ -199,7 +307,7 @@ Resources: MemorySize: 128 Architectures: ["arm64"] Handler: bootstrap - Runtime: provided.al2 + Runtime: provided.al2023 Timeout: 5 CodeUri: target/lambda/basic/ @@ -226,188 +334,274 @@ $ aws lambda invoke $ cat output.json # Prints: {"msg": "Command Say Hi! executed."} ``` -#### 2.4. Serverless Framework +## Local development and testing -Alternatively, you can build a Rust-based Lambda function declaratively using the [Serverless framework Rust plugin](https://github.com/softprops/serverless-rust). +### Testing your code with unit and integration tests -A number of getting started Serverless application templates exist to get you up and running quickly: +AWS Lambda events are plain structures deserialized from JSON objects. +If your function handler uses the standard runtime, you can use `serde` to deserialize +your text fixtures into the structures, and call your handler directly: -- a minimal [echo function](https://github.com/softprops/serverless-aws-rust) to demonstrate what the smallest Rust function setup looks like -- a minimal [http function](https://github.com/softprops/serverless-aws-rust-http) to demonstrate how to interface with API Gateway using Rust's native [http](https://crates.io/crates/http) crate (note this will be a git dependency until 0.2 is published) -- a combination [multi function service](https://github.com/softprops/serverless-aws-rust-multi) to demonstrate how to set up a services with multiple independent functions +```rust,no_run +#[test] +fn test_my_lambda_handler() { + let input = serde_json::from_str("{\"command\": \"Say Hi!\"}").expect("failed to parse event"); + let context = lambda_runtime::Context::default(); -Assuming your host machine has a relatively recent version of node, you [won't need to install any host-wide serverless dependencies](https://blog.npmjs.org/post/162869356040/introducing-npx-an-npm-package-runner). To get started, run the following commands to create a new lambda Rust application -and install project level dependencies. + let event = lambda_runtime::LambdaEvent::new(input, context); -```bash -$ npx serverless install \ - --url https://github.com/softprops/serverless-aws-rust \ - --name my-new-app \ - && cd my-new-app \ - && npm install --silent + my_lambda_handler(event).await.expect("failed to handle event"); +} ``` -Deploy it using the standard serverless workflow: +If you're using `lambda_http` to receive HTTP events, you can also create `http_lambda::Request` +structures from plain text fixtures: + +```rust,no_run +#[test] +fn test_my_lambda_handler() { + let input = include_str!("apigw_proxy_request.json"); + + let request = lambda_http::request::from_str(input) + .expect("failed to create request"); + + let response = my_lambda_handler(request).await.expect("failed to handle request"); +} +``` + +### Local dev server with Cargo Lambda + +[Cargo Lambda](https://www.cargo-lambda.info) provides a local server that emulates the AWS Lambda control plane. This server works on Windows, Linux, and MacOS. In the root of your Lambda project. You can run the following subcommand to compile your function(s) and start the server. ```bash -# build, package, and deploy service to aws lambda -$ npx serverless deploy +cargo lambda watch ``` -Invoke it using serverless framework or a configured AWS integrated trigger source: +Now you can use the `cargo lambda invoke` to send requests to your function. For example: ```bash -$ npx serverless invoke -f hello -d '{"foo":"bar"}' +cargo lambda invoke --data-ascii '{ "command": "hi" }' ``` -#### 2.5. Docker +Running the command on a HTTP function (Function URL, API Gateway, etc) will require you to use the appropriate scheme. You can find examples of these schemes [here](https://github.com/aws/aws-lambda-rust-runtime/tree/main/lambda-http/tests/data). Otherwise, you will be presented with the following error. -Alternatively, you can build a Rust-based Lambda function in a [docker mirror of the AWS Lambda provided runtime with the Rust toolchain preinstalled](https://github.com/rust-serverless/lambda-rust). +```rust,no_run +Error: serde_json::error::Error -Running the following command will start an ephemeral docker container, which will build your Rust application and produce a zip file containing its binary auto-renamed to `bootstrap` to meet the AWS Lambda's expectations for binaries under `target/lambda_runtime/release/{your-binary-name}.zip`. Typically, this is just the name of your crate if you are using the cargo default binary (i.e. `main.rs`): + × data did not match any variant of untagged enum LambdaRequest +``` + +An simpler alternative is to cURL the following endpoint based on the address and port you defined. For example: ```bash -# build and package deploy-ready artifact -$ docker run --rm \ - -v ${PWD}:/code \ - -v ${HOME}/.cargo/registry:/root/.cargo/registry \ - -v ${HOME}/.cargo/git:/root/.cargo/git \ - rustserverless/lambda-rust +curl -v -X POST \ + 'http://127.0.0.1:9000/lambda-url//' \ + -H 'content-type: application/json' \ + -d '{ "command": "hi" }' ``` -With your application build and packaged, it's ready to ship to production. You can also invoke it locally to verify is behavior using the [lambci :provided docker container](https://hub.docker.com/r/lambci/lambda/), which is also a mirror of the AWS Lambda provided runtime with build dependencies omitted: +> **Warning** +> Do not remove the `content-type` header. It is necessary to instruct the function how to deserialize the request body. + +You can read more about how [cargo lambda watch](https://www.cargo-lambda.info/commands/watch.html) and [cargo lambda invoke](https://www.cargo-lambda.info/commands/invoke.html) work on the project's [documentation page](https://www.cargo-lambda.info). + +### Local testing with Runtime Interface Emulator (RIE) + +For testing with the official AWS Lambda Runtime Interface Emulator: ```bash -# start a docker container replicating the "provided" lambda runtime -# awaiting an event to be provided via stdin -$ unzip -o \ - target/lambda/release/{your-binary-name}.zip \ - -d /tmp/lambda && \ - docker run \ - -i -e DOCKER_LAMBDA_USE_STDIN=1 \ - --rm \ - -v /tmp/lambda:/var/task \ - lambci/lambda:provided +make test-rie +``` -# provide an event payload via stdin (typically a json blob) +By default, this builds and tests the `basic-lambda` example. To build and test a custom handler: -# Ctrl-D to yield control back to your function +```bash +HANDLER="basic-tenant-id" make test-rie ``` -## Local development and testing +To test Lambda Managed Instances (concurrent polling): -### Testing your code with unit and integration tests +```bash +RIE_MAX_CONCURRENCY=4 make test-rie +``` -AWS Lambda events are plain structures deserialized from JSON objects. -If your function handler uses the standard runtime, you can use `serde` to deserialize -your text fixtures into the structures, and call your handler directly: +Different examples expect different payload formats. Check the example's source code in `examples/EXAMPLE_NAME/src/main.rs` -```rust,no_run -#[test] -fn test_my_lambda_handler() { - let input = serde_json::from_str("{\"command\": \"Say Hi!\"}").expect("failed to parse event"); - let context = lambda_runtime::Context::default(); +### Dockerized test harness - let event = lambda_runtime::LambdaEvent::new(input, context); +For automated testing with AWS's containerized test runner: - my_lambda_handler(event).await.expect("failed to handle event"); +```bash +make test-dockerized +``` + +This runs test suites defined in `test/dockerized/*.json` files using the [containerized-test-runner-for-aws-lambda](https://github.com/aws/containerized-test-runner-for-aws-lambda). Test suites specify handlers to test (from examples), request payloads, and expected response assertions. + +Example test suite (`test/dockerized/core.json`): +```json +{ + "tests": [ + { + "name": "test_echo", + "handler": "basic-lambda", + "request": { + "command": "test" + }, + "assertions": [ + { + "response": { + "msg": "Command test executed." + }, + "transform": "{msg: .msg}" + } + ] + } + ] } ``` -If you're using `lambda_http` to receive HTTP events, you can also create `http_lambda::Request` -structures from plain text fixtures: +The `transform` field uses jq syntax to extract specific fields from responses before comparison, useful when responses include dynamic fields like request IDs. -```rust,no_run -#[test] -fn test_my_lambda_handler() { - let input = include_str!("apigw_proxy_request.json"); +### Lambda Debug Proxy - let request = lambda_http::request::from_str(input) - .expect("failed to create request"); +Lambdas can be run and debugged locally using a special [Lambda debug proxy](https://github.com/rimutaka/lambda-debug-proxy) (a non-AWS repo maintained by @rimutaka), which is a Lambda function that forwards incoming requests to one AWS SQS queue and reads responses from another queue. A local proxy running on your development computer reads the queue, calls your Lambda locally and sends back the response. This approach allows debugging of Lambda functions locally while being part of your AWS workflow. The Lambda handler code does not need to be modified between the local and AWS versions. - let response = my_lambda_handler(request).await.expect("failed to handle request"); -} -``` +## Tracing and Logging -### Cargo Lambda +The Rust Runtime for Lambda integrates with the [Tracing](https://tracing.rs) libraries to provide tracing and logging. -[Cargo Lambda](https://www.cargo-lambda.info) provides a local server that emulates the AWS Lambda control plane. This server works on Windows, Linux, and MacOS. In the root of your Lambda project, run the subcommand `cargo lambda start` to start the server. Your function will be compiled when the server receives the first request to process. Use the subcommand `cargo lambda invoke` to send requests to your function. The `start` subcommand will watch your function's code for changes, and it will compile it every time you save the source after making changes. +By default, the runtime emits `tracing` events that you can collect via `tracing-subscriber`. It also enabled a feature called `tracing` that exposes a default subscriber with sensible options to send logging information to AWS CloudWatch. Follow the next example that shows how to enable the default subscriber: -You can read more about how [cargo lambda watch](https://www.cargo-lambda.info/commands/watch.html) and [cargo lambda invoke](https://www.cargo-lambda.info/commands/watch.html) work on the [project's documentation page](https://www.cargo-lambda.info). +```rust +use lambda_runtime::{run, service_fn, tracing, Error}; -### Lambda Debug Proxy +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing::init_default_subscriber(); + run(service_fn(|event| tracing::info!(?event))).await +} +``` -Lambdas can be run and debugged locally using a special [Lambda debug proxy](https://github.com/rimutaka/lambda-debug-proxy) (a non-AWS repo maintained by @rimutaka), which is a Lambda function that forwards incoming requests to one AWS SQS queue and reads responses from another queue. A local proxy running on your development computer reads the queue, calls your Lambda locally and sends back the response. This approach allows debugging of Lambda functions locally while being part of your AWS workflow. The Lambda handler code does not need to be modified between the local and AWS versions. +The subscriber uses `RUST_LOG` environment variable to determine the log level for your function. It also uses [Lambda's advanced logging controls](https://aws.amazon.com/blogs/compute/introducing-advanced-logging-controls-for-aws-lambda-functions/), if configured. + +By default, the log level to emit events is `INFO`. Log at `TRACE` level for more detail, including a dump of the raw payload. ## AWS event objects -This project does not currently include Lambda event struct definitions. Instead, the community-maintained [`aws_lambda_events`](https://crates.io/crates/aws_lambda_events) crate can be leveraged to provide strongly-typed Lambda event structs. You can create your own custom event objects and their corresponding structs as well. +This project includes Lambda event struct definitions, [`aws_lambda_events`](https://crates.io/crates/aws_lambda_events). This crate can be leveraged to provide strongly-typed Lambda event structs. You can create your own custom event objects and their corresponding structs as well. -### Custom event objects +### Builder pattern for event responses -To serialize and deserialize events and responses, we suggest using the [`serde`](https://github.com/serde-rs/serde) library. To receive custom events, annotate your structure with Serde's macros: +The `aws_lambda_events` crate provides an optional `builders` feature that adds builder pattern support for constructing event responses. This is particularly useful when working with custom context types that don't implement `Default`. -```rust,no_run -use serde::{Serialize, Deserialize}; -use serde_json::json; -use std::error::Error; +Enable the builders feature in your `Cargo.toml`: -#[derive(Serialize, Deserialize)] -pub struct NewIceCreamEvent { - pub flavors: Vec, -} +```toml +[dependencies] +aws_lambda_events = { version = "*", features = ["builders"] } +``` -#[derive(Serialize, Deserialize)] -pub struct NewIceCreamResponse { - pub flavors_added_count: usize, +Example with API Gateway custom authorizers: + +```rust +use aws_lambda_events::event::apigw::{ + ApiGatewayV2CustomAuthorizerSimpleResponseBuilder, + ApiGatewayV2CustomAuthorizerV2Request, +}; +use lambda_runtime::{Error, LambdaEvent}; + +struct MyContext { + user_id: String, + permissions: Vec, } -fn main() -> Result<(), Box> { - let flavors = json!({ - "flavors": [ - "Nocciola", - "抹茶", - "आम" - ] - }); - - let event: NewIceCreamEvent = serde_json::from_value(flavors)?; - let response = NewIceCreamResponse { - flavors_added_count: event.flavors.len(), +async fn handler( + event: LambdaEvent, +) -> Result, Error> { + let context = MyContext { + user_id: "user-123".to_string(), + permissions: vec!["read".to_string()], }; - serde_json::to_string(&response)?; - Ok(()) + let response = ApiGatewayV2CustomAuthorizerSimpleResponseBuilder::default() + .is_authorized(true) + .context(context) + .build()?; + + Ok(response) } ``` -## Feature flags in lambda_http +Example with SQS batch responses: + +```rust +use aws_lambda_events::event::sqs::{ + BatchItemFailureBuilder, + SqsBatchResponseBuilder, + SqsEvent, +}; +use lambda_runtime::{Error, LambdaEvent}; + +async fn handler(event: LambdaEvent) -> Result { + let mut failures = Vec::new(); + + for record in event.payload.records { + if let Err(_) = process_record(&record).await { + let failure = BatchItemFailureBuilder::default() + .item_identifier(record.message_id.unwrap()) + .build()?; + failures.push(failure); + } + } + + let response = SqsBatchResponseBuilder::default() + .batch_item_failures(failures) + .build()?; + + Ok(response) +} +``` -`lambda_http` is a wrapper for HTTP events coming from three different services, Amazon Load Balancer (ALB), Amazon Api Gateway (APIGW), and AWS Lambda Function URLs. Amazon Api Gateway can also send events from three different endpoints, REST APIs, HTTP APIs, and WebSockets. `lambda_http` transforms events from all these sources into native `http::Request` objects, so you can incorporate Rust HTTP semantics into your Lambda functions. +See the [examples directory](https://github.com/aws/aws-lambda-rust-runtime/tree/main/lambda-events/examples) for more builder pattern examples. -By default, `lambda_http` compiles your function to support any of those services. This increases the compile time of your function because we have to generate code for all the sources. In reality, you'll usually put a Lambda function only behind one of those sources. You can choose which source to generate code for with feature flags. +### Custom event objects -The available features flags for `lambda_http` are the following: +To serialize and deserialize events and responses, we suggest using the [`serde`](https://github.com/serde-rs/serde) library. To receive custom events, annotate your structure with Serde's macros: -- `alb`: for events coming from [Amazon Elastic Load Balancer](https://aws.amazon.com/elasticloadbalancing/). -- `apigw_rest`: for events coming from [Amazon API Gateway Rest APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html). -- `apigw_http`: for events coming from [Amazon API Gateway HTTP APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html) and [AWS Lambda Function URLs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html). -- `apigw_websockets`: for events coming from [Amazon API Gateway WebSockets](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api.html). +```rust,no_run +use lambda_runtime::{service_fn, Error, LambdaEvent}; +use serde::{Deserialize, Serialize}; -If you only want to support one of these sources, you can disable the default features, and enable only the source that you care about in your package's `Cargo.toml` file. Substitute the dependency line for `lambda_http` for the snippet below, changing the feature that you want to enable: +#[derive(Debug, Deserialize)] +pub struct NewIceCreamEvent { + pub flavors: Vec, +} -```toml -[dependencies.lambda_http] -version = "0.5.3" -default-features = false -features = ["apigw_rest"] -``` +#[derive(Serialize)] +pub struct NewIceCreamResponse { + pub flavors_added_count: usize, +} -This will make your function compile much faster. +#[tokio::main] +async fn main() -> Result<(), Error> { + lambda_runtime::run(service_fn(handler)).await?; + Ok(()) +} + +async fn handler( + event: LambdaEvent, +) -> Result { + println!("Received ice cream event: {:?}", event.payload); + + Ok(NewIceCreamResponse { + flavors_added_count: event.payload.flavors.len(), + }) +} +``` ## Supported Rust Versions (MSRV) -The AWS Lambda Rust Runtime requires a minimum of Rust 1.62, and is not guaranteed to build on compiler versions earlier than that. +The AWS Lambda Rust Runtime requires a minimum of Rust 1.84.0, and is not guaranteed to build on compiler versions earlier than that. ## Security diff --git a/examples/advanced-appconfig-feature-flags/.gitignore b/examples/advanced-appconfig-feature-flags/.gitignore new file mode 100644 index 000000000..c41cc9e35 --- /dev/null +++ b/examples/advanced-appconfig-feature-flags/.gitignore @@ -0,0 +1 @@ +/target \ No newline at end of file diff --git a/examples/advanced-appconfig-feature-flags/Cargo.toml b/examples/advanced-appconfig-feature-flags/Cargo.toml new file mode 100644 index 000000000..51b708ecc --- /dev/null +++ b/examples/advanced-appconfig-feature-flags/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "lambda-appconfig" +version = "0.1.0" +edition = "2021" + +# Starting in Rust 1.62 you can use `cargo add` to add dependencies +# to your project. +# +# If you're using an older Rust version, +# download cargo-edit(https://github.com/killercup/cargo-edit#installation) +# to install the `add` subcommand. +# +# Running `cargo add DEPENDENCY_NAME` will +# add the latest version of a dependency to the list, +# and it will keep the alphabetic ordering for you. + +[dependencies] +async-trait = "0.1.88" +lambda_runtime = { path = "../../lambda-runtime" } +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "2.0" +tokio = { version = "1", features = ["macros"] } diff --git a/examples/advanced-appconfig-feature-flags/README.md b/examples/advanced-appconfig-feature-flags/README.md new file mode 100644 index 000000000..3a95ec9fc --- /dev/null +++ b/examples/advanced-appconfig-feature-flags/README.md @@ -0,0 +1,65 @@ +# Rust Lambda with AppConfig Feature Flag + +This project demonstrates a Rust-based AWS Lambda function that uses AWS AppConfig for feature flagging. The function is deployed using AWS CDK and includes automatic rollback capabilities based on error rates. + +## Lambda Function (src/main.rs) + +The Lambda function is written in Rust and does the following: + +1. Integrates with AWS AppConfig to fetch configuration at runtime. +2. Uses a feature flag to determine whether to respond in Spanish. +3. Processes incoming events. +4. Returns a response based on the event and the current feature flag state. + +The function is designed to work with the AWS AppConfig Extension for Lambda, allowing for efficient configuration retrieval. + +## Deployment (cdk directory) + +The project uses AWS CDK for infrastructure as code and deployment. To deploy the project: + +1. Ensure you have the AWS CDK CLI installed and configured. +2. Navigate to the `cdk` directory. +3. Install dependencies: + ``` + npm install + ``` +4. Build the CDK stack: + ``` + npm run build + ``` +5. Deploy the stack: + ``` + cdk deploy + ``` + +## AWS Resources (cdk/lib/cdk-stack.ts) + +The CDK stack creates the following AWS resources: + +1. **AppConfig Application**: Named "MyRustLambdaApp", this is the container for your configuration and feature flags. + +2. **AppConfig Environment**: A "Production" environment is created within the application. + +3. **AppConfig Configuration Profile**: Defines the schema and validation for your configuration. + +4. **AppConfig Hosted Configuration Version**: Contains the actual configuration data, including the "spanish-response" feature flag. + +5. **AppConfig Deployment Strategy**: Defines how configuration changes are rolled out. + +6. **Lambda Function**: A Rust-based function that uses the AppConfig configuration. + - Uses the AWS AppConfig Extension Layer for efficient configuration retrieval. + - Configured with ARM64 architecture and 128MB of memory. + - 30-second timeout. + +7. **CloudWatch Alarm**: Monitors the Lambda function's error rate. + - Triggers if there are more than 5 errors per minute. + +8. **AppConfig Deployment**: Connects all AppConfig components and includes a rollback trigger based on the CloudWatch alarm. + +9. **IAM Role**: Grants the Lambda function permissions to interact with AppConfig and CloudWatch. + +This setup allows for feature flagging with automatic rollback capabilities, ensuring rapid and safe deployment of new features or configurations. + +## Usage + +After deployment, you can update the feature flag in AppConfig to control the Lambda function's behavior. The function will automatically fetch the latest configuration, and if error rates exceed the threshold, AppConfig will automatically roll back to the previous stable configuration. diff --git a/examples/advanced-appconfig-feature-flags/cdk/.npmignore b/examples/advanced-appconfig-feature-flags/cdk/.npmignore new file mode 100644 index 000000000..c1d6d45dc --- /dev/null +++ b/examples/advanced-appconfig-feature-flags/cdk/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/examples/advanced-appconfig-feature-flags/cdk/README.md b/examples/advanced-appconfig-feature-flags/cdk/README.md new file mode 100644 index 000000000..9315fe5b9 --- /dev/null +++ b/examples/advanced-appconfig-feature-flags/cdk/README.md @@ -0,0 +1,14 @@ +# Welcome to your CDK TypeScript project + +This is a blank project for CDK development with TypeScript. + +The `cdk.json` file tells the CDK Toolkit how to execute your app. + +## Useful commands + +* `npm run build` compile typescript to js +* `npm run watch` watch for changes and compile +* `npm run test` perform the jest unit tests +* `npx cdk deploy` deploy this stack to your default AWS account/region +* `npx cdk diff` compare deployed stack with current state +* `npx cdk synth` emits the synthesized CloudFormation template diff --git a/examples/advanced-appconfig-feature-flags/cdk/bin/cdk.ts b/examples/advanced-appconfig-feature-flags/cdk/bin/cdk.ts new file mode 100644 index 000000000..1d8bfd976 --- /dev/null +++ b/examples/advanced-appconfig-feature-flags/cdk/bin/cdk.ts @@ -0,0 +1,21 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { CdkStack } from '../lib/cdk-stack'; + +const app = new cdk.App(); +new CdkStack(app, 'CdkStack', { + /* If you don't specify 'env', this stack will be environment-agnostic. + * Account/Region-dependent features and context lookups will not work, + * but a single synthesized template can be deployed anywhere. */ + + /* Uncomment the next line to specialize this stack for the AWS Account + * and Region that are implied by the current CLI configuration. */ + // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, + + /* Uncomment the next line if you know exactly what Account and Region you + * want to deploy the stack to. */ + // env: { account: '123456789012', region: 'us-east-1' }, + + /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ +}); \ No newline at end of file diff --git a/examples/advanced-appconfig-feature-flags/cdk/cdk.json b/examples/advanced-appconfig-feature-flags/cdk/cdk.json new file mode 100644 index 000000000..87963c5f1 --- /dev/null +++ b/examples/advanced-appconfig-feature-flags/cdk/cdk.json @@ -0,0 +1,72 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/cdk.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false + } +} diff --git a/examples/advanced-appconfig-feature-flags/cdk/jest.config.js b/examples/advanced-appconfig-feature-flags/cdk/jest.config.js new file mode 100644 index 000000000..08263b895 --- /dev/null +++ b/examples/advanced-appconfig-feature-flags/cdk/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + roots: ['/test'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest' + } +}; diff --git a/examples/advanced-appconfig-feature-flags/cdk/lib/cdk-stack.ts b/examples/advanced-appconfig-feature-flags/cdk/lib/cdk-stack.ts new file mode 100644 index 000000000..3e76deb96 --- /dev/null +++ b/examples/advanced-appconfig-feature-flags/cdk/lib/cdk-stack.ts @@ -0,0 +1,110 @@ +import * as cdk from 'aws-cdk-lib'; +import * as appconfig from 'aws-cdk-lib/aws-appconfig'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; +import { Construct } from 'constructs'; +import { RustFunction } from 'cargo-lambda-cdk'; + +export class CdkStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // Create AppConfig Application + const application = new appconfig.CfnApplication(this, 'MyApplication', { + name: 'MyRustLambdaApp', + }); + + // Create AppConfig Environment + const environment = new appconfig.CfnEnvironment(this, 'MyEnvironment', { + applicationId: application.ref, + name: 'Production', + }); + + // Create AppConfig Configuration Profile + const configProfile = new appconfig.CfnConfigurationProfile(this, 'MyConfigProfile', { + applicationId: application.ref, + name: 'MyConfigProfile', + locationUri: 'hosted', + }); + + // Create AppConfig Hosted Configuration Version + const hostedConfig = new appconfig.CfnHostedConfigurationVersion(this, 'MyHostedConfig', { + applicationId: application.ref, + configurationProfileId: configProfile.ref, + content: JSON.stringify({ + 'spanish-response': false + }), + contentType: 'application/json', + }); + + // Create AppConfig Deployment Strategy + const deploymentStrategy = new appconfig.CfnDeploymentStrategy(this, 'MyDeploymentStrategy', { + name: 'MyDeploymentStrategy', + deploymentDurationInMinutes: 0, + growthFactor: 100, + replicateTo: 'NONE', + }); + + const architecture = lambda.Architecture.ARM_64; + const layerVersion = architecture === lambda.Architecture.ARM_64 ? '68' : '60'; + + // Create Lambda function using cargo-lambda-cdk + const myFunction = new RustFunction(this, 'MyRustFunction', { + functionName: 'my-rust-lambda', + manifestPath: '..', // Points to the parent directory where Cargo.toml is located + architecture, + memorySize: 128, + timeout: cdk.Duration.seconds(30), + environment: { + APPLICATION_ID: application.ref, + ENVIRONMENT_ID: environment.ref, + CONFIGURATION_PROFILE_ID: configProfile.ref, + AWS_APPCONFIG_EXTENSION_PREFETCH_LIST: `/applications/${application.ref}/environments/${environment.ref}/configurations/${configProfile.ref}`, + }, + layers: [ + lambda.LayerVersion.fromLayerVersionArn( + this, + 'AppConfigExtensionLayer', + `arn:aws:lambda:${this.region}:027255383542:layer:AWS-AppConfig-Extension:${layerVersion}` + ), + ], + }); + + // Create CloudWatch Alarm for rollback + const errorRateAlarm = new cloudwatch.Alarm(this, 'ErrorRateAlarm', { + metric: myFunction.metricErrors({ + period: cdk.Duration.minutes(1), + statistic: 'sum', + }), + threshold: 5, + evaluationPeriods: 1, + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, + alarmDescription: 'Alarm if the error rate is greater than 5 errors per minute', + }); + + // Create AppConfig Deployment with rollback configuration + new appconfig.CfnDeployment(this, 'MyDeployment', { + applicationId: application.ref, + environmentId: environment.ref, + deploymentStrategyId: deploymentStrategy.ref, + configurationProfileId: configProfile.ref, + configurationVersion: hostedConfig.ref, + tags: [ + { + key: 'RollbackTrigger', + value: errorRateAlarm.alarmArn, + }, + ], + }); + + // Grant AppConfig permissions to the Lambda function + myFunction.addToRolePolicy(new cdk.aws_iam.PolicyStatement({ + actions: [ + 'appconfig:GetConfiguration', + 'appconfig:StartConfigurationSession', + 'cloudwatch:PutMetricData', + ], + resources: ['*'], + })); + } +} diff --git a/examples/advanced-appconfig-feature-flags/cdk/package-lock.json b/examples/advanced-appconfig-feature-flags/cdk/package-lock.json new file mode 100644 index 000000000..ae4b827d4 --- /dev/null +++ b/examples/advanced-appconfig-feature-flags/cdk/package-lock.json @@ -0,0 +1,7563 @@ +{ + "name": "cdk", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "cdk", + "version": "0.1.0", + "dependencies": { + "aws-cdk-lib": "2.253.0", + "cargo-lambda-cdk": "^0.0.35", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + }, + "bin": { + "cdk": "bin/cdk.js" + }, + "devDependencies": { + "@types/jest": "^29.5.12", + "@types/node": "20.14.2", + "aws-cdk": "2.159.1", + "jest": "^29.7.0", + "ts-jest": "^29.1.4", + "ts-node": "^10.9.2", + "typescript": "~5.4.5" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.273", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.273.tgz", + "integrity": "sha512-X57HYUtHt9BQrlrzUNcMyRsDUCoakYNnY6qh5lNwRCHPtQoTfXmuISkfLk0AjLkcbS5lw1LLTQFiQhTDXfiTvg==" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.1.tgz", + "integrity": "sha512-We4bmHaowOPHr+IQR4/FyTGjRfjgBj4ICMjtqmJeBDWad3Q/6St12NT07leNtyuukv2qMhtSZJQorD8KpKTwRA==" + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "53.22.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-53.22.0.tgz", + "integrity": "sha512-2GLhjjf7Db697rRyYt6rjN06fwbBIiewPo/jxSCyUN259ejF7NQQRwvrdAQb9Aq2jPaIIp1cfHSoEdXyA6Bb7Q==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.4" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.7.4", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", + "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", + "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.6", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz", + "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==", + "dev": true, + "dependencies": { + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.6.tgz", + "integrity": "sha512-sXaDXaJN9SNLymBdlWFA+bjzBhFD617ZaFiY13dGt7TVslVvVgA6fkZOP7Ki3IGElC45lwHdOTrCtKZGVAWeLQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.4.tgz", + "integrity": "sha512-uMOCoHVU52BsSWxPOMVv5qKRdeSlPuImUCB2dlPuBSU+W2/ROE7/Zg8F2Kepbk+8yBa68LlRKxO+xgEVWorsDg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", + "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.6", + "@babel/parser": "^7.25.6", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.13", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.13.tgz", + "integrity": "sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/node": { + "version": "20.14.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", + "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, + "node_modules/aws-cdk": { + "version": "2.159.1", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.159.1.tgz", + "integrity": "sha512-bkJOxic/NpJYQCF3MQhfyJVlFtIzMJeVGZp9jZa7TczxJp79Q/TNKzVJYv6GFabNS1wglGPfWkFB/rIJlRhJkg==", + "dev": true, + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 14.15.0" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.253.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.253.0.tgz", + "integrity": "sha512-36nNLOz0WJDWraEzU7zlmYY8nsM+YEroBWpRZ84LZPaeEe4Ozy4RhBvdCz/jIGYtLbtiFnfk+przNW2NNRjz5Q==", + "bundleDependencies": [ + "@balena/dockerignore", + "@aws-cdk/cloud-assembly-api", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml", + "mime-types" + ], + "dependencies": { + "@aws-cdk/asset-awscli-v1": "2.2.273", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.1", + "@aws-cdk/cloud-assembly-api": "^2.2.2", + "@aws-cdk/cloud-assembly-schema": "^53.18.0", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.3.3", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^10.2.3", + "punycode": "^2.3.1", + "semver": "^7.7.4", + "table": "^6.9.0", + "yaml": "1.10.3" + }, + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "constructs": "^10.5.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api": { + "version": "2.2.2", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.4" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "@aws-cdk/cloud-assembly-schema": ">=53.15.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.18.0", + "inBundle": 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/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "4.0.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "5.0.5", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.1.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.3.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.3.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.2.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "10.2.5", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.7.4", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.9.0", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.3", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001662", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001662.tgz", + "integrity": "sha512-sgMUVwLmGseH8ZIrm1d51UbrhqMCH3jvS7gF/M6byuHOnKyLOBL7W8yz5V02OHwgLGA36o/AFhWzzh4uc5aqTA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/cargo-lambda-cdk": { + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/cargo-lambda-cdk/-/cargo-lambda-cdk-0.0.35.tgz", + "integrity": "sha512-989zAeN/SpXhE1BmjtCuLmTX6nC9qm4CmlVFFJO6ob368w9dZBamlI3jouc+NAX+yW8vbYOih8LUkUIXCWF6VA==", + "bundleDependencies": [ + "js-toml" + ], + "dependencies": { + "js-toml": "^1.0.2" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.215.0", + "constructs": "^10.0.5" + } + }, + "node_modules/cargo-lambda-cdk/node_modules/@babel/runtime-corejs3": { + "version": "7.26.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.30.2", + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/cargo-lambda-cdk/node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/cargo-lambda-cdk/node_modules/@chevrotain/gast": { + "version": "11.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/cargo-lambda-cdk/node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/cargo-lambda-cdk/node_modules/@chevrotain/types": { + "version": "11.0.3", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/cargo-lambda-cdk/node_modules/@chevrotain/utils": { + "version": "11.0.3", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/cargo-lambda-cdk/node_modules/chevrotain": { + "version": "11.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/cargo-lambda-cdk/node_modules/core-js-pure": { + "version": "3.39.0", + "hasInstallScript": true, + "inBundle": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cargo-lambda-cdk/node_modules/js-toml": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "chevrotain": "^11.0.3", + "xregexp": "^5.1.1" + } + }, + "node_modules/cargo-lambda-cdk/node_modules/lodash-es": { + "version": "4.17.21", + "inBundle": true, + "license": "MIT" + }, + "node_modules/cargo-lambda-cdk/node_modules/regenerator-runtime": { + "version": "0.14.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/cargo-lambda-cdk/node_modules/xregexp": { + "version": "5.1.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@babel/runtime-corejs3": "^7.16.5" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", + "dev": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/constructs": { + "version": "10.5.1", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.5.1.tgz", + "integrity": "sha512-f/TfFXiS3G/yVIXDjOQn9oTlyu9Wo7Fxyjj7lb8r92iO81jR2uST+9MstxZTmDGx/CgIbxCXkFXgupnLTNxQZg==" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "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==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.27", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.27.tgz", + "integrity": "sha512-o37j1vZqCoEgBuWWXLHQgTN/KDKe7zwpiY5CPeq2RvUqOyJw9xnrULzZAEVQ5p4h+zjMk7hgtOoPdnLxr7m/jw==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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==", + "dev": true + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "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" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "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, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "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, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "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, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "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, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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 + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-jest": { + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "dev": true, + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@aws-cdk/asset-awscli-v1": { + "version": "2.2.273", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.273.tgz", + "integrity": "sha512-X57HYUtHt9BQrlrzUNcMyRsDUCoakYNnY6qh5lNwRCHPtQoTfXmuISkfLk0AjLkcbS5lw1LLTQFiQhTDXfiTvg==" + }, + "@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.1.tgz", + "integrity": "sha512-We4bmHaowOPHr+IQR4/FyTGjRfjgBj4ICMjtqmJeBDWad3Q/6St12NT07leNtyuukv2qMhtSZJQorD8KpKTwRA==" + }, + "@aws-cdk/cloud-assembly-schema": { + "version": "53.22.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-53.22.0.tgz", + "integrity": "sha512-2GLhjjf7Db697rRyYt6rjN06fwbBIiewPo/jxSCyUN259ejF7NQQRwvrdAQb9Aq2jPaIIp1cfHSoEdXyA6Bb7Q==", + "requires": { + "jsonschema": "~1.4.1", + "semver": "^7.7.4" + }, + "dependencies": { + "jsonschema": { + "version": "1.4.1", + "bundled": true + }, + "semver": { + "version": "7.7.4", + "bundled": true + } + } + }, + "@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + } + }, + "@babel/compat-data": { + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", + "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", + "dev": true + }, + "@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + } + }, + "@babel/generator": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", + "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", + "dev": true, + "requires": { + "@babel/types": "^7.25.6", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + } + }, + "@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "requires": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-module-transforms": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "dev": true + }, + "@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "requires": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "dev": true + }, + "@babel/helpers": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz", + "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==", + "dev": true, + "requires": { + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6" + } + }, + "@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "dev": true, + "requires": { + "@babel/types": "^7.25.6" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-import-attributes": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.6.tgz", + "integrity": "sha512-sXaDXaJN9SNLymBdlWFA+bjzBhFD617ZaFiY13dGt7TVslVvVgA6fkZOP7Ki3IGElC45lwHdOTrCtKZGVAWeLQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.24.8" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.4.tgz", + "integrity": "sha512-uMOCoHVU52BsSWxPOMVv5qKRdeSlPuImUCB2dlPuBSU+W2/ROE7/Zg8F2Kepbk+8yBa68LlRKxO+xgEVWorsDg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.24.8" + } + }, + "@babel/template": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" + } + }, + "@babel/traverse": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", + "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.6", + "@babel/parser": "^7.25.6", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6", + "debug": "^4.3.1", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "requires": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + } + }, + "@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "requires": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + } + }, + "@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3" + } + }, + "@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + } + }, + "@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + } + }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + } + }, + "@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7" + } + }, + "@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "29.5.13", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.13.tgz", + "integrity": "sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==", + "dev": true, + "requires": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "@types/node": { + "version": "20.14.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", + "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + }, + "@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true + }, + "acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "requires": { + "acorn": "^8.11.0" + } + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, + "aws-cdk": { + "version": "2.159.1", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.159.1.tgz", + "integrity": "sha512-bkJOxic/NpJYQCF3MQhfyJVlFtIzMJeVGZp9jZa7TczxJp79Q/TNKzVJYv6GFabNS1wglGPfWkFB/rIJlRhJkg==", + "dev": true, + "requires": { + "fsevents": "2.3.2" + } + }, + "aws-cdk-lib": { + "version": "2.253.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.253.0.tgz", + "integrity": "sha512-36nNLOz0WJDWraEzU7zlmYY8nsM+YEroBWpRZ84LZPaeEe4Ozy4RhBvdCz/jIGYtLbtiFnfk+przNW2NNRjz5Q==", + "requires": { + "@aws-cdk/asset-awscli-v1": "2.2.273", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.1", + "@aws-cdk/cloud-assembly-api": "^2.2.2", + "@aws-cdk/cloud-assembly-schema": "^53.18.0", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.3.3", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^10.2.3", + "punycode": "^2.3.1", + "semver": "^7.7.4", + "table": "^6.9.0", + "yaml": "1.10.3" + }, + "dependencies": { + "@aws-cdk/cloud-assembly-api": { + "version": "2.2.2", + "bundled": true, + "requires": { + "jsonschema": "~1.4.1", + "semver": "^7.7.4" + } + }, + "@balena/dockerignore": { + "version": "1.0.2", + "bundled": true + }, + "ajv": { + "version": "8.18.0", + "bundled": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "bundled": true + }, + "ansi-styles": { + "version": "4.3.0", + "bundled": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "astral-regex": { + "version": "2.0.0", + "bundled": true + }, + "balanced-match": { + "version": "4.0.4", + "bundled": true + }, + "brace-expansion": { + "version": "5.0.5", + "bundled": true, + "requires": { + "balanced-match": "^4.0.2" + } + }, + "case": { + "version": "1.6.3", + "bundled": true + }, + "color-convert": { + "version": "2.0.1", + "bundled": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "bundled": true + }, + "emoji-regex": { + "version": "8.0.0", + "bundled": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "bundled": true + }, + "fast-uri": { + "version": "3.1.0", + "bundled": true + }, + "fs-extra": { + "version": "11.3.3", + "bundled": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "graceful-fs": { + "version": "4.2.11", + "bundled": true + }, + "ignore": { + "version": "5.3.2", + "bundled": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "bundled": true + }, + "json-schema-traverse": { + "version": "1.0.0", + "bundled": true + }, + "jsonfile": { + "version": "6.2.0", + "bundled": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "jsonschema": { + "version": "1.5.0", + "bundled": true + }, + "lodash.truncate": { + "version": "4.4.2", + "bundled": true + }, + "mime-db": { + "version": "1.52.0", + "bundled": true + }, + "mime-types": { + "version": "2.1.35", + "bundled": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "10.2.5", + "bundled": true, + "requires": { + "brace-expansion": "^5.0.5" + } + }, + "punycode": { + "version": "2.3.1", + "bundled": true + }, + "require-from-string": { + "version": "2.0.2", + "bundled": true + }, + "semver": { + "version": "7.7.4", + "bundled": true + }, + "slice-ansi": { + "version": "4.0.0", + "bundled": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, + "string-width": { + "version": "4.2.3", + "bundled": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "bundled": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "table": { + "version": "6.9.0", + "bundled": true, + "requires": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + } + }, + "universalify": { + "version": "2.0.1", + "bundled": true + }, + "yaml": { + "version": "1.10.3", + "bundled": true + } + } + }, + "babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "requires": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "dependencies": { + "istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + } + } + } + }, + "babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + } + }, + "babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "browserslist": { + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + } + }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001662", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001662.tgz", + "integrity": "sha512-sgMUVwLmGseH8ZIrm1d51UbrhqMCH3jvS7gF/M6byuHOnKyLOBL7W8yz5V02OHwgLGA36o/AFhWzzh4uc5aqTA==", + "dev": true + }, + "cargo-lambda-cdk": { + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/cargo-lambda-cdk/-/cargo-lambda-cdk-0.0.35.tgz", + "integrity": "sha512-989zAeN/SpXhE1BmjtCuLmTX6nC9qm4CmlVFFJO6ob368w9dZBamlI3jouc+NAX+yW8vbYOih8LUkUIXCWF6VA==", + "requires": { + "js-toml": "^1.0.2" + }, + "dependencies": { + "@babel/runtime-corejs3": { + "version": "7.26.0", + "bundled": true, + "requires": { + "core-js-pure": "^3.30.2", + "regenerator-runtime": "^0.14.0" + } + }, + "@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "bundled": true, + "requires": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "@chevrotain/gast": { + "version": "11.0.3", + "bundled": true, + "requires": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "bundled": true + }, + "@chevrotain/types": { + "version": "11.0.3", + "bundled": true + }, + "@chevrotain/utils": { + "version": "11.0.3", + "bundled": true + }, + "chevrotain": { + "version": "11.0.3", + "bundled": true, + "requires": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "core-js-pure": { + "version": "3.39.0", + "bundled": true + }, + "js-toml": { + "version": "1.0.2", + "bundled": true, + "requires": { + "chevrotain": "^11.0.3", + "xregexp": "^5.1.1" + } + }, + "lodash-es": { + "version": "4.17.21", + "bundled": true + }, + "regenerator-runtime": { + "version": "0.14.1", + "bundled": true + }, + "xregexp": { + "version": "5.1.1", + "bundled": true, + "requires": { + "@babel/runtime-corejs3": "^7.16.5" + } + } + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true + }, + "ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true + }, + "cjs-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", + "dev": true + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "constructs": { + "version": "10.5.1", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.5.1.tgz", + "integrity": "sha512-f/TfFXiS3G/yVIXDjOQn9oTlyu9Wo7Fxyjj7lb8r92iO81jR2uST+9MstxZTmDGx/CgIbxCXkFXgupnLTNxQZg==" + }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + } + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "requires": {} + }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, + "ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "requires": { + "jake": "^10.8.5" + } + }, + "electron-to-chromium": { + "version": "1.5.27", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.27.tgz", + "integrity": "sha512-o37j1vZqCoEgBuWWXLHQgTN/KDKe7zwpiY5CPeq2RvUqOyJw9xnrULzZAEVQ5p4h+zjMk7hgtOoPdnLxr7m/jw==", + "dev": true + }, + "emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true + }, + "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==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true + }, + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "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" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true + }, + "expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "requires": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, + "filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "requires": { + "minimatch": "^5.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "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 + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "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 + }, + "import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "requires": { + "hasown": "^2.0.2" + } + }, + "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==", + "dev": true + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "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 + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "requires": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "dependencies": { + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, + "istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "requires": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + } + }, + "jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + } + }, + "jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "requires": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + } + }, + "jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + } + }, + "jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + } + }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + } + }, + "jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + } + }, + "jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + } + }, + "jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "requires": {} + }, + "jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true + }, + "jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + } + }, + "jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "requires": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + } + }, + "jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "dependencies": { + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + } + } + }, + "jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + } + }, + "jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "dependencies": { + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true + } + } + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + } + } + }, + "jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + } + }, + "jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + }, + "dependencies": { + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true + } + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "requires": { + "tmpl": "1.0.5" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "requires": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + } + }, + "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 + }, + "minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "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, + "requires": { + "path-key": "^3.0.0" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true + }, + "picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true + }, + "pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, + "pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true + }, + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "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 + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + } + }, + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "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 + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "ts-jest": { + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "dev": true, + "requires": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" + }, + "dependencies": { + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true + } + } + }, + "ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + }, + "typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "dev": true, + "requires": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + } + }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + } + }, + "walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "requires": { + "makeerror": "1.0.12" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/examples/advanced-appconfig-feature-flags/cdk/package.json b/examples/advanced-appconfig-feature-flags/cdk/package.json new file mode 100644 index 000000000..0f590c21f --- /dev/null +++ b/examples/advanced-appconfig-feature-flags/cdk/package.json @@ -0,0 +1,28 @@ +{ + "name": "cdk", + "version": "0.1.0", + "bin": { + "cdk": "bin/cdk.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk" + }, + "devDependencies": { + "@types/jest": "^29.5.12", + "@types/node": "20.14.2", + "aws-cdk": "2.159.1", + "jest": "^29.7.0", + "ts-jest": "^29.1.4", + "ts-node": "^10.9.2", + "typescript": "~5.4.5" + }, + "dependencies": { + "aws-cdk-lib": "2.253.0", + "cargo-lambda-cdk": "^0.0.35", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + } +} \ No newline at end of file diff --git a/examples/advanced-appconfig-feature-flags/cdk/test/cdk.test.ts b/examples/advanced-appconfig-feature-flags/cdk/test/cdk.test.ts new file mode 100644 index 000000000..1e6b29c85 --- /dev/null +++ b/examples/advanced-appconfig-feature-flags/cdk/test/cdk.test.ts @@ -0,0 +1,17 @@ +// import * as cdk from 'aws-cdk-lib'; +// import { Template } from 'aws-cdk-lib/assertions'; +// import * as Cdk from '../lib/cdk-stack'; + +// example test. To run these tests, uncomment this file along with the +// example resource in lib/cdk-stack.ts +test('SQS Queue Created', () => { +// const app = new cdk.App(); +// // WHEN +// const stack = new Cdk.CdkStack(app, 'MyTestStack'); +// // THEN +// const template = Template.fromStack(stack); + +// template.hasResourceProperties('AWS::SQS::Queue', { +// VisibilityTimeout: 300 +// }); +}); diff --git a/examples/advanced-appconfig-feature-flags/cdk/tsconfig.json b/examples/advanced-appconfig-feature-flags/cdk/tsconfig.json new file mode 100644 index 000000000..aaa7dc510 --- /dev/null +++ b/examples/advanced-appconfig-feature-flags/cdk/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "es2020", + "dom" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +} diff --git a/examples/advanced-appconfig-feature-flags/src/appconfig.rs b/examples/advanced-appconfig-feature-flags/src/appconfig.rs new file mode 100644 index 000000000..9dba66107 --- /dev/null +++ b/examples/advanced-appconfig-feature-flags/src/appconfig.rs @@ -0,0 +1,88 @@ +//! # Rust AppConfig Client +//! +//! This library provides a Rust client for interacting with the AWS AppConfig local extension for AWS Lambda and ECS. +//! It allows you to retrieve configuration data for your application based on the application ID, environment ID, +//! and configuration profile ID. +//! +//! ## Features +//! +//! - Simple API for retrieving configuration data +//! - Asynchronous operations using `tokio` and `reqwest` +//! - Error handling with custom `AppConfigError` type +//! - Deserialization of configuration data into user-defined types +//! +//! ## Usage +//! +//! ```rust +//! use appconfig::{AppConfigClient, ConfigurationFetcher, AppConfigError}; +//! +//! #[tokio::main] +//! async fn main() -> Result<(), AppConfigError> { +//! let client = AppConfigClient::new("app_id", "env_id", "profile_id", 2772); +//! +//! let config: YourConfigType = client.get_configuration().await?; +//! +//! println!("Received config: {:?}", config); +//! +//! Ok(()) +//! } +//! ``` +use async_trait::async_trait; +use reqwest::Client; +use serde::de::DeserializeOwned; +use thiserror::Error; + +#[derive(Clone)] +pub struct AppConfigClient { + client: Client, + application_id: String, + environment_id: String, + configuration_profile_id: String, + port: u16, +} + +impl AppConfigClient { + pub fn new(application_id: &str, environment_id: &str, configuration_profile_id: &str, port: u16) -> Self { + AppConfigClient { + client: Client::new(), + application_id: application_id.to_string(), + environment_id: environment_id.to_string(), + configuration_profile_id: configuration_profile_id.to_string(), + port, + } + } +} + +#[async_trait] +impl ConfigurationFetcher for AppConfigClient { + async fn get_configuration(&self) -> Result + where + T: DeserializeOwned + Send, + { + let url = format!( + "http://localhost:{}/applications/{}/environments/{}/configurations/{}", + self.port, self.application_id, self.environment_id, self.configuration_profile_id + ); + + let response = self.client.get(&url).send().await?; + let config: T = response.json().await?; + + Ok(config) + } +} + +#[derive(Error, Debug)] +pub enum AppConfigError { + #[error("Failed to send request: {0}")] + RequestError(#[from] reqwest::Error), + + #[error("Failed to parse JSON: {0}")] + JsonParseError(#[from] serde_json::Error), +} + +#[async_trait] +pub trait ConfigurationFetcher { + async fn get_configuration(&self) -> Result + where + T: DeserializeOwned + Send; +} diff --git a/examples/advanced-appconfig-feature-flags/src/main.rs b/examples/advanced-appconfig-feature-flags/src/main.rs new file mode 100644 index 000000000..87ec54fac --- /dev/null +++ b/examples/advanced-appconfig-feature-flags/src/main.rs @@ -0,0 +1,126 @@ +use lambda_runtime::{run, service_fn, tracing, Error, LambdaEvent}; +use serde::{Deserialize, Serialize}; +use std::env; + +mod appconfig; +use crate::appconfig::{AppConfigClient, ConfigurationFetcher}; + +#[derive(Deserialize)] +struct Request { + quote: String, +} + +#[derive(Serialize)] +struct Response { + req_id: String, + msg: String, +} + +#[derive(Deserialize)] +struct AppConfig { + #[serde(rename = "spanish-response")] + spanish_response: bool, + // Add other fields as needed +} + +async fn function_handler( + event: LambdaEvent, + config_fetcher: &T, +) -> Result { + // Extract some useful info from the request + let quote = event.payload.quote; + + // Send a GET request to the local AppConfig endpoint + let config: AppConfig = config_fetcher.get_configuration().await?; + + // Use the feature flag + let msg = if config.spanish_response { + format!("{quote}, in spanish.") + } else { + format!("{quote}.") + }; + + // Return `Response` (it will be serialized to JSON automatically by the runtime) + Ok(Response { + req_id: event.context.request_id, + msg, + }) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing::init_default_subscriber(); + + // Extract the AppConfig port from the environment + let app_config_port = env::var("AWS_APPCONFIG_EXTENSION_HTTP_PORT") + .unwrap_or_else(|_| "2772".to_string()) + .parse::() + .expect("Invalid port number for AWS_APPCONFIG_EXTENSION_HTTP_PORT"); + + // Create a new AppConfigClient with the extracted port + let app_config_client = AppConfigClient::new( + &env::var("APPLICATION_ID").expect("APPLICATION_ID must be set"), + &env::var("ENVIRONMENT_ID").expect("ENVIRONMENT_ID must be set"), + &env::var("CONFIGURATION_PROFILE_ID").expect("CONFIGURATION_PROFILE_ID must be set"), + app_config_port, + ); + + // Use a closure to capture app_config_client and pass it to function_handler + run(service_fn(|event| function_handler(event, &app_config_client))).await +} + +#[cfg(test)] +mod tests { + use super::*; + use async_trait::async_trait; + use lambda_runtime::Context; + use serde::de::DeserializeOwned; + + struct MockConfigFetcher { + spanish_response: bool, + } + + #[async_trait] + impl ConfigurationFetcher for MockConfigFetcher { + async fn get_configuration(&self) -> Result + where + T: DeserializeOwned + Send, + { + let value = serde_json::json!({ + "spanish-response": self.spanish_response + }); + let value: T = serde_json::from_value(value)?; + Ok(value) + } + } + + #[tokio::test] + async fn test_function_handler_english() { + let mock_fetcher = MockConfigFetcher { + spanish_response: false, + }; + let request = Request { + quote: "Hello, world".to_string(), + }; + let context = Context::default(); + let event = LambdaEvent::new(request, context); + + let result = function_handler(event, &mock_fetcher).await.unwrap(); + + assert_eq!(result.msg, "Hello, world."); + } + + #[tokio::test] + async fn test_function_handler_spanish() { + let mock_fetcher = MockConfigFetcher { spanish_response: true }; + let request = Request { + quote: "Hello, world".to_string(), + }; + let context = Context::default(); + let event = LambdaEvent::new(request, context); + + let result = function_handler(event, &mock_fetcher).await.unwrap(); + + assert_eq!(result.msg, "Hello, world, in spanish."); + } +} diff --git a/examples/advanced-sqs-multiple-functions-shared-data/Cargo.toml b/examples/advanced-sqs-multiple-functions-shared-data/Cargo.toml new file mode 100644 index 000000000..bd41a01a0 --- /dev/null +++ b/examples/advanced-sqs-multiple-functions-shared-data/Cargo.toml @@ -0,0 +1,14 @@ +[workspace] +resolver = "2" + +members = [ + "producer", + "consumer", + "pizza_lib", +] + +[profile.release] +opt-level = 'z' +lto = true +codegen-units = 1 +panic = 'abort' \ No newline at end of file diff --git a/examples/advanced-sqs-multiple-functions-shared-data/README.md b/examples/advanced-sqs-multiple-functions-shared-data/README.md new file mode 100644 index 000000000..83136b9b8 --- /dev/null +++ b/examples/advanced-sqs-multiple-functions-shared-data/README.md @@ -0,0 +1,28 @@ +# AWS Lambda Function example + +## Build & Deploy + +1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the function with `cargo lambda build --release` +4. Make sure to edit the QUEUE_URL env variable in producer/Cargo.toml +3. Deploy boths functions to AWS Lambda with + +`cargo lambda deploy consumer --iam-role YOUR_ROLE` + +`cargo lambda deploy producer --iam-role YOUR_ROLE` + +## Build for ARM 64 + +Build the function with `cargo lambda build --release --arm64` + +## Add the SQS trigger to the consumer function + +You can use aws-cli to create an event source mapping: + +```bash +aws lambda create-event-source-mapping \ +--function-name consumer \ +--region \ +--event-source-arn \ +--batch-size 1 +``` \ No newline at end of file diff --git a/examples/advanced-sqs-multiple-functions-shared-data/consumer/Cargo.toml b/examples/advanced-sqs-multiple-functions-shared-data/consumer/Cargo.toml new file mode 100644 index 000000000..e82dc1d34 --- /dev/null +++ b/examples/advanced-sqs-multiple-functions-shared-data/consumer/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "consumer" +version = "0.1.0" +edition = "2021" + +[dependencies] +#aws dependencies +aws_lambda_events = { path = "../../../lambda-events", features = ["sqs"], default-features = false } + +#lambda runtime +lambda_runtime = { path = "../../../lambda-runtime" } +tokio = { version = "1", features = ["macros"] } + +#shared lib +pizza_lib = { path = "../pizza_lib" } diff --git a/examples/advanced-sqs-multiple-functions-shared-data/consumer/src/main.rs b/examples/advanced-sqs-multiple-functions-shared-data/consumer/src/main.rs new file mode 100644 index 000000000..c076a502f --- /dev/null +++ b/examples/advanced-sqs-multiple-functions-shared-data/consumer/src/main.rs @@ -0,0 +1,19 @@ +use aws_lambda_events::event::sqs::SqsEventObj; +use lambda_runtime::{service_fn, tracing, Error, LambdaEvent}; +use pizza_lib::Pizza; + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing::init_default_subscriber(); + let func = service_fn(func); + lambda_runtime::run(func).await?; + Ok(()) +} + +async fn func(event: LambdaEvent>) -> Result<(), Error> { + for record in event.payload.records.iter() { + let pizza = &record.body; + tracing::info!(name = pizza.name, toppings = ?pizza.toppings, "pizza order received"); + } + Ok(()) +} diff --git a/examples/advanced-sqs-multiple-functions-shared-data/pizza_lib/Cargo.toml b/examples/advanced-sqs-multiple-functions-shared-data/pizza_lib/Cargo.toml new file mode 100644 index 000000000..2dd69db13 --- /dev/null +++ b/examples/advanced-sqs-multiple-functions-shared-data/pizza_lib/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "pizza_lib" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0.219", features = ["derive"] } diff --git a/examples/advanced-sqs-multiple-functions-shared-data/pizza_lib/src/lib.rs b/examples/advanced-sqs-multiple-functions-shared-data/pizza_lib/src/lib.rs new file mode 100644 index 000000000..638fa762d --- /dev/null +++ b/examples/advanced-sqs-multiple-functions-shared-data/pizza_lib/src/lib.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct Pizza { + pub name: String, + pub toppings: Vec, +} diff --git a/examples/advanced-sqs-multiple-functions-shared-data/producer/Cargo.toml b/examples/advanced-sqs-multiple-functions-shared-data/producer/Cargo.toml new file mode 100644 index 000000000..2772f6500 --- /dev/null +++ b/examples/advanced-sqs-multiple-functions-shared-data/producer/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "producer" +version = "0.1.0" +edition = "2021" + +[package.metadata.lambda.deploy] +env = { "QUEUE_URL" = "https://changeMe" } + +[dependencies] +#aws dependencies +aws-config = { version = "1.8.1", features = ["behavior-version-latest"] } +aws-sdk-sqs = "1.74.0" + +#lambda runtime +lambda_runtime = { path = "../../../lambda-runtime" } +serde_json = "1.0.140" +tokio = { version = "1", features = ["macros"] } + +#shared lib +pizza_lib = { path = "../pizza_lib" } diff --git a/examples/advanced-sqs-multiple-functions-shared-data/producer/src/main.rs b/examples/advanced-sqs-multiple-functions-shared-data/producer/src/main.rs new file mode 100644 index 000000000..6a2883f32 --- /dev/null +++ b/examples/advanced-sqs-multiple-functions-shared-data/producer/src/main.rs @@ -0,0 +1,54 @@ +use lambda_runtime::{service_fn, tracing, Error, LambdaEvent}; +use pizza_lib::Pizza; +use serde_json::{json, Value}; + +struct SQSManager { + client: aws_sdk_sqs::Client, + queue_url: String, +} + +impl SQSManager { + fn new(client: aws_sdk_sqs::Client, queue_url: String) -> Self { + Self { client, queue_url } + } +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing::init_default_subscriber(); + + // read the queue url from the environment + let queue_url = std::env::var("QUEUE_URL").expect("could not read QUEUE_URL"); + // build the config from environment variables (fed by AWS Lambda) + let config = aws_config::load_from_env().await; + // create our SQS Manager + let sqs_manager = SQSManager::new(aws_sdk_sqs::Client::new(&config), queue_url); + let sqs_manager_ref = &sqs_manager; + + // no need to create a SQS Client for each incoming request, let's use a shared state + let handler_func_closure = |event: LambdaEvent| async move { process_event(event, sqs_manager_ref).await }; + lambda_runtime::run(service_fn(handler_func_closure)).await?; + Ok(()) +} + +async fn process_event(_: LambdaEvent, sqs_manager: &SQSManager) -> Result<(), Error> { + // let's create our pizza + let message = Pizza { + name: "margherita".to_string(), + toppings: vec![ + "San Marzano Tomatoes".to_string(), + "Fresh Mozzarella".to_string(), + "Basil".to_string(), + ], + }; + // send our message to SQS + sqs_manager + .client + .send_message() + .queue_url(&sqs_manager.queue_url) + .message_body(json!(message).to_string()) + .send() + .await?; + + Ok(()) +} diff --git a/examples/advanced-sqs-partial-batch-failures/Cargo.toml b/examples/advanced-sqs-partial-batch-failures/Cargo.toml new file mode 100644 index 000000000..f02e4efb3 --- /dev/null +++ b/examples/advanced-sqs-partial-batch-failures/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "advanced-sqs-partial-batch-failures" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = "^1" +serde_json = "^1" +aws_lambda_events = { path = "../../lambda-events" } +lambda_runtime = { path = "../../lambda-runtime" } +tokio = { version = "1", features = ["macros"] } +futures = "0.3" diff --git a/examples/advanced-sqs-partial-batch-failures/README.md b/examples/advanced-sqs-partial-batch-failures/README.md new file mode 100644 index 000000000..9711db8ba --- /dev/null +++ b/examples/advanced-sqs-partial-batch-failures/README.md @@ -0,0 +1,16 @@ +# AWS Lambda Function that receives events from SQS + +This example shows how to process events from an SQS queue using the partial batch failure feature. + +_Important note:_ your lambda sqs trigger _needs_ to be configured with partial batch response support +(the `ReportBatchItemFailures` flag set to true), otherwise failed message will be not be reprocessed. You may see more details [here](https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#services-sqs-batchfailurereporting). + +## Build & Deploy + +1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the function with `cargo lambda build --release` +3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` + +## Build for ARM 64 + +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/advanced-sqs-partial-batch-failures/src/main.rs b/examples/advanced-sqs-partial-batch-failures/src/main.rs new file mode 100644 index 000000000..4af67b8ce --- /dev/null +++ b/examples/advanced-sqs-partial-batch-failures/src/main.rs @@ -0,0 +1,161 @@ +use aws_lambda_events::{ + event::sqs::SqsEventObj, + sqs::{BatchItemFailure, SqsBatchResponse, SqsMessageObj}, +}; +use futures::Future; +use lambda_runtime::{ + run, service_fn, + tracing::{self, Instrument}, + Error, LambdaEvent, +}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +/// [To customize] Your object definition, sent to the SQS queue triggering this lambda. +#[derive(Deserialize, Serialize)] +struct Data { + text: String, +} + +/// [To customize] Your buisness logic to handle the payload of one SQS message. +async fn data_handler(data: Data) -> Result<(), Error> { + // Some processing + tracing::info!(text = ?data.text, "processing data"); + // simulate error + if data.text == "bad request" { + Err("Processing error".into()) + } else { + Ok(()) + } +} + +/// Main function for the lambda executable. +#[tokio::main] +async fn main() -> Result<(), Error> { + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); + + run_sqs_partial_batch_failure(data_handler).await +} + +/// This function will handle the message batches from SQS. +/// It calls the provided user function `f` on every message concurrently and reports to SQS +/// which message failed to be processed so that only those are retried. +/// +/// Important note: your lambda sqs trigger *needs* to be configured with partial batch response support +/// with the ` ReportBatchItemFailures` flag set to true, otherwise failed message will be dropped, +/// for more details see: +/// +/// +/// +/// Note that if you are looking for parallel processing (multithread) instead of concurrent processing, +/// you can do so by spawning a task inside your function `f`. +async fn run_sqs_partial_batch_failure(f: T) -> Result<(), Error> +where + T: Fn(D) -> R, + D: DeserializeOwned, + R: Future>, +{ + run(service_fn(|e| batch_handler(&f, e))).await +} + +/// Helper function to lift the user provided `f` function from message to batch of messages. +/// See `run_sqs` for the easier function to use. +async fn batch_handler( + f: T, + event: LambdaEvent>, +) -> Result +where + T: Fn(D) -> F, + F: Future>, + D: DeserializeOwned, +{ + tracing::trace!("Handling batch size {}", event.payload.records.len()); + let create_task = |msg| { + // We need to keep the message_id to report failures to SQS + let SqsMessageObj { message_id, body, .. } = msg; + let span = tracing::span!(tracing::Level::INFO, "Handling SQS msg", message_id); + let task = async { + //TODO catch panics like the `run` function from lambda_runtime + f(serde_json::from_value(body)?).await + } + .instrument(span); + (message_id.unwrap_or_default(), task) + }; + let (ids, tasks): (Vec<_>, Vec<_>) = event.payload.records.into_iter().map(create_task).unzip(); + let results = futures::future::join_all(tasks).await; // Run tasks concurrently + let failure_items = ids + .into_iter() + .zip(results) + .filter_map( + // Only keep the message_id of failed tasks + |(id, res)| match res { + Ok(()) => None, + Err(err) => { + tracing::error!("Failed to process msg {id}, {err}"); + Some(id) + } + }, + ) + .map(|id| { + let mut failure_item = BatchItemFailure::default(); + failure_item.item_identifier = id; + failure_item + }) + .collect(); + + Ok({ + let mut response = SqsBatchResponse::default(); + response.batch_item_failures = failure_items; + response + }) +} + +#[cfg(test)] +mod test { + use lambda_runtime::Context; + + use super::*; + + #[derive(Serialize, Deserialize, Debug)] + struct UserData { + should_error: bool, + } + async fn user_fn(data: UserData) -> Result<(), Error> { + if data.should_error { + Err("Processing Error".into()) + } else { + Ok(()) + } + } + + #[tokio::test] + async fn test() { + let msg_to_fail: SqsMessageObj = serde_json::from_str( + r#"{ + "messageId": "1", + "body": "{\"should_error\": true}" + }"#, + ) + .unwrap(); + let msg_to_succeed: SqsMessageObj = serde_json::from_str( + r#"{ + "messageId": "0", + "body": "{\"should_error\" : false}" + }"#, + ) + .unwrap(); + + let lambda_event = LambdaEvent { + payload: { + let mut event_object = SqsEventObj::default(); + event_object.records = vec![msg_to_fail, msg_to_succeed]; + event_object + }, + context: Context::default(), + }; + + let r = batch_handler(user_fn, lambda_event).await.unwrap(); + assert_eq!(r.batch_item_failures.len(), 1); + assert_eq!(r.batch_item_failures[0].item_identifier, "1"); + } +} diff --git a/examples/basic-cognito-post-confirmation/.gitignore b/examples/basic-cognito-post-confirmation/.gitignore new file mode 100644 index 000000000..c41cc9e35 --- /dev/null +++ b/examples/basic-cognito-post-confirmation/.gitignore @@ -0,0 +1 @@ +/target \ No newline at end of file diff --git a/examples/basic-cognito-post-confirmation/Cargo.toml b/examples/basic-cognito-post-confirmation/Cargo.toml new file mode 100644 index 000000000..93369e515 --- /dev/null +++ b/examples/basic-cognito-post-confirmation/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "basic-cognito-post-confirmation" +version = "0.1.0" +edition = "2021" + +# Starting in Rust 1.62 you can use `cargo add` to add dependencies +# to your project. +# +# If you're using an older Rust version, +# download cargo-edit(https://github.com/killercup/cargo-edit#installation) +# to install the `add` subcommand. +# +# Running `cargo add DEPENDENCY_NAME` will +# add the latest version of a dependency to the list, +# and it will keep the alphabetic ordering for you. + +[dependencies] +aws-config = { version = "1.8.1", features = ["behavior-version-latest"] } +aws-sdk-ses = "1.77.0" +aws_lambda_events = { path = "../../lambda-events", default-features = false, features = ["cognito"] } + +lambda_runtime = { path = "../../lambda-runtime" } +tokio = { version = "1", features = ["macros"] } + diff --git a/examples/basic-cognito-post-confirmation/README.md b/examples/basic-cognito-post-confirmation/README.md new file mode 100644 index 000000000..4391e16c3 --- /dev/null +++ b/examples/basic-cognito-post-confirmation/README.md @@ -0,0 +1,15 @@ +# Cognito Post Confirmation Request example + +This example shows how to write a Lambda function in Rust to process Cognito's Post Confirmation requests. + +This is a translation of the example in the AWS Docs to Rust: https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-post-confirmation.html#aws-lambda-triggers-post-confirmation-example + +## Build & Deploy + +1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the function with `cargo lambda build --release` +3. Deploy the function to AWS Lambda with `cargo lambda deploy` + +## Build for ARM 64 + +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/basic-cognito-post-confirmation/src/main.rs b/examples/basic-cognito-post-confirmation/src/main.rs new file mode 100644 index 000000000..178bd3a95 --- /dev/null +++ b/examples/basic-cognito-post-confirmation/src/main.rs @@ -0,0 +1,60 @@ +use aws_config::BehaviorVersion; +use aws_lambda_events::event::cognito::CognitoEventUserPoolsPostConfirmation; +use aws_sdk_ses::{ + types::{Body, Content, Destination, Message}, + Client, +}; +use lambda_runtime::{run, service_fn, tracing, Error, LambdaEvent}; + +const SOURCE_EMAIL: &str = ""; + +async fn function_handler( + client: &aws_sdk_ses::Client, + event: LambdaEvent, +) -> Result { + let payload = event.payload; + + if let Some(email) = payload.request.user_attributes.get("email") { + let body = if let Some(name) = payload.request.user_attributes.get("name") { + format!("Welcome {name}, you have been confirmed.") + } else { + "Welcome, you have been confirmed.".to_string() + }; + send_post_confirmation_email(client, email, "Cognito Identity Provider registration completed", &body).await?; + } + + // Cognito always expect a response with the same shape as + // the event when it handles Post Confirmation triggers. + Ok(payload) +} + +async fn send_post_confirmation_email(client: &Client, email: &str, subject: &str, body: &str) -> Result<(), Error> { + let destination = Destination::builder().to_addresses(email).build(); + let subject = Content::builder().data(subject).build()?; + let body = Content::builder().data(body).build()?; + + let message = Message::builder() + .body(Body::builder().text(body).build()) + .subject(subject) + .build(); + + client + .send_email() + .source(SOURCE_EMAIL) + .destination(destination) + .message(message) + .send() + .await?; + + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing::init_default_subscriber(); + + let config = aws_config::load_defaults(BehaviorVersion::latest()).await; + let client = Client::new(&config); + + run(service_fn(|event| function_handler(&client, event))).await +} diff --git a/examples/basic-error-error-crates-integration/.gitignore b/examples/basic-error-error-crates-integration/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/examples/basic-error-error-crates-integration/.gitignore @@ -0,0 +1 @@ +/target diff --git a/examples/basic-error-error-crates-integration/Cargo.toml b/examples/basic-error-error-crates-integration/Cargo.toml new file mode 100644 index 000000000..24fbc8dca --- /dev/null +++ b/examples/basic-error-error-crates-integration/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "basic-error-error-crates-integration" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +eyre = "0.6.12" +lambda_runtime = { path = "../../lambda-runtime", features = ["anyhow", "eyre", "miette"] } +miette = "7.6.0" +serde = "1" +tokio = { version = "1", features = ["macros"] } diff --git a/examples/basic-error-error-crates-integration/README.md b/examples/basic-error-error-crates-integration/README.md new file mode 100644 index 000000000..2e46c55db --- /dev/null +++ b/examples/basic-error-error-crates-integration/README.md @@ -0,0 +1,15 @@ +# AWS Lambda Function Error Handling with several popular error crates. + +This example shows how to use external error types like `anyhow::Error`, `eyre::Report`, and `miette::Report`. + +To use the integrations with these crates, you need to enable to respective feature flag in the runtime which provides the implemetation of `into_diagnostic` for specific error types provided by these crates. + +## Build & Deploy + +1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the function with `cargo lambda build --release` +3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` + +## Build for ARM 64 + +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/basic-error-error-crates-integration/src/main.rs b/examples/basic-error-error-crates-integration/src/main.rs new file mode 100644 index 000000000..176bd54b3 --- /dev/null +++ b/examples/basic-error-error-crates-integration/src/main.rs @@ -0,0 +1,44 @@ +use lambda_runtime::{run, service_fn, Diagnostic, Error, LambdaEvent}; +use serde::Deserialize; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +enum ErrorType { + Anyhow, + Eyre, + Miette, +} + +#[derive(Deserialize)] +struct Request { + error_type: ErrorType, +} + +fn anyhow_error() -> anyhow::Result<()> { + anyhow::bail!("This is an error message from Anyhow"); +} + +fn eyre_error() -> eyre::Result<()> { + eyre::bail!("This is an error message from Eyre"); +} + +fn miette_error() -> miette::Result<()> { + miette::bail!("This is an error message from Miette"); +} + +/// Transform an anyhow::Error, eyre::Report, or miette::Report into a lambda_runtime::Diagnostic. +/// It does it by enabling the feature `anyhow`, `eyre` or `miette` in the runtime dependency. +/// Those features enable the implementation of `From for Diagnostic` +/// for `anyhow::Error`, `eyre::Report`, and `miette::Report`. +async fn function_handler(event: LambdaEvent) -> Result<(), Diagnostic> { + match event.payload.error_type { + ErrorType::Anyhow => anyhow_error().map_err(|e| e.into()), + ErrorType::Eyre => eyre_error().map_err(|e| e.into()), + ErrorType::Miette => miette_error().map_err(|e| e.into()), + } +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + run(service_fn(function_handler)).await +} diff --git a/examples/basic-error-handling/Cargo.toml b/examples/basic-error-handling/Cargo.toml index e8699141d..a0267f978 100644 --- a/examples/basic-error-handling/Cargo.toml +++ b/examples/basic-error-handling/Cargo.toml @@ -3,20 +3,9 @@ name = "error-handling" version = "0.1.0" edition = "2021" - -# Use cargo-edit(https://github.com/killercup/cargo-edit#installation) -# to manage dependencies. -# Running `cargo add DEPENDENCY_NAME` will -# add the latest version of a dependency to the list, -# and it will keep the alphabetic ordering for you. - [dependencies] lambda_runtime = { path = "../../lambda-runtime" } -serde = "1.0.136" -serde_json = "1.0.81" -simple-error = "0.2.3" +serde = "1.0.219" +serde_json = "1.0.140" +simple-error = "0.3.1" tokio = { version = "1", features = ["macros"] } -tracing = { version = "0.1", features = ["log"] } -tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } - - diff --git a/examples/basic-error-handling/README.md b/examples/basic-error-handling/README.md index 5cae85fb9..5eef42077 100644 --- a/examples/basic-error-handling/README.md +++ b/examples/basic-error-handling/README.md @@ -1,11 +1,13 @@ -# AWS Lambda Function example +# AWS Lambda Function Error handling example + +This example shows how to return a custom error type for unexpected failures. ## Build & Deploy 1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) -2. Build the function with `cargo lambda build --release` +2. Build the function with `cargo lambda build --release` 3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` ## Build for ARM 64 -Build the function with `cargo lambda build --release --arm64` +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/basic-error-handling/src/main.rs b/examples/basic-error-handling/src/main.rs index 8d2748498..f434c9039 100644 --- a/examples/basic-error-handling/src/main.rs +++ b/examples/basic-error-handling/src/main.rs @@ -1,7 +1,7 @@ -/// See https://github.com/awslabs/aws-lambda-rust-runtime for more info on Rust runtime for AWS Lambda -use lambda_runtime::{service_fn, Error, LambdaEvent}; +/// See for more info on Rust runtime for AWS Lambda +use lambda_runtime::{service_fn, tracing, Error, LambdaEvent}; use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; +use serde_json::json; use std::fs::File; /// A simple Lambda request structure with just one field @@ -43,19 +43,14 @@ impl std::fmt::Display for CustomError { /// Display the error struct as a JSON string fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let err_as_json = json!(self).to_string(); - write!(f, "{}", err_as_json) + write!(f, "{err_as_json}") } } #[tokio::main] async fn main() -> Result<(), Error> { - // The runtime logging can be enabled here by initializing `tracing` with `tracing-subscriber` - // While `tracing` is used internally, `log` can be used as well if preferred. - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - // disabling time is handy because CloudWatch will add the ingestion time. - .without_time() - .init(); + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); // call the actual handler of the request let func = service_fn(func); @@ -64,14 +59,14 @@ async fn main() -> Result<(), Error> { } /// The actual handler of the Lambda request. -pub(crate) async fn func(event: LambdaEvent) -> Result { +pub(crate) async fn func(event: LambdaEvent) -> Result { let (event, ctx) = event.into_parts(); // check what action was requested - match serde_json::from_value::(event)?.event_type { + match event.event_type { EventType::SimpleError => { // generate a simple text message error using `simple_error` crate - return Err(Box::new(simple_error::SimpleError::new("A simple error as requested!"))); + Err(Box::new(simple_error::SimpleError::new("A simple error as requested!"))) } EventType::CustomError => { // generate a custom error using our own structure @@ -80,7 +75,7 @@ pub(crate) async fn func(event: LambdaEvent) -> Result { req_id: ctx.request_id, msg: "A custom error as requested!".into(), }; - return Err(Box::new(cust_err)); + Err(Box::new(cust_err)) } EventType::ExternalError => { // try to open a non-existent file to get an error and propagate it with `?` @@ -99,7 +94,7 @@ pub(crate) async fn func(event: LambdaEvent) -> Result { msg: "OK".into(), }; - return Ok(json!(resp)); + Ok(resp) } } } diff --git a/examples/basic-error-thiserror/.gitignore b/examples/basic-error-thiserror/.gitignore new file mode 100644 index 000000000..c41cc9e35 --- /dev/null +++ b/examples/basic-error-thiserror/.gitignore @@ -0,0 +1 @@ +/target \ No newline at end of file diff --git a/examples/basic-error-thiserror/Cargo.toml b/examples/basic-error-thiserror/Cargo.toml new file mode 100644 index 000000000..f2b0b4499 --- /dev/null +++ b/examples/basic-error-thiserror/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "basic-error-thiserror" +version = "0.1.0" +edition = "2021" + +# Starting in Rust 1.62 you can use `cargo add` to add dependencies +# to your project. +# +# If you're using an older Rust version, +# download cargo-edit(https://github.com/killercup/cargo-edit#installation) +# to install the `add` subcommand. +# +# Running `cargo add DEPENDENCY_NAME` will +# add the latest version of a dependency to the list, +# and it will keep the alphabetic ordering for you. + +[dependencies] + +lambda_runtime = { path = "../../lambda-runtime" } +serde = "1" +thiserror = "2.0" +tokio = { version = "1", features = ["macros"] } diff --git a/examples/basic-error-thiserror/README.md b/examples/basic-error-thiserror/README.md new file mode 100644 index 000000000..b9bf1827d --- /dev/null +++ b/examples/basic-error-thiserror/README.md @@ -0,0 +1,13 @@ +# AWS Lambda Function Error handling example + +This example shows how to implement the `Diagnostic` trait to return a specific `error_type` in the Lambda error response. If you don't use the `error_type` field, you don't need to implement `Diagnostic`, the type will be generated based on the error type name. + +## Build & Deploy + +1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the function with `cargo lambda build --release` +3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` + +## Build for ARM 64 + +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/basic-error-thiserror/src/main.rs b/examples/basic-error-thiserror/src/main.rs new file mode 100644 index 000000000..2c01b833d --- /dev/null +++ b/examples/basic-error-thiserror/src/main.rs @@ -0,0 +1,36 @@ +use lambda_runtime::{service_fn, Diagnostic, Error, LambdaEvent}; +use serde::Deserialize; + +#[derive(Deserialize)] +struct Request {} + +#[derive(Debug, thiserror::Error)] +pub enum ExecutionError { + #[error("transient database error: {0}")] + DatabaseError(String), + #[error("unexpected error: {0}")] + Unexpected(String), +} + +impl From for Diagnostic { + fn from(value: ExecutionError) -> Diagnostic { + let (error_type, error_message) = match value { + ExecutionError::DatabaseError(err) => ("Retryable", err.to_string()), + ExecutionError::Unexpected(err) => ("NonRetryable", err.to_string()), + }; + Diagnostic { + error_type: error_type.into(), + error_message, + } + } +} + +/// This is the main body for the Lambda function +async fn function_handler(_event: LambdaEvent) -> Result<(), ExecutionError> { + Err(ExecutionError::Unexpected("ooops".to_string())) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + lambda_runtime::run(service_fn(function_handler)).await +} diff --git a/examples/basic-lambda-concurrent-s3/Cargo.toml b/examples/basic-lambda-concurrent-s3/Cargo.toml new file mode 100644 index 000000000..6678c18b0 --- /dev/null +++ b/examples/basic-lambda-concurrent-s3/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "basic-lambda-concurrent-s3" +version = "0.1.0" +edition = "2021" + +[dependencies] +aws-config = { version = "1", features = ["behavior-version-latest"] } +aws-sdk-s3 = "1" +lambda_runtime = { path = "../../lambda-runtime", features = ["concurrency-tokio"] } +serde = { version = "1", features = ["derive"] } +tokio = { version = "1", features = ["macros"] } diff --git a/examples/basic-lambda-concurrent-s3/src/main.rs b/examples/basic-lambda-concurrent-s3/src/main.rs new file mode 100644 index 000000000..ee8a07c4e --- /dev/null +++ b/examples/basic-lambda-concurrent-s3/src/main.rs @@ -0,0 +1,38 @@ +use aws_config::BehaviorVersion; +use lambda_runtime::{service_fn, Error, LambdaEvent}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize)] +struct Request { + bucket: String, + key: String, + body: String, +} + +#[derive(Serialize)] +struct Response { + message: String, +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + let config = aws_config::load_defaults(BehaviorVersion::latest()).await; + let s3_client = aws_sdk_s3::Client::new(&config); + + lambda_runtime::run_concurrent(service_fn(move |event: LambdaEvent| { + let s3_client = s3_client.clone(); // cheap clone, no Arc needed + async move { + s3_client + .put_object() + .bucket(&event.payload.bucket) + .key(&event.payload.key) + .body(event.payload.body.into_bytes().into()) + .send() + .await?; + Ok::(Response { + message: "uploaded".into(), + }) + } + })) + .await +} diff --git a/examples/basic-lambda-concurrent/Cargo.toml b/examples/basic-lambda-concurrent/Cargo.toml new file mode 100644 index 000000000..7c770aead --- /dev/null +++ b/examples/basic-lambda-concurrent/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "basic-lambda-concurrent" +version = "0.1.0" +edition = "2021" + +[dependencies] +lambda_runtime = { path = "../../lambda-runtime", features = ["concurrency-tokio"] } +serde = "1.0.219" +tokio = { version = "1", features = ["macros"] } diff --git a/examples/basic-lambda-concurrent/src/main.rs b/examples/basic-lambda-concurrent/src/main.rs new file mode 100644 index 000000000..0b106b961 --- /dev/null +++ b/examples/basic-lambda-concurrent/src/main.rs @@ -0,0 +1,89 @@ +// This example requires the following input to succeed: +// { "command": "do something" } + +use lambda_runtime::{service_fn, tracing, Diagnostic, Error, LambdaEvent}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize)] +struct Request { + command: String, +} + +#[derive(Serialize)] +struct Response { + req_id: String, + msg: String, +} + +#[derive(Debug)] +struct HandlerError(String); + +impl std::fmt::Display for HandlerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for Diagnostic { + fn from(e: HandlerError) -> Diagnostic { + Diagnostic { + error_type: "HandlerError".into(), + error_message: e.0, + } + } +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); + + let max_concurrency = std::env::var("AWS_LAMBDA_MAX_CONCURRENCY").unwrap_or_else(|_| "not set".to_string()); + tracing::info!(AWS_LAMBDA_MAX_CONCURRENCY = %max_concurrency, "starting concurrent handler"); + + let func = service_fn(my_handler); + if let Err(err) = lambda_runtime::run_concurrent(func).await { + tracing::error!(error = %err, "run error"); + return Err(err); + } + Ok(()) +} + +pub(crate) async fn my_handler(event: LambdaEvent) -> Result { + let command = event.payload.command; + + if command == "fail" { + return Err(HandlerError("simulated handler error".into())); + } + + let resp = Response { + req_id: event.context.request_id, + msg: format!("Command {command} executed."), + }; + + Ok(resp) +} + +#[cfg(test)] +mod tests { + use crate::{my_handler, Request}; + use lambda_runtime::{Context, LambdaEvent}; + + #[tokio::test] + async fn response_is_good_for_simple_input() { + let id = "ID"; + + let mut context = Context::default(); + context.request_id = id.to_string(); + + let payload = Request { + command: "X".to_string(), + }; + let event = LambdaEvent { payload, context }; + + let result = my_handler(event).await.unwrap(); + + assert_eq!(result.msg, "Command X executed."); + assert_eq!(result.req_id, id.to_string()); + } +} diff --git a/examples/basic-lambda-external-runtime/Cargo.toml b/examples/basic-lambda-external-runtime/Cargo.toml new file mode 100644 index 000000000..40f24d81b --- /dev/null +++ b/examples/basic-lambda-external-runtime/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "basic-lambda-external-runtime" +version = "0.1.0" +edition = "2021" + +[dependencies] +async-channel = "2.5.0" +futures-lite = "2.6.0" +lambda_runtime = { path = "../../lambda-runtime" } +serde = "1.0.219" +tokio = "1.46.1" diff --git a/examples/basic-lambda-external-runtime/README.md b/examples/basic-lambda-external-runtime/README.md new file mode 100644 index 000000000..498f8a500 --- /dev/null +++ b/examples/basic-lambda-external-runtime/README.md @@ -0,0 +1,11 @@ +# AWS Lambda Function example + +## Build & Deploy + +1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the function with `cargo lambda build --release` +3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` + +## Build for ARM 64 + +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/basic-lambda-external-runtime/src/main.rs b/examples/basic-lambda-external-runtime/src/main.rs new file mode 100644 index 000000000..87891ebc4 --- /dev/null +++ b/examples/basic-lambda-external-runtime/src/main.rs @@ -0,0 +1,95 @@ +use std::{io, thread}; + +use futures_lite::future; +use lambda_runtime::{service_fn, tracing, Error, LambdaEvent}; +use serde::{Deserialize, Serialize}; +use tokio::runtime::Builder; + +/// This is also a made-up example. Requests come into the runtime as unicode +/// strings in json format, which can map to any structure that implements `serde::Deserialize` +/// The runtime pays no attention to the contents of the request payload. +#[derive(Deserialize)] +struct Request { + command: String, +} + +/// This is a made-up example of what a response structure may look like. +/// There is no restriction on what it can be. The runtime requires responses +/// to be serialized into json. The runtime pays no attention +/// to the contents of the response payload. +#[derive(Serialize)] +struct Response { + req_id: String, + msg: String, +} + +fn main() -> Result<(), io::Error> { + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); + + // Create a channel used to send and receive outputs from our lambda handler. Realistically, this would be either an unbounded channel + // or a bounded channel with a higher capacity as needed. + let (lambda_tx, lambda_rx) = async_channel::bounded(1); + + // Create a bounded channel used to communicate our shutdown signal across threads. + let (shutdown_tx, shutdown_rx) = async_channel::bounded(1); + + // Build a single-threaded (or multi-threaded using Builder::new_multi_thread) runtime to spawn our lambda work onto. + let tokio_runtime = Builder::new_current_thread() + .thread_name("lambda-runtime") + .enable_all() + .build() + .expect("build lambda runtime"); + + // Run the lambda runtime worker thread to completion. The response is sent to the other "runtime" to be processed as needed. + thread::spawn(move || { + let func = service_fn(my_handler); + if let Ok(response) = tokio_runtime.block_on(lambda_runtime::run(func)) { + lambda_tx.send_blocking(response).expect("send lambda result"); + }; + }); + + // Run the mock runtime to completion. + my_runtime(move || future::block_on(app_runtime_task(lambda_rx.clone(), shutdown_tx.clone()))); + + // Block the main thread until a shutdown signal is received. + future::block_on(shutdown_rx.recv()).map_err(|err| io::Error::other(format!("{err:?}"))) +} + +pub(crate) async fn my_handler(event: LambdaEvent) -> Result { + // extract some useful info from the request + let command = event.payload.command; + + // prepare the response + let resp = Response { + req_id: event.context.request_id, + msg: format!("Command {command} executed."), + }; + + // return `Response` (it will be serialized to JSON automatically by the runtime) + Ok(resp) +} + +/// A task to be ran on the custom runtime. Once a response from the lambda runtime is received then a shutdown signal +/// is sent to the main thread notifying the process to exit. +pub(crate) async fn app_runtime_task(lambda_rx: async_channel::Receiver<()>, shutdown_tx: async_channel::Sender<()>) { + loop { + // Receive the response sent by the lambda handle and process as needed. + if let Ok(result) = lambda_rx.recv().await { + tracing::debug!(?result); + // We're ready to shutdown our app. Send the shutdown signal notifying the main thread to exit the process. + shutdown_tx.send(()).await.expect("send shutdown signal"); + break; + } + + // more app logic would be here... + } +} + +/// Construct the mock runtime worker thread(s) to spawn some work onto. +fn my_runtime(func: impl Fn() + Send + 'static) { + thread::Builder::new() + .name("my-runtime".into()) + .spawn(func) + .expect("spawn my_runtime worker"); +} diff --git a/examples/basic-lambda/Cargo.toml b/examples/basic-lambda/Cargo.toml index ebf36913f..afc727c4f 100644 --- a/examples/basic-lambda/Cargo.toml +++ b/examples/basic-lambda/Cargo.toml @@ -3,18 +3,7 @@ name = "basic-lambda" version = "0.1.0" edition = "2021" - -# Use cargo-edit(https://github.com/killercup/cargo-edit#installation) -# to manage dependencies. -# Running `cargo add DEPENDENCY_NAME` will -# add the latest version of a dependency to the list, -# and it will keep the alphabetic ordering for you. - [dependencies] lambda_runtime = { path = "../../lambda-runtime" } -serde = "1.0.136" +serde = "1.0.219" tokio = { version = "1", features = ["macros"] } -tracing = { version = "0.1", features = ["log"] } -tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } - - diff --git a/examples/basic-lambda/README.md b/examples/basic-lambda/README.md index 5cae85fb9..498f8a500 100644 --- a/examples/basic-lambda/README.md +++ b/examples/basic-lambda/README.md @@ -3,9 +3,9 @@ ## Build & Deploy 1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) -2. Build the function with `cargo lambda build --release` +2. Build the function with `cargo lambda build --release` 3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` ## Build for ARM 64 -Build the function with `cargo lambda build --release --arm64` +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/basic-lambda/src/main.rs b/examples/basic-lambda/src/main.rs index 30a748513..396c3afdc 100644 --- a/examples/basic-lambda/src/main.rs +++ b/examples/basic-lambda/src/main.rs @@ -1,7 +1,7 @@ // This example requires the following input to succeed: // { "command": "do something" } -use lambda_runtime::{service_fn, Error, LambdaEvent}; +use lambda_runtime::{service_fn, tracing, Error, LambdaEvent}; use serde::{Deserialize, Serialize}; /// This is also a made-up example. Requests come into the runtime as unicode @@ -24,14 +24,14 @@ struct Response { #[tokio::main] async fn main() -> Result<(), Error> { - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - // disabling time is handy because CloudWatch will add the ingestion time. - .without_time() - .init(); + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); let func = service_fn(my_handler); - lambda_runtime::run(func).await?; + if let Err(err) = lambda_runtime::run(func).await { + eprintln!("run error: {:?}", err); + return Err(err); + } Ok(()) } @@ -42,9 +42,33 @@ pub(crate) async fn my_handler(event: LambdaEvent) -> Result( + event: LambdaEvent, + size: u32, + client: &T, +) -> Result> { + tracing::info!("handler starts"); + + let context: GetObjectContext = event.payload.get_object_context.unwrap(); + + let route = context.output_route; + let token = context.output_token; + let s3_url = context.input_s3_url; + + tracing::info!("Route: {}, s3_url: {}", route, s3_url); + + let image = client.get_file(s3_url)?; + tracing::info!("Image loaded. Length: {}", image.len()); + + let thumbnail = get_thumbnail(image, size); + tracing::info!("thumbnail created. Length: {}", thumbnail.len()); + + client.send_file(route, token, thumbnail).await +} + +#[cfg(not(test))] +fn get_thumbnail(vec: Vec, size: u32) -> Vec { + let reader = std::io::Cursor::new(vec); + let mut thumbnails = thumbnailer::create_thumbnails( + reader, + mime::IMAGE_PNG, + [thumbnailer::ThumbnailSize::Custom((size, size))], + ) + .unwrap(); + + let thumbnail = thumbnails.pop().unwrap(); + let mut buf = std::io::Cursor::new(Vec::new()); + thumbnail.write_png(&mut buf).unwrap(); + + buf.into_inner() +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); + + let shared_config = aws_config::load_from_env().await; + let client = S3Client::new(&shared_config); + let client_ref = &client; + + let func = service_fn(move |event| async move { function_handler(event, 128, client_ref).await }); + + let _ = run(func).await; + + Ok(()) +} + +#[cfg(test)] +fn get_thumbnail(vec: Vec, _size: u32) -> Vec { + let s = unsafe { std::str::from_utf8_unchecked(&vec) }; + + match s { + "IMAGE" => "THUMBNAIL".into(), + _ => "Input is not IMAGE".into(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use async_trait::async_trait; + use aws_lambda_events::s3::object_lambda::{ + Configuration, HeadObjectContext, ListObjectsContext, ListObjectsV2Context, UserIdentity, UserRequest, + }; + use lambda_runtime::{Context, LambdaEvent}; + use mockall::mock; + use s3::{GetFile, SendFile}; + use serde_json::json; + + #[tokio::test] + async fn response_is_good() { + mock! { + FakeS3Client {} + + impl GetFile for FakeS3Client { + fn get_file(&self, url: String) -> Result, Box>; + } + #[async_trait] + impl SendFile for FakeS3Client { + async fn send_file(&self, route: String, token: String, vec: Vec) -> Result>; + } + } + + let mut mock = MockFakeS3Client::new(); + + mock.expect_get_file() + .withf(|u| u.eq("S3_URL")) + .returning(|_1| Ok("IMAGE".into())); + + mock.expect_send_file() + .withf(|r, t, by| r.eq("O_ROUTE") && t.eq("O_TOKEN") && by == "THUMBNAIL".as_bytes()) + .returning(|_1, _2, _3| Ok("File sent.".to_string())); + + let payload = get_s3_event(); + let context = Context::default(); + let event = LambdaEvent { payload, context }; + + let result = function_handler(event, 10, &mock).await.unwrap(); + + assert_eq!(("File sent."), result); + } + + fn get_s3_event() -> S3ObjectLambdaEvent { + let mut event = S3ObjectLambdaEvent::default(); + event.x_amz_request_id = "ID".to_string(); + event.head_object_context = Some(HeadObjectContext::default()); + event.list_objects_context = Some(ListObjectsContext::default()); + event.get_object_context = Some({ + let mut context = GetObjectContext::default(); + context.input_s3_url = "S3_URL".to_string(); + context.output_route = "O_ROUTE".to_string(); + context.output_token = "O_TOKEN".to_string(); + context + }); + event.list_objects_v2_context = Some(ListObjectsV2Context::default()); + event.protocol_version = "VERSION".to_string(); + event.user_identity = UserIdentity::default(); + event.user_request = UserRequest::default(); + event.configuration = { + let mut configuration = Configuration::default(); + configuration.access_point_arn = "APRN".to_string(); + configuration.supporting_access_point_arn = "SAPRN".to_string(); + configuration.payload = json!(null); + configuration + }; + event + } +} diff --git a/examples/basic-s3-object-lambda-thumbnail/src/s3.rs b/examples/basic-s3-object-lambda-thumbnail/src/s3.rs new file mode 100644 index 000000000..69b46ec28 --- /dev/null +++ b/examples/basic-s3-object-lambda-thumbnail/src/s3.rs @@ -0,0 +1,97 @@ +use async_trait::async_trait; +use aws_sdk_s3::{ + error::SdkError, operation::write_get_object_response::WriteGetObjectResponseError, primitives::ByteStream, + Client as S3Client, +}; +use lambda_runtime::tracing; +use std::{error, io::Read}; + +pub trait GetFile { + fn get_file(&self, url: String) -> Result, Box>; +} + +#[async_trait] +pub trait SendFile { + async fn send_file(&self, route: String, token: String, vec: Vec) -> Result>; +} + +impl GetFile for S3Client { + fn get_file(&self, url: String) -> Result, Box> { + tracing::info!("get file url {}", url); + + let mut res = ureq::get(&url).call()?; + let len: usize = res + .headers() + .get("Content-Length") + .and_then(|h| h.to_str().ok()) + .and_then(|s| s.parse().ok()) + .unwrap(); + + let mut bytes: Vec = Vec::with_capacity(len); + + std::io::Read::take(res.body_mut().as_reader(), 10_000_000).read_to_end(&mut bytes)?; + + tracing::info!("got {} bytes", bytes.len()); + + Ok(bytes) + } +} + +#[async_trait] +impl SendFile for S3Client { + async fn send_file(&self, route: String, token: String, vec: Vec) -> Result> { + tracing::info!("send file route {}, token {}, length {}", route, token, vec.len()); + + let bytes = ByteStream::from(vec); + + let write = self + .write_get_object_response() + .request_route(route) + .request_token(token) + .status_code(200) + .body(bytes) + .send() + .await; + + if let Err(err) = write { + check_error(err); + Err("WriteGetObjectResponse creation error".into()) + } else { + Ok("File sent.".to_string()) + } + } +} + +fn check_error(error: SdkError) { + match error { + SdkError::ConstructionFailure(_err) => { + tracing::info!("ConstructionFailure"); + } + SdkError::DispatchFailure(err) => { + tracing::info!("DispatchFailure"); + if err.is_io() { + tracing::info!("IO error"); + } + if err.is_timeout() { + tracing::info!("Timeout error"); + } + if err.is_user() { + tracing::info!("User error"); + } + if err.is_other() { + tracing::info!("Other error"); + } + } + SdkError::ResponseError(_err) => tracing::info!("ResponseError"), + SdkError::TimeoutError(_err) => tracing::info!("TimeoutError"), + SdkError::ServiceError(err) => { + tracing::info!("ServiceError"); + let wgore = err.into_err(); + let meta = wgore.meta(); + let code = meta.code().unwrap_or_default(); + let msg = meta.message().unwrap_or_default(); + tracing::info!("code: {}, message: {}, meta: {}", code, msg, meta); + } + _ => tracing::info!("other error"), + } +} diff --git a/examples/basic-s3-object-lambda-thumbnail/testdata/image.png b/examples/basic-s3-object-lambda-thumbnail/testdata/image.png new file mode 100644 index 000000000..078d155f6 Binary files /dev/null and b/examples/basic-s3-object-lambda-thumbnail/testdata/image.png differ diff --git a/examples/basic-s3-object-lambda-thumbnail/testdata/thumbnail.png b/examples/basic-s3-object-lambda-thumbnail/testdata/thumbnail.png new file mode 100644 index 000000000..59a37c8dc Binary files /dev/null and b/examples/basic-s3-object-lambda-thumbnail/testdata/thumbnail.png differ diff --git a/examples/basic-s3-thumbnail/Cargo.toml b/examples/basic-s3-thumbnail/Cargo.toml new file mode 100644 index 000000000..fdbd79be1 --- /dev/null +++ b/examples/basic-s3-thumbnail/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "basic-s3-thumbnail" +version = "0.1.0" +edition = "2021" + +# Starting in Rust 1.62 you can use `cargo add` to add dependencies +# to your project. +# +# If you're using an older Rust version, +# download cargo-edit(https://github.com/killercup/cargo-edit#installation) +# to install the `add` subcommand. +# +# Running `cargo add DEPENDENCY_NAME` will +# add the latest version of a dependency to the list, +# and it will keep the alphabetic ordering for you. + +[dependencies] +aws_lambda_events = { path = "../../lambda-events" } +lambda_runtime = { path = "../../lambda-runtime" } +tokio = { version = "1", features = ["macros"] } +aws-config = { version = "1.8.1", features = ["behavior-version-latest"] } +aws-sdk-s3 = "1.96.0" +thumbnailer = "0.5.1" +mime = "0.3.17" +async-trait = "0.1.88" + +[dev-dependencies] +mockall = "0.13.1" +chrono = "0.4" diff --git a/examples/basic-s3-thumbnail/README.md b/examples/basic-s3-thumbnail/README.md new file mode 100644 index 000000000..000874cc3 --- /dev/null +++ b/examples/basic-s3-thumbnail/README.md @@ -0,0 +1,32 @@ +# AWS Lambda Function that uses S3 + +This example processes S3 events. If the event is a CREATE event, +it downloads the created file, generates a thumbnail from it +(it assumes that the file is an image) and uploads it to S3 into a bucket named +[original-bucket-name]-thumbs. + +## Set up +1. Create a lambda function and upload the bootloader.zip +2. Go to aws services S3 +3. Create a bucket, let's say with name bucketx +4. Create another bucket bucketx-thumbs +5. Got to the bucketx properties tab, event notifications +6. Create lambda event notification for "all object create event" and select your lambda function +7. Go to the lambda function, configuration and open the role name +8. Add "AmazonS3FullAccess" permission + +## Test + +1. Go to S3 and upload a png picture into bucketx. Beware to not have spaces or any special characters in the file name +2. Go to S3 bucketx-thumbs and check if an image is created there. + + +## Build & Deploy + +1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the function with `cargo lambda build --release` +3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` + +## Build for ARM 64 + +Build the function with `cargo lambda build --release --arm64` \ No newline at end of file diff --git a/examples/basic-s3-thumbnail/src/main.rs b/examples/basic-s3-thumbnail/src/main.rs new file mode 100644 index 000000000..0d647b05c --- /dev/null +++ b/examples/basic-s3-thumbnail/src/main.rs @@ -0,0 +1,237 @@ +use aws_lambda_events::{event::s3::S3Event, s3::S3EventRecord}; +use aws_sdk_s3::Client as S3Client; +use lambda_runtime::{run, service_fn, tracing, Error, LambdaEvent}; +use s3::{GetFile, PutFile}; + +mod s3; + +/** +This lambda handler + * listen to file creation events + * downloads the created file + * creates a thumbnail from it + * uploads the thumbnail to bucket "[original bucket name]-thumbs". + +Make sure that + * the created png file has no strange characters in the name + * there is another bucket with "-thumbs" suffix in the name + * this lambda only gets event from png file creation + * this lambda has permission to put file into the "-thumbs" bucket +*/ +pub(crate) async fn function_handler( + event: LambdaEvent, + size: u32, + client: &T, +) -> Result<(), Error> { + let records = event.payload.records; + + for record in records.into_iter() { + let (bucket, key) = match get_file_props(record) { + Ok(touple) => touple, + Err(msg) => { + tracing::info!("Record skipped with reason: {}", msg); + continue; + } + }; + + let image = match client.get_file(&bucket, &key).await { + Ok(vec) => vec, + Err(msg) => { + tracing::info!("Can not get file from S3: {}", msg); + continue; + } + }; + + let thumbnail = match get_thumbnail(image, size) { + Ok(vec) => vec, + Err(msg) => { + tracing::info!("Can not create thumbnail: {}", msg); + continue; + } + }; + + let mut thumbs_bucket = bucket.to_owned(); + thumbs_bucket.push_str("-thumbs"); + + // It uploads the thumbnail into a bucket name suffixed with "-thumbs" + // So it needs file creation permission into that bucket + + match client.put_file(&thumbs_bucket, &key, thumbnail).await { + Ok(msg) => tracing::info!(msg), + Err(msg) => tracing::info!("Can not upload thumbnail: {}", msg), + } + } + + Ok(()) +} + +fn get_file_props(record: S3EventRecord) -> Result<(String, String), String> { + record + .event_name + .filter(|s| s.starts_with("ObjectCreated")) + .ok_or("Wrong event")?; + + let bucket = record + .s3 + .bucket + .name + .filter(|s| !s.is_empty()) + .ok_or("No bucket name")?; + + let key = record.s3.object.key.filter(|s| !s.is_empty()).ok_or("No object key")?; + + Ok((bucket, key)) +} + +#[cfg(not(test))] +fn get_thumbnail(vec: Vec, size: u32) -> Result, String> { + use std::io::Cursor; + + use thumbnailer::{create_thumbnails, ThumbnailSize}; + + let reader = Cursor::new(vec); + let mime = mime::IMAGE_PNG; + let sizes = [ThumbnailSize::Custom((size, size))]; + + let thumbnail = match create_thumbnails(reader, mime, sizes) { + Ok(mut thumbnails) => thumbnails.pop().ok_or("No thumbnail created")?, + Err(thumb_error) => return Err(thumb_error.to_string()), + }; + + let mut buf = Cursor::new(Vec::new()); + + match thumbnail.write_png(&mut buf) { + Ok(_) => Ok(buf.into_inner()), + Err(_) => Err("Unknown error when Thumbnail::write_png".to_string()), + } +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); + + let shared_config = aws_config::load_from_env().await; + let client = S3Client::new(&shared_config); + let client_ref = &client; + + let func = service_fn(move |event| async move { function_handler(event, 128, client_ref).await }); + + run(func).await?; + + Ok(()) +} + +#[cfg(test)] +fn get_thumbnail(vec: Vec, _size: u32) -> Result, String> { + let s = unsafe { std::str::from_utf8_unchecked(&vec) }; + + match s { + "IMAGE" => Ok("THUMBNAIL".into()), + _ => Err("Input is not IMAGE".to_string()), + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::*; + use async_trait::async_trait; + //use aws_lambda_events::chrono::DateTime; + use aws_lambda_events::s3::{S3Bucket, S3Entity, S3Object, S3RequestParameters, S3UserIdentity}; + use aws_sdk_s3::operation::get_object::GetObjectError; + use lambda_runtime::{Context, LambdaEvent}; + use mockall::mock; + use s3::{GetFile, PutFile}; + + #[tokio::test] + async fn response_is_good() { + let mut context = Context::default(); + context.request_id = "test-request-id".to_string(); + + let bucket = "test-bucket"; + let key = "test-key"; + + mock! { + FakeS3Client {} + + #[async_trait] + impl GetFile for FakeS3Client { + async fn get_file(&self, bucket: &str, key: &str) -> Result, GetObjectError>; + } + #[async_trait] + impl PutFile for FakeS3Client { + async fn put_file(&self, bucket: &str, key: &str, bytes: Vec) -> Result; + } + } + + let mut mock = MockFakeS3Client::new(); + + mock.expect_get_file() + .withf(|b, k| b.eq(bucket) && k.eq(key)) + .returning(|_1, _2| Ok("IMAGE".into())); + + mock.expect_put_file() + .withf(|bu, ke, by| bu.eq("test-bucket-thumbs") && ke.eq(key) && by.eq("THUMBNAIL".as_bytes())) + .return_const(Ok("Done".to_string())); + + let payload = get_s3_event("ObjectCreated", bucket, key); + let event = LambdaEvent { payload, context }; + + function_handler(event, 10, &mock).await.unwrap(); + + assert_eq!((), ()); + } + + fn get_s3_event(event_name: &str, bucket_name: &str, object_key: &str) -> S3Event { + let mut event = S3Event::default(); + event.records = vec![get_s3_event_record(event_name, bucket_name, object_key)]; + event + } + + fn get_s3_event_record(event_name: &str, bucket_name: &str, object_key: &str) -> S3EventRecord { + let mut s3_bucket = S3Bucket::default(); + s3_bucket.name = (Some(bucket_name.to_string())); + s3_bucket.owner_identity = { + let mut s3_user_identity = S3UserIdentity::default(); + s3_user_identity.principal_id = Some(String::default()); + Some(s3_user_identity) + }; + s3_bucket.arn = Some(String::default()); + + let mut s3_object = S3Object::default(); + s3_object.key = Some(object_key.to_string()); + s3_object.size = Some(1); + s3_object.url_decoded_key = Some(String::default()); + s3_object.version_id = Some(String::default()); + s3_object.e_tag = Some(String::default()); + s3_object.sequencer = Some(String::default()); + + let mut s3_entity = S3Entity::default(); + s3_entity.schema_version = Some(String::default()); + s3_entity.configuration_id = Some(String::default()); + s3_entity.bucket = s3_bucket; + s3_entity.object = s3_object; + + let mut s3_event_record = S3EventRecord::default(); + s3_event_record.event_version = Some(String::default()); + s3_event_record.event_source = Some(String::default()); + s3_event_record.aws_region = Some(String::default()); + s3_event_record.event_time = chrono::DateTime::default(); + s3_event_record.event_name = Some(event_name.to_string()); + s3_event_record.principal_id = { + let mut s3_user_identity = S3UserIdentity::default(); + s3_user_identity.principal_id = Some("X".to_string()); + s3_user_identity + }; + s3_event_record.request_parameters = { + let mut s3_request_parameters = S3RequestParameters::default(); + s3_request_parameters.source_ip_address = Some(String::default()); + s3_request_parameters + }; + s3_event_record.response_elements = HashMap::new(); + s3_event_record.s3 = s3_entity; + s3_event_record + } +} diff --git a/examples/basic-s3-thumbnail/src/s3.rs b/examples/basic-s3-thumbnail/src/s3.rs new file mode 100644 index 000000000..1a7593710 --- /dev/null +++ b/examples/basic-s3-thumbnail/src/s3.rs @@ -0,0 +1,50 @@ +use async_trait::async_trait; +use aws_sdk_s3::{operation::get_object::GetObjectError, primitives::ByteStream, Client as S3Client}; +use lambda_runtime::tracing; + +#[async_trait] +pub trait GetFile { + async fn get_file(&self, bucket: &str, key: &str) -> Result, GetObjectError>; +} + +#[async_trait] +pub trait PutFile { + async fn put_file(&self, bucket: &str, key: &str, bytes: Vec) -> Result; +} + +#[async_trait] +impl GetFile for S3Client { + async fn get_file(&self, bucket: &str, key: &str) -> Result, GetObjectError> { + tracing::info!("get file bucket {}, key {}", bucket, key); + + let output = self.get_object().bucket(bucket).key(key).send().await; + + return match output { + Ok(response) => { + let bytes = response.body.collect().await.unwrap().to_vec(); + tracing::info!("Object is downloaded, size is {}", bytes.len()); + Ok(bytes) + } + Err(err) => { + let service_err = err.into_service_error(); + let meta = service_err.meta(); + tracing::info!("Error from aws when downloding: {}", meta.to_string()); + Err(service_err) + } + }; + } +} + +#[async_trait] +impl PutFile for S3Client { + async fn put_file(&self, bucket: &str, key: &str, vec: Vec) -> Result { + tracing::info!("put file bucket {}, key {}", bucket, key); + let bytes = ByteStream::from(vec); + let result = self.put_object().bucket(bucket).key(key).body(bytes).send().await; + + match result { + Ok(_) => Ok(format!("Uploaded a file with key {key} into {bucket}")), + Err(err) => Err(err.into_service_error().meta().message().unwrap().to_string()), + } + } +} diff --git a/examples/basic-sdk/Cargo.toml b/examples/basic-sdk/Cargo.toml new file mode 100644 index 000000000..6e680aaf9 --- /dev/null +++ b/examples/basic-sdk/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "basic-sdk" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-trait = "0.1" +aws-config = { version = "1.8.1", features = ["behavior-version-latest"] } +aws-sdk-s3 = "1.96.0" +lambda_runtime = { path = "../../lambda-runtime" } +serde = "1.0.219" +tokio = { version = "1", features = ["macros"] } + +[dev-dependencies] +mockall = "0.13.1" diff --git a/examples/basic-sdk/README.md b/examples/basic-sdk/README.md new file mode 100644 index 000000000..ebe70dc7e --- /dev/null +++ b/examples/basic-sdk/README.md @@ -0,0 +1,21 @@ + +## basic-sdk example + +This is an sample function that uses the [AWS SDK](https://github.com/aws/aws-sdk-rust) to +list the contents of an S3 bucket specified by the invoker. It uses standard credentials as defined +in the function's execution role to make calls against S3. + +### Running Locally +You can use `cargo lambda watch` to spin up a local version of the function. This will automatically re-compile and restart +itself when it observes changes to the code. If you invoke `watch` with no other context then the function will not have +the environment variables necessary to supply on SDK calls. To get around this you can manually supply a credentials file +profile for the SDK to resolve and use in your function: +``` +AWS_PROFILE=my-profile cargo lambda watch +``` + +### Invoking +You can invoke by simply leveraging `cargo lambda invoke` with the payload expected by the function handler. +``` +cargo lambda invoke --data-ascii '{"bucket":"my-bucket"}' +``` diff --git a/examples/basic-sdk/src/main.rs b/examples/basic-sdk/src/main.rs new file mode 100644 index 000000000..a021f4c20 --- /dev/null +++ b/examples/basic-sdk/src/main.rs @@ -0,0 +1,130 @@ +use async_trait::async_trait; +use aws_sdk_s3::{operation::list_objects_v2::ListObjectsV2Output, Client as S3Client}; +use lambda_runtime::{service_fn, tracing, Error, LambdaEvent}; +use serde::{Deserialize, Serialize}; + +/// The request defines what bucket to list +#[derive(Deserialize)] +struct Request { + bucket: String, +} + +/// The response contains a Lambda-generated request ID and +/// the list of objects in the bucket. +#[derive(Serialize)] +struct Response { + req_id: String, + bucket: String, + objects: Vec, +} + +#[cfg_attr(test, mockall::automock)] +#[async_trait] +trait ListObjects { + async fn list_objects(&self, bucket: &str) -> Result; +} + +#[async_trait] +impl ListObjects for S3Client { + async fn list_objects(&self, bucket: &str) -> Result { + self.list_objects_v2().bucket(bucket).send().await.map_err(|e| e.into()) + } +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); + + let shared_config = aws_config::load_from_env().await; + let client = S3Client::new(&shared_config); + let client_ref = &client; + + let func = service_fn(move |event| async move { my_handler(event, client_ref).await }); + lambda_runtime::run(func).await?; + + Ok(()) +} + +async fn my_handler(event: LambdaEvent, client: &T) -> Result { + let bucket = event.payload.bucket; + + let objects_rsp = client.list_objects(&bucket).await?; + let objects: Vec<_> = objects_rsp + .contents() + .iter() + .filter_map(|o| o.key().map(|k| k.to_string())) + .collect(); + + // prepare the response + let rsp = Response { + req_id: event.context.request_id, + bucket: bucket.clone(), + objects, + }; + + // return `Response` (it will be serialized to JSON automatically by the runtime) + Ok(rsp) +} + +#[cfg(test)] +mod tests { + use super::*; + use aws_sdk_s3::types::Object; + use lambda_runtime::{Context, LambdaEvent}; + use mockall::predicate::eq; + + #[tokio::test] + async fn response_is_good_for_good_bucket() { + let mut context = Context::default(); + context.request_id = "test-request-id".to_string(); + + let mut mock_client = MockListObjects::default(); + mock_client + .expect_list_objects() + .with(eq("test-bucket")) + .returning(|_| { + Ok(ListObjectsV2Output::builder() + .contents(Object::builder().key("test-key-0").build()) + .contents(Object::builder().key("test-key-1").build()) + .contents(Object::builder().key("test-key-2").build()) + .build()) + }); + + let payload = Request { + bucket: "test-bucket".to_string(), + }; + let event = LambdaEvent { payload, context }; + + let result = my_handler(event, &mock_client).await.unwrap(); + + let expected_keys = vec![ + "test-key-0".to_string(), + "test-key-1".to_string(), + "test-key-2".to_string(), + ]; + assert_eq!(result.req_id, "test-request-id".to_string()); + assert_eq!(result.bucket, "test-bucket".to_string()); + assert_eq!(result.objects, expected_keys); + } + + #[tokio::test] + async fn response_is_bad_for_bad_bucket() { + let mut context = Context::default(); + context.request_id = "test-request-id".to_string(); + + let mut mock_client = MockListObjects::default(); + mock_client + .expect_list_objects() + .with(eq("unknown-bucket")) + .returning(|_| Err(Error::from("test-sdk-error"))); + + let payload = Request { + bucket: "unknown-bucket".to_string(), + }; + let event = LambdaEvent { payload, context }; + + let result = my_handler(event, &mock_client).await; + assert!(result.is_err()); + } +} diff --git a/examples/basic-shared-resource/Cargo.toml b/examples/basic-shared-resource/Cargo.toml index a26bf3ccd..514bef32c 100644 --- a/examples/basic-shared-resource/Cargo.toml +++ b/examples/basic-shared-resource/Cargo.toml @@ -3,18 +3,7 @@ name = "shared-resource" version = "0.1.0" edition = "2021" - -# Use cargo-edit(https://github.com/killercup/cargo-edit#installation) -# to manage dependencies. -# Running `cargo add DEPENDENCY_NAME` will -# add the latest version of a dependency to the list, -# and it will keep the alphabetic ordering for you. - [dependencies] lambda_runtime = { path = "../../lambda-runtime" } -serde = "1.0.136" +serde = "1.0.219" tokio = { version = "1", features = ["macros"] } -tracing = { version = "0.1", features = ["log"] } -tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } - - diff --git a/examples/basic-shared-resource/README.md b/examples/basic-shared-resource/README.md index 5cae85fb9..498f8a500 100644 --- a/examples/basic-shared-resource/README.md +++ b/examples/basic-shared-resource/README.md @@ -3,9 +3,9 @@ ## Build & Deploy 1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) -2. Build the function with `cargo lambda build --release` +2. Build the function with `cargo lambda build --release` 3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` ## Build for ARM 64 -Build the function with `cargo lambda build --release --arm64` +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/basic-shared-resource/src/main.rs b/examples/basic-shared-resource/src/main.rs index a157e5ea1..795c2c973 100644 --- a/examples/basic-shared-resource/src/main.rs +++ b/examples/basic-shared-resource/src/main.rs @@ -4,7 +4,7 @@ // Run it with the following input: // { "command": "do something" } -use lambda_runtime::{service_fn, Error, LambdaEvent}; +use lambda_runtime::{service_fn, tracing, Error, LambdaEvent}; use serde::{Deserialize, Serialize}; /// This is also a made-up example. Requests come into the runtime as unicode @@ -45,11 +45,7 @@ impl SharedClient { #[tokio::main] async fn main() -> Result<(), Error> { // required to enable CloudWatch error logging by the runtime - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - // disabling time is handy because CloudWatch will add the ingestion time. - .without_time() - .init(); + tracing::init_default_subscriber(); let client = SharedClient::new("Shared Client 1 (perhaps a database)"); let client_ref = &client; diff --git a/examples/basic-snapstart/Cargo.toml b/examples/basic-snapstart/Cargo.toml new file mode 100644 index 000000000..77ec94e58 --- /dev/null +++ b/examples/basic-snapstart/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "basic-snapstart" +version = "0.1.0" +edition = "2021" + +[dependencies] +lambda_runtime = { path = "../../lambda-runtime" } +serde = "1.0.219" +serde_json = "1.0" +tokio = { version = "1", features = ["macros"] } diff --git a/examples/basic-snapstart/Dockerfile b/examples/basic-snapstart/Dockerfile new file mode 100644 index 000000000..50d8068d2 --- /dev/null +++ b/examples/basic-snapstart/Dockerfile @@ -0,0 +1,38 @@ +# Container image for the basic-snapstart example. +# +# SnapStart for functions packaged as OCI images requires a custom base image +# whose runtime implements the restore lifecycle — which `lambda_runtime` now +# does. This image builds the example as the Lambda `bootstrap` on top of the +# AWS-provided base (`provided:al2023`), which supports SnapStart. +# +# Build from the REPOSITORY ROOT (the example uses path dependencies on the +# workspace crates, so the build context must include them): +# +# docker build -f examples/basic-snapstart/Dockerfile -t basic-snapstart . +# +# Then push to ECR and create the function with SnapStart enabled +# (PublishedVersions) and the architecture matching your build host. + +# ---- build stage ---- +FROM public.ecr.aws/lambda/provided:al2023 AS builder + +RUN dnf install -y gcc && \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" + +# Copy only the workspace crates this example depends on via path, then the +# example itself: lambda_runtime -> lambda_runtime_api_client. +COPY Cargo.* /build/ +COPY lambda-runtime /build/lambda-runtime +COPY lambda-runtime-api-client /build/lambda-runtime-api-client +COPY examples/basic-snapstart /build/examples/basic-snapstart + +WORKDIR /build/examples/basic-snapstart +RUN cargo build --release + +# ---- runtime stage ---- +FROM public.ecr.aws/lambda/provided:al2023 + +COPY --from=builder /build/examples/basic-snapstart/target/release/basic-snapstart ${LAMBDA_RUNTIME_DIR}/bootstrap + +ENTRYPOINT [ "/var/runtime/bootstrap" ] diff --git a/examples/basic-snapstart/README.md b/examples/basic-snapstart/README.md new file mode 100644 index 000000000..c6a9ee40f --- /dev/null +++ b/examples/basic-snapstart/README.md @@ -0,0 +1,58 @@ +# AWS Lambda SnapStart example (lambda_runtime) + +This example shows how to use AWS Lambda **SnapStart** with the `lambda_runtime` +crate. It implements the [`SnapStartResource`] trait on an `AppState` and +registers it on the runtime, so the runtime runs the resource's `before_snapshot` +hook before the VM snapshot and its `after_restore` hook after each restore. + +SnapStart reduces cold-start latency by snapshotting the initialized execution +environment and restoring from it. Resources created during init (connections, +credentials, unique values) may be invalid after restore — `before_snapshot` / +`after_restore` are where you release and re-establish them. When SnapStart is +not enabled, the hooks are never called and the function behaves normally. + +## Why a container image? + +SnapStart for functions packaged as OCI images requires a custom base image +whose runtime implements the restore lifecycle — which `lambda_runtime` now does. +This example ships a `Dockerfile` that builds the binary as the Lambda +`bootstrap` on top of `public.ecr.aws/lambda/provided:al2023`, which supports +SnapStart. + +## Build & Deploy + +Build the image from the **repository root** (the example depends on the +workspace crates via path, so the build context must include them): + +```sh +docker build -f examples/basic-snapstart/Dockerfile -t basic-snapstart . +``` + +Push it to ECR and create the function from the image: + +```sh +aws ecr create-repository --repository-name basic-snapstart +docker tag basic-snapstart:latest .dkr.ecr..amazonaws.com/basic-snapstart:latest +docker push .dkr.ecr..amazonaws.com/basic-snapstart:latest + +aws lambda create-function \ + --function-name basic-snapstart \ + --package-type Image \ + --code ImageUri=.dkr.ecr..amazonaws.com/basic-snapstart:latest \ + --role \ + --snap-start ApplyOn=PublishedVersions +``` + +SnapStart applies to **published versions**, so publish a version to trigger +snapshot creation, then invoke that version (or an alias pointing at it): + +```sh +aws lambda publish-version --function-name basic-snapstart +aws lambda invoke --function-name basic-snapstart:1 --payload '{"name":"world"}' out.json +``` + +## Architecture + +The compiled `bootstrap` is architecture-specific. Build on (or for) the same +architecture as the target function — `provided:al2023` is available for both +`x86_64` and `arm64`. diff --git a/examples/basic-snapstart/src/main.rs b/examples/basic-snapstart/src/main.rs new file mode 100644 index 000000000..ed9f39f6c --- /dev/null +++ b/examples/basic-snapstart/src/main.rs @@ -0,0 +1,85 @@ +// This example demonstrates using SnapStart with the lambda_runtime crate. +// +// Implement the `SnapStartResource` trait on the types that hold +// snapshot-sensitive state (connections, credentials, cached values) and +// register them on the runtime. When deployed with SnapStart enabled, the +// runtime will: +// 1. Initialize and create resources (e.g., database connections) +// 2. Run each resource's `before_snapshot` hook (reverse registration order) +// before the VM snapshot +// 3. Call `/restore/next`, which blocks until the VM is restored +// 4. Run each resource's `after_restore` hook (registration order) after restore +// 5. Enter the normal invocation loop +// +// When SnapStart is NOT enabled, the hooks are never called and the runtime +// behaves exactly as before. + +use lambda_runtime::{service_fn, tracing, BoxFuture, Error, LambdaEvent, Runtime, SnapStartResource}; +use serde::{Deserialize, Serialize}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; + +#[derive(Deserialize)] +struct Request { + name: String, +} + +#[derive(Serialize)] +struct Response { + message: String, + invocation_count: u64, +} + +struct AppState { + counter: AtomicU64, +} + +impl AppState { + fn new() -> Self { + Self { + counter: AtomicU64::new(0), + } + } +} + +impl SnapStartResource for AppState { + fn before_snapshot(&self) -> BoxFuture<'_, Result<(), Error>> { + Box::pin(async move { + tracing::info!("Releasing resources before snapshot"); + self.counter.store(0, Ordering::Relaxed); + Ok(()) + }) + } + + fn after_restore(&self) -> BoxFuture<'_, Result<(), Error>> { + Box::pin(async move { + tracing::info!("Re-establishing resources after restore"); + Ok(()) + }) + } +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing::init_default_subscriber(); + + let state = Arc::new(AppState::new()); + + let state_ref = state.clone(); + let handler = service_fn(move |event: LambdaEvent| { + let state = state_ref.clone(); + async move { + let count = state.counter.fetch_add(1, Ordering::Relaxed) + 1; + Ok::<_, Error>(Response { + message: format!("Hello, {}!", event.payload.name), + invocation_count: count, + }) + } + }); + + Runtime::new(handler) + .register_snapstart_resource(state.clone()) + .run() + .await?; + Ok(()) +} diff --git a/examples/basic-sqs/Cargo.toml b/examples/basic-sqs/Cargo.toml new file mode 100644 index 000000000..36efee366 --- /dev/null +++ b/examples/basic-sqs/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "basic-sqs" +version = "0.1.0" +edition = "2021" + +# Starting in Rust 1.62 you can use `cargo add` to add dependencies +# to your project. +# +# If you're using an older Rust version, +# download cargo-edit(https://github.com/killercup/cargo-edit#installation) +# to install the `add` subcommand. +# +# Running `cargo add DEPENDENCY_NAME` will +# add the latest version of a dependency to the list, +# and it will keep the alphabetic ordering for you. + +[dependencies] +aws_lambda_events = { path = "../../lambda-events" } +lambda_runtime = { path = "../../lambda-runtime" } +serde = "1.0.219" +tokio = { version = "1", features = ["macros"] } diff --git a/examples/basic-sqs/README.md b/examples/basic-sqs/README.md new file mode 100644 index 000000000..16731de13 --- /dev/null +++ b/examples/basic-sqs/README.md @@ -0,0 +1,13 @@ +# AWS Lambda Function that receives events from SQS + +This example shows how to process events from a SQS queue. + +## Build & Deploy + +1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the function with `cargo lambda build --release` +3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` + +## Build for ARM 64 + +Build the function with `cargo lambda build --release --arm64` \ No newline at end of file diff --git a/examples/basic-sqs/src/main.rs b/examples/basic-sqs/src/main.rs new file mode 100644 index 000000000..f04272be0 --- /dev/null +++ b/examples/basic-sqs/src/main.rs @@ -0,0 +1,27 @@ +use aws_lambda_events::event::sqs::SqsEventObj; +use lambda_runtime::{run, service_fn, tracing, Error, LambdaEvent}; +use serde::{Deserialize, Serialize}; + +/// Object that you send to SQS and plan to process on the function. +#[derive(Deserialize, Serialize)] +struct Data { + id: String, + text: String, +} + +/// This is the main body for the function. +/// You can use the data sent into SQS here. +async fn function_handler(event: LambdaEvent>) -> Result<(), Error> { + let data = &event.payload.records[0].body; + tracing::info!(id = ?data.id, text = ?data.text, "data received from SQS"); + + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); + + run(service_fn(function_handler)).await +} diff --git a/examples/basic-streaming-response/Cargo.toml b/examples/basic-streaming-response/Cargo.toml new file mode 100644 index 000000000..3f1bacaca --- /dev/null +++ b/examples/basic-streaming-response/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "basic-streaming-response" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +lambda_runtime = { path = "../../lambda-runtime" } +tokio = { version = "1", features = ["macros"] } +serde_json = "1.0" \ No newline at end of file diff --git a/examples/basic-streaming-response/README.md b/examples/basic-streaming-response/README.md new file mode 100644 index 000000000..ac744a333 --- /dev/null +++ b/examples/basic-streaming-response/README.md @@ -0,0 +1,13 @@ +# AWS Lambda Function example + +## Build & Deploy + +1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the function with `cargo lambda build --release` +3. Deploy the function to AWS Lambda with `cargo lambda deploy --enable-function-url --iam-role YOUR_ROLE` +4. Enable Lambda streaming response on Lambda console: change the function url's invoke mode to `RESPONSE_STREAM` +5. Verify the function works: `curl -v -N `. The results should be streamed back with 0.5 second pause between each word. + +## Build for ARM 64 + +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/basic-streaming-response/src/main.rs b/examples/basic-streaming-response/src/main.rs new file mode 100644 index 000000000..fe112b803 --- /dev/null +++ b/examples/basic-streaming-response/src/main.rs @@ -0,0 +1,31 @@ +use lambda_runtime::{ + service_fn, + streaming::{channel, Body, Response}, + tracing, Error, LambdaEvent, +}; +use serde_json::Value; +use std::{thread, time::Duration}; + +async fn func(_event: LambdaEvent) -> Result, Error> { + let messages = ["Hello", "world", "from", "Lambda!"]; + + let (mut tx, rx) = channel(); + + tokio::spawn(async move { + for message in messages.iter() { + tx.send_data((message.to_string() + "\n").into()).await.unwrap(); + thread::sleep(Duration::from_millis(500)); + } + }); + + Ok(Response::from(rx)) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); + + lambda_runtime::run(service_fn(func)).await?; + Ok(()) +} diff --git a/examples/basic-tenant-id/Cargo.toml b/examples/basic-tenant-id/Cargo.toml new file mode 100644 index 000000000..2b1144cb6 --- /dev/null +++ b/examples/basic-tenant-id/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "basic-tenant-id" +version = "0.1.0" +edition = "2021" + +[dependencies] +lambda_runtime = { path = "../../lambda-runtime" } +serde_json = "1.0" +tokio = { version = "1", features = ["macros"] } +tracing = { version = "0.1", features = ["log"] } +tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } diff --git a/examples/basic-tenant-id/README.md b/examples/basic-tenant-id/README.md new file mode 100644 index 000000000..eb233e75e --- /dev/null +++ b/examples/basic-tenant-id/README.md @@ -0,0 +1,38 @@ +# Basic Tenant ID Example + +This example demonstrates how to access and use tenant ID information in a Lambda function. + +## Key Features + +- Extracts tenant ID from Lambda runtime headers +- Includes tenant ID in tracing logs +- Returns tenant ID in the response +- Handles cases where tenant ID is not provided + +## Usage + +The tenant ID is automatically extracted from the `lambda-runtime-aws-tenant-id` header and made available in the Lambda context. + +```rust +async fn function_handler(event: LambdaEvent) -> Result { + let (event, context) = event.into_parts(); + + // Access tenant ID from context + match &context.tenant_id { + Some(tenant_id) => println!("Processing for tenant: {}", tenant_id), + None => println!("No tenant ID provided"), + } + + // ... rest of function logic +} +``` + +## Testing + +You can test this function locally using cargo lambda: + +```bash +cargo lambda invoke --data-ascii '{"test": "data"}' +``` + +The tenant ID will be None when testing locally unless you set up a mock runtime environment with the appropriate headers. diff --git a/examples/basic-tenant-id/src/main.rs b/examples/basic-tenant-id/src/main.rs new file mode 100644 index 000000000..1789bcba3 --- /dev/null +++ b/examples/basic-tenant-id/src/main.rs @@ -0,0 +1,36 @@ +use lambda_runtime::{service_fn, Error, LambdaEvent}; +use serde_json::{json, Value}; + +async fn function_handler(event: LambdaEvent) -> Result { + let (event, context) = event.into_parts(); + + // Access tenant ID from context + let tenant_info = match &context.tenant_id { + Some(tenant_id) => format!("Processing request for tenant: {}", tenant_id), + None => "No tenant ID provided".to_string(), + }; + + tracing::info!("Request ID: {}", context.request_id); + tracing::info!("Tenant info: {}", tenant_info); + + // Include tenant ID in response + let response = json!({ + "message": "Hello from Lambda!", + "request_id": context.request_id, + "tenant_id": context.tenant_id, + "input": event + }); + + Ok(response) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .with_target(false) + .without_time() + .init(); + + lambda_runtime::run(service_fn(function_handler)).await +} diff --git a/examples/check-examples.sh b/examples/check-examples.sh index 288245796..36e3038e1 100755 --- a/examples/check-examples.sh +++ b/examples/check-examples.sh @@ -1,11 +1,16 @@ #!/usr/bin/env bash set -e +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +export CARGO_TARGET_DIR="$SCRIPT_DIR/../target" + +echo "==> Using shared target directory: $CARGO_TARGET_DIR" + for f in *; do if [ -d "$f" ]; then echo "==> Checking example: $f" cd $f - cargo check + cargo test cd .. fi done diff --git a/examples/extension-basic/Cargo.toml b/examples/extension-basic/Cargo.toml index caf0818c6..d7cf1f135 100644 --- a/examples/extension-basic/Cargo.toml +++ b/examples/extension-basic/Cargo.toml @@ -3,18 +3,6 @@ name = "extension-basic" version = "0.1.0" edition = "2021" - -# Use cargo-edit(https://github.com/killercup/cargo-edit#installation) -# to manage dependencies. -# Running `cargo add DEPENDENCY_NAME` will -# add the latest version of a dependency to the list, -# and it will keep the alphabetic ordering for you. - [dependencies] lambda-extension = { path = "../../lambda-extension" } -serde = "1.0.136" tokio = { version = "1", features = ["macros"] } -tracing = { version = "0.1", features = ["log"] } -tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } - - diff --git a/examples/extension-basic/README.md b/examples/extension-basic/README.md index 6f9636c92..b566774e7 100644 --- a/examples/extension-basic/README.md +++ b/examples/extension-basic/README.md @@ -8,7 +8,6 @@ The last command will give you an ARN for the extension layer that you can use in your functions. - ## Build for ARM 64 Build the extension with `cargo lambda build --release --extension --arm64` diff --git a/examples/extension-basic/src/main.rs b/examples/extension-basic/src/main.rs index 54784b03d..e76d638f2 100644 --- a/examples/extension-basic/src/main.rs +++ b/examples/extension-basic/src/main.rs @@ -1,4 +1,4 @@ -use lambda_extension::{service_fn, Error, LambdaEvent, NextEvent}; +use lambda_extension::{service_fn, tracing, Error, LambdaEvent, NextEvent}; async fn my_extension(event: LambdaEvent) -> Result<(), Error> { match event.next { @@ -14,13 +14,8 @@ async fn my_extension(event: LambdaEvent) -> Result<(), Error> { #[tokio::main] async fn main() -> Result<(), Error> { - // The runtime logging can be enabled here by initializing `tracing` with `tracing-subscriber` - // While `tracing` is used internally, `log` can be used as well if preferred. - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - // disabling time is handy because CloudWatch will add the ingestion time. - .without_time() - .init(); + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); let func = service_fn(my_extension); lambda_extension::run(func).await diff --git a/examples/extension-combined/Cargo.toml b/examples/extension-combined/Cargo.toml index d776f4889..93aacca10 100644 --- a/examples/extension-combined/Cargo.toml +++ b/examples/extension-combined/Cargo.toml @@ -3,18 +3,6 @@ name = "extension-combined" version = "0.1.0" edition = "2021" - -# Use cargo-edit(https://github.com/killercup/cargo-edit#installation) -# to manage dependencies. -# Running `cargo add DEPENDENCY_NAME` will -# add the latest version of a dependency to the list, -# and it will keep the alphabetic ordering for you. - [dependencies] lambda-extension = { path = "../../lambda-extension" } -serde = "1.0.136" tokio = { version = "1", features = ["macros"] } -tracing = { version = "0.1", features = ["log"] } -tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } - - diff --git a/examples/extension-combined/README.md b/examples/extension-combined/README.md index 6f9636c92..b566774e7 100644 --- a/examples/extension-combined/README.md +++ b/examples/extension-combined/README.md @@ -8,7 +8,6 @@ The last command will give you an ARN for the extension layer that you can use in your functions. - ## Build for ARM 64 Build the extension with `cargo lambda build --release --extension --arm64` diff --git a/examples/extension-combined/src/main.rs b/examples/extension-combined/src/main.rs index bc6cd5e08..ce6054e83 100644 --- a/examples/extension-combined/src/main.rs +++ b/examples/extension-combined/src/main.rs @@ -1,7 +1,6 @@ use lambda_extension::{ - service_fn, Error, Extension, LambdaEvent, LambdaLog, LambdaLogRecord, NextEvent, SharedService, + service_fn, tracing, Error, Extension, LambdaEvent, LambdaLog, LambdaLogRecord, NextEvent, SharedService, }; -use tracing::info; async fn my_extension(event: LambdaEvent) -> Result<(), Error> { match event.next { @@ -18,8 +17,8 @@ async fn my_extension(event: LambdaEvent) -> Result<(), Error> { async fn my_log_processor(logs: Vec) -> Result<(), Error> { for log in logs { match log.record { - LambdaLogRecord::Function(record) => info!("[logs] [function] {}", record), - LambdaLogRecord::Extension(record) => info!("[logs] [extension] {}", record), + LambdaLogRecord::Function(record) => tracing::info!("[logs] [function] {}", record), + LambdaLogRecord::Extension(record) => tracing::info!("[logs] [extension] {}", record), _ => (), } } @@ -29,13 +28,8 @@ async fn my_log_processor(logs: Vec) -> Result<(), Error> { #[tokio::main] async fn main() -> Result<(), Error> { - // The runtime logging can be enabled here by initializing `tracing` with `tracing-subscriber` - // While `tracing` is used internally, `log` can be used as well if preferred. - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - // disabling time is handy because CloudWatch will add the ingestion time. - .without_time() - .init(); + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); let func = service_fn(my_extension); let logs_processor = SharedService::new(service_fn(my_log_processor)); diff --git a/examples/extension-custom-events/Cargo.toml b/examples/extension-custom-events/Cargo.toml index a826a1379..dfef4c4b0 100644 --- a/examples/extension-custom-events/Cargo.toml +++ b/examples/extension-custom-events/Cargo.toml @@ -3,18 +3,6 @@ name = "extension-custom-events" version = "0.1.0" edition = "2021" - -# Use cargo-edit(https://github.com/killercup/cargo-edit#installation) -# to manage dependencies. -# Running `cargo add DEPENDENCY_NAME` will -# add the latest version of a dependency to the list, -# and it will keep the alphabetic ordering for you. - [dependencies] lambda-extension = { path = "../../lambda-extension" } -serde = "1.0.136" tokio = { version = "1", features = ["macros"] } -tracing = { version = "0.1", features = ["log"] } -tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } - - diff --git a/examples/extension-custom-events/README.md b/examples/extension-custom-events/README.md index 6f9636c92..b566774e7 100644 --- a/examples/extension-custom-events/README.md +++ b/examples/extension-custom-events/README.md @@ -8,7 +8,6 @@ The last command will give you an ARN for the extension layer that you can use in your functions. - ## Build for ARM 64 Build the extension with `cargo lambda build --release --extension --arm64` diff --git a/examples/extension-custom-events/src/main.rs b/examples/extension-custom-events/src/main.rs index 7717fc2e9..1590e14a9 100644 --- a/examples/extension-custom-events/src/main.rs +++ b/examples/extension-custom-events/src/main.rs @@ -1,4 +1,4 @@ -use lambda_extension::{service_fn, Error, Extension, LambdaEvent, NextEvent}; +use lambda_extension::{service_fn, tracing, Error, Extension, LambdaEvent, NextEvent}; async fn my_extension(event: LambdaEvent) -> Result<(), Error> { match event.next { @@ -16,13 +16,8 @@ async fn my_extension(event: LambdaEvent) -> Result<(), Error> { #[tokio::main] async fn main() -> Result<(), Error> { - // The runtime logging can be enabled here by initializing `tracing` with `tracing-subscriber` - // While `tracing` is used internally, `log` can be used as well if preferred. - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - // disabling time is handy because CloudWatch will add the ingestion time. - .without_time() - .init(); + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); Extension::new() .with_events(&["SHUTDOWN"]) diff --git a/examples/extension-custom-service/Cargo.toml b/examples/extension-custom-service/Cargo.toml index c9ff789a0..8d0e4575d 100644 --- a/examples/extension-custom-service/Cargo.toml +++ b/examples/extension-custom-service/Cargo.toml @@ -3,18 +3,6 @@ name = "extension-custom-service" version = "0.1.0" edition = "2021" - -# Use cargo-edit(https://github.com/killercup/cargo-edit#installation) -# to manage dependencies. -# Running `cargo add DEPENDENCY_NAME` will -# add the latest version of a dependency to the list, -# and it will keep the alphabetic ordering for you. - [dependencies] lambda-extension = { path = "../../lambda-extension" } -serde = "1.0.136" tokio = { version = "1", features = ["macros"] } -tracing = { version = "0.1", features = ["log"] } -tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } - - diff --git a/examples/extension-custom-service/README.md b/examples/extension-custom-service/README.md index 6f9636c92..b566774e7 100644 --- a/examples/extension-custom-service/README.md +++ b/examples/extension-custom-service/README.md @@ -8,7 +8,6 @@ The last command will give you an ARN for the extension layer that you can use in your functions. - ## Build for ARM 64 Build the extension with `cargo lambda build --release --extension --arm64` diff --git a/examples/extension-custom-service/src/main.rs b/examples/extension-custom-service/src/main.rs index 968dd9047..9a97732c9 100644 --- a/examples/extension-custom-service/src/main.rs +++ b/examples/extension-custom-service/src/main.rs @@ -1,4 +1,4 @@ -use lambda_extension::{run, Error, InvokeEvent, LambdaEvent, NextEvent, Service}; +use lambda_extension::{run, tracing, Error, InvokeEvent, LambdaEvent, NextEvent, Service}; use std::{ future::{ready, Future}, pin::Pin, @@ -33,13 +33,8 @@ impl Service for MyExtension { #[tokio::main] async fn main() -> Result<(), Error> { - // The runtime logging can be enabled here by initializing `tracing` with `tracing-subscriber` - // While `tracing` is used internally, `log` can be used as well if preferred. - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - // disabling time is handy because CloudWatch will add the ingestion time. - .without_time() - .init(); + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); run(MyExtension::default()).await } diff --git a/examples/extension-internal-flush/Cargo.toml b/examples/extension-internal-flush/Cargo.toml new file mode 100644 index 000000000..1a0747dd8 --- /dev/null +++ b/examples/extension-internal-flush/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "extension-internal-flush" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +aws_lambda_events = { path = "../../lambda-events" } +lambda-extension = { path = "../../lambda-extension" } +lambda_runtime = { path = "../../lambda-runtime" } +serde = "1.0.219" +tokio = { version = "1.46", features = ["macros", "sync"] } diff --git a/examples/extension-internal-flush/README.md b/examples/extension-internal-flush/README.md new file mode 100644 index 000000000..c64086767 --- /dev/null +++ b/examples/extension-internal-flush/README.md @@ -0,0 +1,46 @@ +# AWS Lambda runtime + internal extension example + +This example demonstrates how to build an AWS Lambda function that includes a +[Lambda internal extension](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-extensions-api.html). +Unlike external extensions that run as separate processes, an internal extension runs within the +main runtime process. + +One use case for internal extensions is to flush logs or telemetry data after the Lambda runtime +handler has finished processing an event but before the execution environment is frozen awaiting the +arrival of the next event. Without an explicit flush, telemetry data may never be sent since the +execution environment will remain frozen and eventually be terminated if no additional events arrive. + +Note that for +[synchronous](https://docs.aws.amazon.com/lambda/latest/dg/invocation-sync.html) Lambda invocations +(e.g., via +[Amazon API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-integrations.html)), +the Lambda service returns the response to the caller immediately. Extensions may continue to run +without introducing an observable delay. + +## Build & Deploy +Two deploy options for internal extensions: + +1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build a function with the internal extension embedded with `cargo lambda build --release` +3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` + +The last command will give you an ARN for the extension layer that you can use in your functions. + +## Build for ARM 64 + +Build the extension with `cargo lambda build --release --arm64` + + +## Test your Function + +From your local: +``` +cargo lambda watch +# in new terminal window +cargo lambda invoke --data-ascii '{"Records":[{"messageId":"MessageID_1","receiptHandle":"MessageReceiptHandle","body":"{\"a\":\"Test\",\"b\":123}"}]}' +``` + +If you have deployed to AWS using the commands in the 'Build & Deploy' section, you can also invoke your function remotely: +``` +cargo lambda invoke extension-internal-flush --remote --data-ascii '{"Records":[{"messageId":"MessageID_1","receiptHandle":"MessageReceiptHandle","body":"{\"a\":\"Test\",\"b\":123}"}]}' +``` \ No newline at end of file diff --git a/examples/extension-internal-flush/src/main.rs b/examples/extension-internal-flush/src/main.rs new file mode 100644 index 000000000..d20213e15 --- /dev/null +++ b/examples/extension-internal-flush/src/main.rs @@ -0,0 +1,119 @@ +use anyhow::anyhow; +use aws_lambda_events::sqs::{SqsBatchResponse, SqsEventObj}; +use lambda_extension::{service_fn, tracing, Error, Extension, NextEvent}; +use serde::{Deserialize, Serialize}; +use tokio::sync::{ + mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, + Mutex, +}; + +use std::sync::Arc; + +/// Implements an internal Lambda extension to flush logs/telemetry after each request. +struct FlushExtension { + request_done_receiver: Mutex>, +} + +impl FlushExtension { + pub fn new(request_done_receiver: UnboundedReceiver<()>) -> Self { + Self { + request_done_receiver: Mutex::new(request_done_receiver), + } + } + + pub async fn invoke(&self, event: lambda_extension::LambdaEvent) -> Result<(), Error> { + match event.next { + // NB: Internal extensions only support the INVOKE event. + NextEvent::Shutdown(shutdown) => { + return Err(anyhow!("extension received unexpected SHUTDOWN event: {:?}", shutdown).into()); + } + NextEvent::Invoke(_e) => {} + } + + eprintln!("[extension] waiting for event to be processed"); + + // Wait for runtime to finish processing event. + self.request_done_receiver + .lock() + .await + .recv() + .await + .ok_or_else(|| anyhow!("channel is closed"))?; + + eprintln!("[extension] flushing logs and telemetry"); + + // + + Ok(()) + } +} + +/// Object that you send to SQS and plan to process with the function. +#[derive(Debug, Deserialize, Serialize)] +struct Data { + a: String, + b: i64, +} + +/// Implements the main event handler for processing events from an SQS queue. +struct EventHandler { + request_done_sender: UnboundedSender<()>, +} + +impl EventHandler { + pub fn new(request_done_sender: UnboundedSender<()>) -> Self { + Self { request_done_sender } + } + + pub async fn invoke( + &self, + event: lambda_runtime::LambdaEvent>, + ) -> Result { + let data = &event.payload.records[0].body; + eprintln!("[runtime] received event {data:?}"); + + // + + // Notify the extension to flush traces. + self.request_done_sender.send(()).map_err(Box::new)?; + + Ok(SqsBatchResponse::default()) + } +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing::init_default_subscriber(); + let (request_done_sender, request_done_receiver) = unbounded_channel::<()>(); + + let flush_extension = Arc::new(FlushExtension::new(request_done_receiver)); + let extension = Extension::new() + // Internal extensions only support INVOKE events. + .with_events(&["INVOKE"]) + .with_events_processor(service_fn(|event| { + let flush_extension = flush_extension.clone(); + async move { flush_extension.invoke(event).await } + })) + // Internal extension names MUST be unique within a given Lambda function. + .with_extension_name("internal-flush") + // Extensions MUST be registered before calling lambda_runtime::run(), which ends the Init + // phase and begins the Invoke phase. + .register() + .await?; + + let handler = Arc::new(EventHandler::new(request_done_sender)); + + tokio::try_join!( + // always poll the handler function first before the flush extension, + // this results in a smaller future due to not needing to track which was polled first + // each time, and also a tiny latency savings + biased; + lambda_runtime::run(service_fn(|event| { + let handler = handler.clone(); + async move { handler.invoke(event).await } + })), + extension.run(), + )?; + + Ok(()) +} diff --git a/examples/extension-logs-basic/Cargo.toml b/examples/extension-logs-basic/Cargo.toml index d1983db83..230ebc7e0 100644 --- a/examples/extension-logs-basic/Cargo.toml +++ b/examples/extension-logs-basic/Cargo.toml @@ -3,18 +3,6 @@ name = "extension-logs-basic" version = "0.1.0" edition = "2021" - -# Use cargo-edit(https://github.com/killercup/cargo-edit#installation) -# to manage dependencies. -# Running `cargo add DEPENDENCY_NAME` will -# add the latest version of a dependency to the list, -# and it will keep the alphabetic ordering for you. - [dependencies] lambda-extension = { path = "../../lambda-extension" } -serde = "1.0.136" tokio = { version = "1", features = ["macros", "rt"] } -tracing = { version = "0.1", features = ["log"] } -tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } - - diff --git a/examples/extension-logs-basic/README.md b/examples/extension-logs-basic/README.md index 6f9636c92..b566774e7 100644 --- a/examples/extension-logs-basic/README.md +++ b/examples/extension-logs-basic/README.md @@ -8,7 +8,6 @@ The last command will give you an ARN for the extension layer that you can use in your functions. - ## Build for ARM 64 Build the extension with `cargo lambda build --release --extension --arm64` diff --git a/examples/extension-logs-basic/src/main.rs b/examples/extension-logs-basic/src/main.rs index f9ea845a9..4d2662e47 100644 --- a/examples/extension-logs-basic/src/main.rs +++ b/examples/extension-logs-basic/src/main.rs @@ -1,4 +1,4 @@ -use lambda_extension::{service_fn, Error, Extension, LambdaLog, LambdaLogRecord, SharedService}; +use lambda_extension::{service_fn, tracing, Error, Extension, LambdaLog, LambdaLogRecord, SharedService}; use tracing::info; async fn handler(logs: Vec) -> Result<(), Error> { @@ -15,13 +15,8 @@ async fn handler(logs: Vec) -> Result<(), Error> { #[tokio::main] async fn main() -> Result<(), Error> { - // The runtime logging can be enabled here by initializing `tracing` with `tracing-subscriber` - // While `tracing` is used internally, `log` can be used as well if preferred. - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - // disabling time is handy because CloudWatch will add the ingestion time. - .without_time() - .init(); + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); let logs_processor = SharedService::new(service_fn(handler)); diff --git a/examples/extension-logs-custom-service/Cargo.toml b/examples/extension-logs-custom-service/Cargo.toml index cbbe20f60..421fe9ff3 100644 --- a/examples/extension-logs-custom-service/Cargo.toml +++ b/examples/extension-logs-custom-service/Cargo.toml @@ -3,18 +3,6 @@ name = "extension-logs-custom-service" version = "0.1.0" edition = "2021" - -# Use cargo-edit(https://github.com/killercup/cargo-edit#installation) -# to manage dependencies. -# Running `cargo add DEPENDENCY_NAME` will -# add the latest version of a dependency to the list, -# and it will keep the alphabetic ordering for you. - [dependencies] lambda-extension = { path = "../../lambda-extension" } -serde = "1.0.136" tokio = { version = "1", features = ["macros"] } -tracing = { version = "0.1", features = ["log"] } -tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } - - diff --git a/examples/extension-logs-custom-service/README.md b/examples/extension-logs-custom-service/README.md index 6f9636c92..b566774e7 100644 --- a/examples/extension-logs-custom-service/README.md +++ b/examples/extension-logs-custom-service/README.md @@ -8,7 +8,6 @@ The last command will give you an ARN for the extension layer that you can use in your functions. - ## Build for ARM 64 Build the extension with `cargo lambda build --release --extension --arm64` diff --git a/examples/extension-logs-custom-service/src/main.rs b/examples/extension-logs-custom-service/src/main.rs index bcdf660d1..ad2db5bcc 100644 --- a/examples/extension-logs-custom-service/src/main.rs +++ b/examples/extension-logs-custom-service/src/main.rs @@ -1,4 +1,4 @@ -use lambda_extension::{Error, Extension, LambdaLog, LambdaLogRecord, Service, SharedService}; +use lambda_extension::{tracing, Error, Extension, LambdaLog, LambdaLogRecord, Service, SharedService}; use std::{ future::{ready, Future}, pin::Pin, @@ -8,7 +8,6 @@ use std::{ }, task::Poll, }; -use tracing::info; /// Custom log processor that increments a counter for each log record. /// @@ -44,8 +43,8 @@ impl Service> for MyLogsProcessor { let counter = self.counter.fetch_add(1, SeqCst); for log in logs { match log.record { - LambdaLogRecord::Function(record) => info!("[logs] [function] {}: {}", counter, record), - LambdaLogRecord::Extension(record) => info!("[logs] [extension] {}: {}", counter, record), + LambdaLogRecord::Function(record) => tracing::info!("[logs] [function] {}: {}", counter, record), + LambdaLogRecord::Extension(record) => tracing::info!("[logs] [extension] {}: {}", counter, record), _ => (), } } @@ -56,13 +55,8 @@ impl Service> for MyLogsProcessor { #[tokio::main] async fn main() -> Result<(), Error> { - // The runtime logging can be enabled here by initializing `tracing` with `tracing-subscriber` - // While `tracing` is used internally, `log` can be used as well if preferred. - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - // disabling time is handy because CloudWatch will add the ingestion time. - .without_time() - .init(); + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); let logs_processor = SharedService::new(MyLogsProcessor::new()); diff --git a/examples/extension-logs-kinesis-firehose/Cargo.toml b/examples/extension-logs-kinesis-firehose/Cargo.toml index 942d0a93b..60026008c 100644 --- a/examples/extension-logs-kinesis-firehose/Cargo.toml +++ b/examples/extension-logs-kinesis-firehose/Cargo.toml @@ -3,15 +3,8 @@ name = "lambda-logs-firehose-extension" version = "0.1.0" edition = "2021" -# Use cargo-edit(https://github.com/killercup/cargo-edit#installation) -# to manage dependencies. -# Running `cargo add DEPENDENCY_NAME` will -# add the latest version of a dependency to the list, -# and it will keep the alphabetic ordering for you. - [dependencies] lambda-extension = { path = "../../lambda-extension" } -tokio = { version = "1.17.0", features = ["full"] } -aws-config = "0.13.0" -aws-sdk-firehose = "0.13.0" - +tokio = { version = "1.46.1", features = ["full"] } +aws-config = { version = "1.8.1", features = ["behavior-version-latest"] } +aws-sdk-firehose = "1.84.0" diff --git a/examples/extension-logs-kinesis-firehose/README.md b/examples/extension-logs-kinesis-firehose/README.md index 6f9636c92..b566774e7 100644 --- a/examples/extension-logs-kinesis-firehose/README.md +++ b/examples/extension-logs-kinesis-firehose/README.md @@ -8,7 +8,6 @@ The last command will give you an ARN for the extension layer that you can use in your functions. - ## Build for ARM 64 Build the extension with `cargo lambda build --release --extension --arm64` diff --git a/examples/extension-logs-kinesis-firehose/src/main.rs b/examples/extension-logs-kinesis-firehose/src/main.rs index d771cf4bb..c9d8a2e47 100644 --- a/examples/extension-logs-kinesis-firehose/src/main.rs +++ b/examples/extension-logs-kinesis-firehose/src/main.rs @@ -1,5 +1,5 @@ -use aws_sdk_firehose::{model::Record, types::Blob, Client}; -use lambda_extension::{Error, Extension, LambdaLog, LambdaLogRecord, Service, SharedService}; +use aws_sdk_firehose::{primitives::Blob, types::Record, Client}; +use lambda_extension::{tracing, Error, Extension, LambdaLog, LambdaLogRecord, Service, SharedService}; use std::{future::Future, pin::Pin, task::Poll}; #[derive(Clone)] @@ -31,7 +31,12 @@ impl Service> for FirehoseLogsProcessor { for log in logs { match log.record { LambdaLogRecord::Function(record) => { - records.push(Record::builder().data(Blob::new(record.as_bytes())).build()) + match Record::builder().data(Blob::new(record.as_bytes())).build() { + Ok(rec) => records.push(rec), + Err(e) => { + return Box::pin(async move { Err(e.into()) }); + } + } } _ => unreachable!(), } @@ -53,6 +58,9 @@ impl Service> for FirehoseLogsProcessor { #[tokio::main] async fn main() -> Result<(), Error> { + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); + let config = aws_config::load_from_env().await; let logs_processor = SharedService::new(FirehoseLogsProcessor::new(Client::new(&config))); diff --git a/examples/extension-telemetry-basic/Cargo.toml b/examples/extension-telemetry-basic/Cargo.toml new file mode 100644 index 000000000..a0fb6b875 --- /dev/null +++ b/examples/extension-telemetry-basic/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "extension-telemetry-basic" +version = "0.1.0" +edition = "2021" + +[dependencies] +lambda-extension = { path = "../../lambda-extension" } +tokio = { version = "1", features = ["macros", "rt"] } diff --git a/examples/extension-telemetry-basic/README.md b/examples/extension-telemetry-basic/README.md new file mode 100644 index 000000000..337e045c9 --- /dev/null +++ b/examples/extension-telemetry-basic/README.md @@ -0,0 +1,13 @@ +# AWS Lambda Telemetry extension example + +## Build & Deploy + +1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the extension with `cargo lambda build --release --extension` +3. Deploy the extension as a layer with `cargo lambda deploy --extension` + +The last command will give you an ARN for the extension layer that you can use in your functions. + +## Build for ARM 64 + +Build the extension with `cargo lambda build --release --extension --arm64` diff --git a/examples/extension-telemetry-basic/src/main.rs b/examples/extension-telemetry-basic/src/main.rs new file mode 100644 index 000000000..7159cf935 --- /dev/null +++ b/examples/extension-telemetry-basic/src/main.rs @@ -0,0 +1,53 @@ +use lambda_extension::{service_fn, tracing, Error, Extension, LambdaTelemetry, LambdaTelemetryRecord, SharedService}; + +async fn handler(events: Vec) -> Result<(), Error> { + for event in events { + match event.record { + LambdaTelemetryRecord::Function(record) => tracing::info!("[logs] [function] {}", record), + LambdaTelemetryRecord::PlatformInitStart { + initialization_type: _, + phase: _, + runtime_version: _, + runtime_version_arn: _, + } => tracing::info!("[platform] Initialization started"), + LambdaTelemetryRecord::PlatformInitRuntimeDone { + initialization_type: _, + phase: _, + status: _, + error_type: _, + spans: _, + } => tracing::info!("[platform] Initialization finished"), + LambdaTelemetryRecord::PlatformStart { + request_id, + version: _, + tracing: _, + } => tracing::info!("[platform] Handling of request {} started", request_id), + LambdaTelemetryRecord::PlatformRuntimeDone { + request_id, + status: _, + error_type: _, + metrics: _, + spans: _, + tracing: _, + } => tracing::info!("[platform] Handling of request {} finished", request_id), + _ => (), + } + } + + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); + + let telemetry_processor = SharedService::new(service_fn(handler)); + + Extension::new() + .with_telemetry_processor(telemetry_processor) + .run() + .await?; + + Ok(()) +} diff --git a/examples/extension-telemetry-generic/Cargo.toml b/examples/extension-telemetry-generic/Cargo.toml new file mode 100644 index 000000000..50806bce7 --- /dev/null +++ b/examples/extension-telemetry-generic/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "extension-telemetry-basic" +version = "0.1.0" +edition = "2021" + +[dependencies] +lambda-extension = { path = "../../lambda-extension" } +serde_json = "1" +tokio = { version = "1", features = ["macros", "rt"] } diff --git a/examples/extension-telemetry-generic/README.md b/examples/extension-telemetry-generic/README.md new file mode 100644 index 000000000..337e045c9 --- /dev/null +++ b/examples/extension-telemetry-generic/README.md @@ -0,0 +1,13 @@ +# AWS Lambda Telemetry extension example + +## Build & Deploy + +1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the extension with `cargo lambda build --release --extension` +3. Deploy the extension as a layer with `cargo lambda deploy --extension` + +The last command will give you an ARN for the extension layer that you can use in your functions. + +## Build for ARM 64 + +Build the extension with `cargo lambda build --release --extension --arm64` diff --git a/examples/extension-telemetry-generic/src/main.rs b/examples/extension-telemetry-generic/src/main.rs new file mode 100644 index 000000000..39bc117e5 --- /dev/null +++ b/examples/extension-telemetry-generic/src/main.rs @@ -0,0 +1,29 @@ +use lambda_extension::{service_fn, tracing, Error, Extension, LambdaTelemetry, LambdaTelemetryRecord, SharedService}; + +async fn handler(events: Vec>) -> Result<(), Error> { + for event in events { + match event.record { + LambdaTelemetryRecord::Function(record) => tracing::info!("[logs] [function] {}", record), + LambdaTelemetryRecord::Extension(record) => tracing::info!("[extension] [function] {}", record), + _ => (), + } + } + + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); + + let telemetry_processor = SharedService::new(service_fn(handler)); + + Extension::new() + .with_telemetry_record_type::() + .with_telemetry_processor(telemetry_processor) + .run() + .await?; + + Ok(()) +} diff --git a/examples/http-axum-apigw-authorizer/Cargo.toml b/examples/http-axum-apigw-authorizer/Cargo.toml new file mode 100644 index 000000000..44c50167a --- /dev/null +++ b/examples/http-axum-apigw-authorizer/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "http-axum-apigw-authorizer" +version = "0.1.0" +edition = "2021" + +[dependencies] +axum = "0.8" +lambda_http = { path = "../../lambda-http" } +serde_json = "1.0" +tokio = { version = "1", features = ["macros"] } diff --git a/examples/http-axum-apigw-authorizer/README.md b/examples/http-axum-apigw-authorizer/README.md new file mode 100644 index 000000000..2d05df59c --- /dev/null +++ b/examples/http-axum-apigw-authorizer/README.md @@ -0,0 +1,13 @@ +# Axum example that integrates with Api Gateway authorizers + +This example shows how to extract information from the Api Gateway Request Authorizer in an Axum handler. + +## Build & Deploy + +1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the function with `cargo lambda build --release` +3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` + +## Build for ARM 64 + +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/http-axum-apigw-authorizer/src/main.rs b/examples/http-axum-apigw-authorizer/src/main.rs new file mode 100644 index 000000000..8adb90240 --- /dev/null +++ b/examples/http-axum-apigw-authorizer/src/main.rs @@ -0,0 +1,84 @@ +use axum::{ + extract::{FromRequest, Request}, + http::StatusCode, + response::Json, + routing::get, + Router, +}; +use lambda_http::{run, tracing, Error, RequestExt}; +use serde_json::{json, Value}; +use std::{collections::HashMap, env::set_var}; + +struct AuthorizerField(String); +struct AuthorizerFields(HashMap); + +impl FromRequest for AuthorizerField +where + S: Send + Sync, +{ + type Rejection = (StatusCode, &'static str); + + async fn from_request(req: Request, _state: &S) -> Result { + req.request_context_ref() + .and_then(|r| r.authorizer()) + .and_then(|a| a.fields.get("field_name")) + .and_then(|f| f.as_str()) + .map(|v| Self(v.to_string())) + .ok_or((StatusCode::BAD_REQUEST, "`field_name` authorizer field is missing")) + } +} + +impl FromRequest for AuthorizerFields +where + S: Send + Sync, +{ + type Rejection = (StatusCode, &'static str); + + async fn from_request(req: Request, _state: &S) -> Result { + req.request_context_ref() + .and_then(|r| r.authorizer()) + .map(|a| Self(a.fields.clone())) + .ok_or((StatusCode::BAD_REQUEST, "authorizer is missing")) + } +} + +async fn extract_field(AuthorizerField(field): AuthorizerField) -> Json { + Json(json!({ "field extracted": field })) +} + +async fn extract_all_fields(AuthorizerFields(fields): AuthorizerFields) -> Json { + Json(json!({ "authorizer fields": fields })) +} + +async fn authorizer_without_extractor(req: Request) -> Result, (StatusCode, &'static str)> { + let auth = req + .request_context_ref() + .and_then(|r| r.authorizer()) + .ok_or((StatusCode::BAD_REQUEST, "authorizer is missing"))?; + + let field1 = auth.fields.get("field1").and_then(|v| v.as_str()).unwrap_or_default(); + let field2 = auth.fields.get("field2").and_then(|v| v.as_str()).unwrap_or_default(); + + Ok(Json(json!({ "field1": field1, "field2": field2 }))) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + // If you use API Gateway stages, the Rust Runtime will include the stage name + // as part of the path that your application receives. + // Setting the following environment variable, you can remove the stage from the path. + // This variable only applies to API Gateway stages, + // you can remove it if you don't use them. + // i.e with: `GET /test-stage/todo/id/123` without: `GET /todo/id/123` + set_var("AWS_LAMBDA_HTTP_IGNORE_STAGE_IN_PATH", "true"); + + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); + + let app = Router::new() + .route("/extract-field", get(extract_field)) + .route("/extract-all-fields", get(extract_all_fields)) + .route("/authorizer-without-extractor", get(authorizer_without_extractor)); + + run(app).await +} diff --git a/examples/http-axum-diesel-ssl/Cargo.toml b/examples/http-axum-diesel-ssl/Cargo.toml new file mode 100755 index 000000000..d21df0b95 --- /dev/null +++ b/examples/http-axum-diesel-ssl/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "http-axum-diesel" +version = "0.1.0" +edition = "2021" + +[dependencies] +axum = "0.8" +diesel = "2.2.11" +diesel-async = { version = "0.6.1", features = ["postgres", "bb8"] } +lambda_http = { path = "../../lambda-http" } +serde = "1.0.219" +futures-util = "0.3.31" +rustls = "0.23.28" +rustls-native-certs = "0.8.1" +tokio = { version = "1.46.1", default-features = false, features = ["macros", "rt-multi-thread"] } +tokio-postgres = "0.7.13" +tokio-postgres-rustls = "0.13.0" diff --git a/examples/http-axum-diesel-ssl/README.md b/examples/http-axum-diesel-ssl/README.md new file mode 100755 index 000000000..8b2330f55 --- /dev/null +++ b/examples/http-axum-diesel-ssl/README.md @@ -0,0 +1,13 @@ +# AWS Lambda Function example + +This example shows how to develop a REST API with Axum and Diesel that connects to a Postgres database. + +## Build & Deploy + +1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the function with `cargo lambda build --release` +3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` + +## Build for ARM 64 + +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/http-axum-diesel-ssl/migrations/2023-04-07-231632_create_posts/down.sql b/examples/http-axum-diesel-ssl/migrations/2023-04-07-231632_create_posts/down.sql new file mode 100755 index 000000000..e00da6550 --- /dev/null +++ b/examples/http-axum-diesel-ssl/migrations/2023-04-07-231632_create_posts/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE posts \ No newline at end of file diff --git a/examples/http-axum-diesel-ssl/migrations/2023-04-07-231632_create_posts/up.sql b/examples/http-axum-diesel-ssl/migrations/2023-04-07-231632_create_posts/up.sql new file mode 100755 index 000000000..aa684de65 --- /dev/null +++ b/examples/http-axum-diesel-ssl/migrations/2023-04-07-231632_create_posts/up.sql @@ -0,0 +1,7 @@ +-- Your SQL goes here +CREATE TABLE posts ( + id SERIAL PRIMARY KEY, + title VARCHAR NOT NULL, + content TEXT NOT NULL, + published BOOLEAN NOT NULL DEFAULT FALSE +) \ No newline at end of file diff --git a/examples/http-axum-diesel-ssl/src/main.rs b/examples/http-axum-diesel-ssl/src/main.rs new file mode 100755 index 000000000..395c18433 --- /dev/null +++ b/examples/http-axum-diesel-ssl/src/main.rs @@ -0,0 +1,152 @@ +use axum::{ + extract::{Path, State}, + response::Json, + routing::get, + Router, +}; +use diesel::{prelude::*, ConnectionError, ConnectionResult}; +use diesel_async::{ + pooled_connection::{bb8::Pool, AsyncDieselConnectionManager, ManagerConfig}, + AsyncPgConnection, RunQueryDsl, +}; +use futures_util::{future::BoxFuture, FutureExt}; +use lambda_http::{http::StatusCode, run, tracing, Error}; +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +table! { + posts (id) { + id -> Integer, + title -> Text, + content -> Text, + published -> Bool, + } +} + +#[derive(Default, Queryable, Selectable, Serialize)] +struct Post { + id: i32, + title: String, + content: String, + published: bool, +} + +#[derive(Deserialize, Insertable)] +#[diesel(table_name = posts)] +struct NewPost { + title: String, + content: String, + published: bool, +} + +type AsyncPool = Pool; +type ServerError = (StatusCode, String); + +async fn create_post(State(pool): State, Json(post): Json) -> Result, ServerError> { + let mut conn = pool.get().await.map_err(internal_server_error)?; + + let post = diesel::insert_into(posts::table) + .values(post) + .returning(Post::as_returning()) + .get_result(&mut conn) + .await + .map_err(internal_server_error)?; + + Ok(Json(post)) +} + +async fn list_posts(State(pool): State) -> Result>, ServerError> { + let mut conn = pool.get().await.map_err(internal_server_error)?; + + let posts = posts::table + .filter(posts::dsl::published.eq(true)) + .load(&mut conn) + .await + .map_err(internal_server_error)?; + + Ok(Json(posts)) +} + +async fn get_post(State(pool): State, Path(post_id): Path) -> Result, ServerError> { + let mut conn = pool.get().await.map_err(internal_server_error)?; + + let post = posts::table + .find(post_id) + .first(&mut conn) + .await + .map_err(internal_server_error)?; + + Ok(Json(post)) +} + +async fn delete_post(State(pool): State, Path(post_id): Path) -> Result<(), ServerError> { + let mut conn = pool.get().await.map_err(internal_server_error)?; + + diesel::delete(posts::table.find(post_id)) + .execute(&mut conn) + .await + .map_err(internal_server_error)?; + + Ok(()) +} + +fn internal_server_error(err: E) -> ServerError { + (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); + + // Set up the database connection + // Format for DATABASE_URL=postgres://your_username:your_password@your_host:5432/your_db?sslmode=require + let db_url = std::env::var("DATABASE_URL").expect("Env var `DATABASE_URL` not set"); + + let mut config = ManagerConfig::default(); + config.custom_setup = Box::new(establish_connection); + + let mgr = AsyncDieselConnectionManager::::new_with_config(db_url, config); + + let pool = Pool::builder() + .max_size(10) + .min_idle(Some(5)) + .max_lifetime(Some(Duration::from_secs(60 * 60 * 24))) + .idle_timeout(Some(Duration::from_secs(60 * 2))) + .build(mgr) + .await?; + + // Set up the API routes + let posts_api = Router::new() + .route("/", get(list_posts).post(create_post)) + .route("/:id", get(get_post).delete(delete_post)) + .route("/get", get(list_posts)) + .route("/get/:id", get(get_post)); + let app = Router::new().nest("/posts", posts_api).with_state(pool); + + run(app).await +} + +fn establish_connection(config: &str) -> BoxFuture> { + let fut = async { + // We first set up the way we want rustls to work. + let rustls_config = rustls::ClientConfig::builder() + .with_root_certificates(root_certs()) + .with_no_client_auth(); + + let tls = tokio_postgres_rustls::MakeRustlsConnect::new(rustls_config); + let (client, conn) = tokio_postgres::connect(config, tls) + .await + .map_err(|e| ConnectionError::BadConnection(e.to_string()))?; + + AsyncPgConnection::try_from_client_and_connection(client, conn).await + }; + fut.boxed() +} + +fn root_certs() -> rustls::RootCertStore { + let mut roots = rustls::RootCertStore::empty(); + let certs = rustls_native_certs::load_native_certs().expect("Certs not loadable!"); + roots.add_parsable_certificates(certs); + roots +} diff --git a/examples/http-axum-diesel/Cargo.toml b/examples/http-axum-diesel/Cargo.toml new file mode 100644 index 000000000..bed367623 --- /dev/null +++ b/examples/http-axum-diesel/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "http-axum-diesel" +version = "0.1.0" +edition = "2021" + +[dependencies] +axum = "0.8" +diesel = "2.2.11" +diesel-async = { version = "0.6.1", features = ["postgres", "bb8"] } +lambda_http = { path = "../../lambda-http" } +serde = "1.0.219" +tokio = { version = "1", features = ["macros"] } diff --git a/examples/http-axum-diesel/README.md b/examples/http-axum-diesel/README.md new file mode 100644 index 000000000..8b2330f55 --- /dev/null +++ b/examples/http-axum-diesel/README.md @@ -0,0 +1,13 @@ +# AWS Lambda Function example + +This example shows how to develop a REST API with Axum and Diesel that connects to a Postgres database. + +## Build & Deploy + +1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the function with `cargo lambda build --release` +3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` + +## Build for ARM 64 + +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/http-axum-diesel/migrations/2023-04-07-231632_create_posts/down.sql b/examples/http-axum-diesel/migrations/2023-04-07-231632_create_posts/down.sql new file mode 100644 index 000000000..e00da6550 --- /dev/null +++ b/examples/http-axum-diesel/migrations/2023-04-07-231632_create_posts/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE posts \ No newline at end of file diff --git a/examples/http-axum-diesel/migrations/2023-04-07-231632_create_posts/up.sql b/examples/http-axum-diesel/migrations/2023-04-07-231632_create_posts/up.sql new file mode 100644 index 000000000..aa684de65 --- /dev/null +++ b/examples/http-axum-diesel/migrations/2023-04-07-231632_create_posts/up.sql @@ -0,0 +1,7 @@ +-- Your SQL goes here +CREATE TABLE posts ( + id SERIAL PRIMARY KEY, + title VARCHAR NOT NULL, + content TEXT NOT NULL, + published BOOLEAN NOT NULL DEFAULT FALSE +) \ No newline at end of file diff --git a/examples/http-axum-diesel/src/main.rs b/examples/http-axum-diesel/src/main.rs new file mode 100644 index 000000000..348d75357 --- /dev/null +++ b/examples/http-axum-diesel/src/main.rs @@ -0,0 +1,115 @@ +use axum::{ + extract::{Path, State}, + response::Json, + routing::get, + Router, +}; +use diesel::prelude::*; +use diesel_async::{ + pooled_connection::{bb8::Pool, AsyncDieselConnectionManager}, + AsyncPgConnection, RunQueryDsl, +}; +use lambda_http::{http::StatusCode, run, tracing, Error}; +use serde::{Deserialize, Serialize}; + +table! { + posts (id) { + id -> Integer, + title -> Text, + content -> Text, + published -> Bool, + } +} + +#[derive(Default, Queryable, Selectable, Serialize)] +struct Post { + id: i32, + title: String, + content: String, + published: bool, +} + +#[derive(Deserialize, Insertable)] +#[diesel(table_name = posts)] +struct NewPost { + title: String, + content: String, + published: bool, +} + +type AsyncPool = Pool; +type ServerError = (StatusCode, String); + +async fn create_post(State(pool): State, Json(post): Json) -> Result, ServerError> { + let mut conn = pool.get().await.map_err(internal_server_error)?; + + let post = diesel::insert_into(posts::table) + .values(post) + .returning(Post::as_returning()) + .get_result(&mut conn) + .await + .map_err(internal_server_error)?; + + Ok(Json(post)) +} + +async fn list_posts(State(pool): State) -> Result>, ServerError> { + let mut conn = pool.get().await.map_err(internal_server_error)?; + + let posts = posts::table + .filter(posts::dsl::published.eq(true)) + .load(&mut conn) + .await + .map_err(internal_server_error)?; + + Ok(Json(posts)) +} + +async fn get_post(State(pool): State, Path(post_id): Path) -> Result, ServerError> { + let mut conn = pool.get().await.map_err(internal_server_error)?; + + let post = posts::table + .find(post_id) + .first(&mut conn) + .await + .map_err(internal_server_error)?; + + Ok(Json(post)) +} + +async fn delete_post(State(pool): State, Path(post_id): Path) -> Result<(), ServerError> { + let mut conn = pool.get().await.map_err(internal_server_error)?; + + diesel::delete(posts::table.find(post_id)) + .execute(&mut conn) + .await + .map_err(internal_server_error)?; + + Ok(()) +} + +fn internal_server_error(err: E) -> ServerError { + (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); + + // Set up the database connection + let db_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL environment variable"); + let config = AsyncDieselConnectionManager::::new(db_url); + let connection = Pool::builder() + .build(config) + .await + .expect("unable to establish the database connection"); + + // Set up the API routes + let posts_api = Router::new() + .route("/", get(list_posts).post(create_post)) + .route("/:id", get(get_post).delete(delete_post)); + let app = Router::new().nest("/posts", posts_api).with_state(connection); + + run(app).await +} diff --git a/examples/http-axum-middleware/Cargo.toml b/examples/http-axum-middleware/Cargo.toml new file mode 100644 index 000000000..f39669411 --- /dev/null +++ b/examples/http-axum-middleware/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "http-axum-middleware" +version = "0.1.0" +edition = "2021" + +[dependencies] +axum = "0.8" +lambda_http = { path = "../../lambda-http", default-features = false, features = [ + "apigw_rest", "tracing" +] } +serde_json = "1.0" +tokio = { version = "1", features = ["macros"] } diff --git a/examples/http-axum-middleware/README.md b/examples/http-axum-middleware/README.md new file mode 100644 index 000000000..498f8a500 --- /dev/null +++ b/examples/http-axum-middleware/README.md @@ -0,0 +1,11 @@ +# AWS Lambda Function example + +## Build & Deploy + +1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the function with `cargo lambda build --release` +3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` + +## Build for ARM 64 + +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/http-axum-middleware/src/main.rs b/examples/http-axum-middleware/src/main.rs new file mode 100644 index 000000000..e335c270f --- /dev/null +++ b/examples/http-axum-middleware/src/main.rs @@ -0,0 +1,38 @@ +//! This example demonstrates how [axum middleware](https://docs.rs/axum/latest/axum/middleware/index.html) +//! can be implemented. +//! +//! To test this: +//! ```sh +//! # start the local server +//! cargo lambda watch +//! # Then send through an example request +//! cargo lambda invoke --data-example apigw-request +//! ``` + +use axum::{response::Json, routing::post, Router}; +use lambda_http::{request::RequestContext::ApiGatewayV1, run, tracing, Error}; +use serde_json::{json, Value}; + +// Sample middleware that logs the request id +async fn mw_sample(req: axum::extract::Request, next: axum::middleware::Next) -> impl axum::response::IntoResponse { + let context = req.extensions().get::(); + if let Some(ApiGatewayV1(ctx)) = context { + tracing::info!("RequestId = {:?}", ctx.request_id); + } + next.run(req).await +} + +async fn handler_sample(body: Json) -> Json { + Json(json!({ "echo": *body })) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing::init_default_subscriber(); + + let app = Router::new() + .route("/testStage/hello/world", post(handler_sample)) + .route_layer(axum::middleware::from_fn(mw_sample)); + + run(app).await +} diff --git a/examples/http-axum-streaming-otel/Cargo.toml b/examples/http-axum-streaming-otel/Cargo.toml new file mode 100644 index 000000000..d917bb033 --- /dev/null +++ b/examples/http-axum-streaming-otel/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "http-axum-streaming-otel" +version = "0.1.0" +edition = "2021" + +[dependencies] +axum = "0.8" +bytes = "1" +lambda_http = { path = "../../lambda-http", default-features = false, features = [ + "apigw_http", "tracing", "opentelemetry" +] } +opentelemetry = "0.30" +opentelemetry_sdk = { version = "0.30", features = ["rt-tokio"] } +opentelemetry-stdout = { version = "0.30", features = ["trace"] } +thiserror = "2.0" +tokio = { version = "1", features = ["macros"] } +tokio-stream = "0.1.2" +tracing = "0.1" +tracing-opentelemetry = "0.31" +tracing-subscriber = "0.3" diff --git a/examples/http-axum-streaming-otel/README.md b/examples/http-axum-streaming-otel/README.md new file mode 100644 index 000000000..194fe4e4b --- /dev/null +++ b/examples/http-axum-streaming-otel/README.md @@ -0,0 +1,25 @@ +# AWS Lambda Function example + +This example shows how to build a **streaming HTTP response** with `Axum` and +run it on AWS Lambda using a custom runtime with OpenTelemetry (OTel) support. + +Tracing data is exported as console log entries visible in CloudWatch. Note that +CloudWatch assigns a `Timestamp` to each log entry based on when it receives the +data (batch exported). To see when work actually occurred, look at the span's +event attributes, which include the precise local timestamps of those events. + +## Build & Deploy + +1. Install + [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the function with `cargo lambda build --release` +3. Deploy the function to AWS Lambda with: + - `cargo lambda deploy --enable-function-url --iam-role YOUR_ROLE` to stream words +4. Enable Lambda streaming response on Lambda console: change the function url's + invoke mode to `RESPONSE_STREAM` +5. Verify the function works: `curl -N `. The results should be + streamed back with 0.5 second pause between each word. + +## Build for ARM 64 + +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/http-axum-streaming-otel/src/main.rs b/examples/http-axum-streaming-otel/src/main.rs new file mode 100644 index 000000000..64f4e49e4 --- /dev/null +++ b/examples/http-axum-streaming-otel/src/main.rs @@ -0,0 +1,106 @@ +//! # Example: Axum Streaming Responses on AWS Lambda with OTel +//! +//! Demonstrates serving **incremental streaming responses** from Axum handlers +//! running in AWS Lambda using a **custom** `lambda_runtime::Runtime` with +//! OpenTelemetry (OTel) support. +//! +//! - Runs with a custom `Runtime` + `StreamAdapter`, which convert Axum +//! responses into streaming bodies delivered as data is produced (unlike the +//! default `run_with_streaming_response` helper). + +use axum::{ + body::Body, + http::{ + self, + header::{CACHE_CONTROL, CONTENT_TYPE}, + StatusCode, + }, + response::{IntoResponse, Response}, + routing::get, + Router, +}; +use bytes::Bytes; +use core::{convert::Infallible, time::Duration}; +use lambda_http::{ + lambda_runtime::{ + layers::{OpenTelemetryFaasTrigger, OpenTelemetryLayer as OtelLayer}, + tracing::Instrument, + Runtime, + }, + tracing, Error, StreamAdapter, +}; +use opentelemetry::trace::TracerProvider; +use opentelemetry_sdk::trace; +use thiserror::Error; +use tokio::sync::mpsc; +use tokio_stream::wrappers::ReceiverStream; +use tracing_subscriber::prelude::*; + +#[derive(Debug, Error)] +pub enum AppError { + #[error("{0}")] + Http(#[from] http::Error), +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() + } +} + +#[tracing::instrument(skip_all)] +async fn stream_words() -> Result { + let (tx, rx) = mpsc::channel::>(8); + let body = Body::from_stream(ReceiverStream::new(rx)); + + tokio::spawn( + async move { + for (idx, msg) in ["Hello", "world", "from", "Lambda!"].iter().enumerate() { + tokio::time::sleep(Duration::from_millis(500)).await; + let line = format!("{msg}\n"); + tracing::info!(chunk.idx = idx, bytes = line.len(), "emit"); + if tx.send(Ok(Bytes::from(line))).await.is_err() { + break; + } + } + } + .instrument(tracing::info_span!("producer.stream_words")), + ); + + Ok(Response::builder() + .status(StatusCode::OK) + .header(CONTENT_TYPE, "text/plain; charset=utf-8") + .header(CACHE_CONTROL, "no-cache") + .body(body)?) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + // Set up OpenTelemetry tracer provider that writes spans to stdout for + // debugging purposes + let exporter = opentelemetry_stdout::SpanExporter::default(); + let tracer_provider = trace::SdkTracerProvider::builder() + .with_batch_exporter(exporter) + .build(); + + // Set up link between OpenTelemetry and tracing crate + tracing_subscriber::registry() + .with(tracing_opentelemetry::OpenTelemetryLayer::new( + tracer_provider.tracer("my-streaming-app"), + )) + .init(); + + let svc = Router::new().route("/", get(stream_words)); + + // Initialize the Lambda runtime and add OpenTelemetry tracing + let runtime = Runtime::new(StreamAdapter::from(svc)).layer( + OtelLayer::new(|| { + if let Err(err) = tracer_provider.force_flush() { + eprintln!("Error flushing traces: {err:#?}"); + } + }) + .with_trigger(OpenTelemetryFaasTrigger::Http), + ); + + runtime.run().await +} diff --git a/examples/http-axum-streaming/Cargo.toml b/examples/http-axum-streaming/Cargo.toml new file mode 100644 index 000000000..a951562b0 --- /dev/null +++ b/examples/http-axum-streaming/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "http-axum-streaming" +version = "0.1.0" +edition = "2021" + +[dependencies] +axum = "0.8" +bytes = "1" +lambda_http = { path = "../../lambda-http", default-features = false, features = [ + "apigw_http", "tracing" +] } +thiserror = "2.0" +tokio = { version = "1", features = ["macros"] } +tokio-stream = "0.1.2" diff --git a/examples/http-axum-streaming/README.md b/examples/http-axum-streaming/README.md new file mode 100644 index 000000000..fe7e573d7 --- /dev/null +++ b/examples/http-axum-streaming/README.md @@ -0,0 +1,20 @@ +# AWS Lambda Function example + +This example demonstrates building a **streaming** HTTP response with Axum, +deployed on AWS Lambda using a custom runtime. + +## Build & Deploy + +1. Install + [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the function with `cargo lambda build --release` +3. Deploy the function to AWS Lambda with: + - `cargo lambda deploy --enable-function-url --iam-role YOUR_ROLE` to stream words +4. Enable Lambda streaming response on Lambda console: change the function url's + invoke mode to `RESPONSE_STREAM` +5. Verify the function works: `curl -N `. The results should be + streamed back with 0.5 second pause between each word. + +## Build for ARM 64 + +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/http-axum-streaming/src/main.rs b/examples/http-axum-streaming/src/main.rs new file mode 100644 index 000000000..1812f8795 --- /dev/null +++ b/examples/http-axum-streaming/src/main.rs @@ -0,0 +1,70 @@ +//! # Example: Axum Streaming Responses on AWS Lambda +//! +//! Demonstrates serving **incremental streaming responses** from Axum handlers +//! running in AWS Lambda. +//! +//! - Runs with `run_with_streaming_response`, which uses the **default Lambda +//! runtime** to convert Axum responses into streaming bodies delivered as +//! data is produced (unlike the OTel example, which used a custom `Runtime` + +//! `StreamAdapter`). + +use axum::{ + body::Body, + http::{ + self, + header::{CACHE_CONTROL, CONTENT_TYPE}, + StatusCode, + }, + response::{IntoResponse, Response}, + routing::get, + Router, +}; +use bytes::Bytes; +use core::{convert::Infallible, time::Duration}; +use lambda_http::{run_with_streaming_response, tracing, Error}; +use thiserror::Error; +use tokio::sync::mpsc; +use tokio_stream::wrappers::ReceiverStream; + +#[derive(Debug, Error)] +pub enum AppError { + #[error("{0}")] + Http(#[from] http::Error), +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() + } +} + +async fn stream_words() -> Result { + let (tx, rx) = mpsc::channel::>(8); + let body = Body::from_stream(ReceiverStream::new(rx)); + + tokio::spawn(async move { + for msg in ["Hello", "world", "from", "Lambda!"] { + tokio::time::sleep(Duration::from_millis(500)).await; + if tx.send(Ok(Bytes::from(format!("{msg}\n")))).await.is_err() { + break; + } + } + }); + + Ok(Response::builder() + .status(StatusCode::OK) + .header(CONTENT_TYPE, "text/plain; charset=utf-8") + .header(CACHE_CONTROL, "no-cache") + .body(body)?) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing::init_default_subscriber(); + + let svc = Router::new().route("/", get(stream_words)); + + // Automatically convert the service into a streaming response with a + // default runtime. + run_with_streaming_response(svc).await +} diff --git a/examples/http-axum/Cargo.toml b/examples/http-axum/Cargo.toml new file mode 100644 index 000000000..7664e7a79 --- /dev/null +++ b/examples/http-axum/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "http-axum" +version = "0.1.0" +edition = "2021" + +[dependencies] +axum = "0.8" +lambda_http = { path = "../../lambda-http" } +serde = "1.0.219" +serde_json = "1.0" +tokio = { version = "1", features = ["macros"] } diff --git a/examples/http-axum/README.md b/examples/http-axum/README.md new file mode 100644 index 000000000..498f8a500 --- /dev/null +++ b/examples/http-axum/README.md @@ -0,0 +1,11 @@ +# AWS Lambda Function example + +## Build & Deploy + +1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the function with `cargo lambda build --release` +3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` + +## Build for ARM 64 + +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/http-axum/cdk/.gitignore b/examples/http-axum/cdk/.gitignore new file mode 100644 index 000000000..f60797b6a --- /dev/null +++ b/examples/http-axum/cdk/.gitignore @@ -0,0 +1,8 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/examples/http-axum/cdk/.npmignore b/examples/http-axum/cdk/.npmignore new file mode 100644 index 000000000..c1d6d45dc --- /dev/null +++ b/examples/http-axum/cdk/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/examples/http-axum/cdk/README.md b/examples/http-axum/cdk/README.md new file mode 100644 index 000000000..902f576e4 --- /dev/null +++ b/examples/http-axum/cdk/README.md @@ -0,0 +1,40 @@ +# Axum HTTP CDK Stack + +This is a basic stack that shows how to deploy the Axum HTTP example with the AWS CDK. + +## Resources + +This stack deploys the Axum HTTP example in AWS Lambda. + +It also creates an API Gateway Rest API to expose the Axum app to the internet. When the deploy is completed, the stack will print the endpoint URL for the gateway. It will look something like this: + +``` +CdkStack.axumEndpointC1B330D3 = https://sr0e4dqg1b.execute-api.us-east-1.amazonaws.com/prod/ +``` + +If you set the environment variable `ENABLE_LAMBDA_RUST_AXUM_FUNCTION_URL=true` in your terminal before deploying the stack, it will also create a Lambda Function URL without any authentication mode. When the deploy completes, the stack will print the endpoint URL for this function. It will look something like this: + +``` +CdkStack.AxumFunctionUrl = https://7st53uq3rpk4jweki2ek765gty0icvuf.lambda-url.us-east-1.on.aws/ +``` + +## Dependencies + +1. Install the AWS CDK with NPM: `npm install -g cdk`. +2. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) + +## Deployment + +Run `npm run build` to complile the stack. + +Then, run `npm run cdk deploy` to deploy the stack on your AWS account. + +## Security + +This example doesn't provide any security configuration. It's up to you to configure the stack with the security settings that are more convenient to you. We're not responsible for resources open to the internet on your AWS account. + +## Cleanup + +Deploying this stack on your account might incur on AWS costs due to the resources that we're deploying. Don't forget to delete those resources from your account if you're not using them any longer. + +Run `npm run cdk destroy` to delete all resources in this stack from your AWS account. diff --git a/examples/http-axum/cdk/bin/cdk.ts b/examples/http-axum/cdk/bin/cdk.ts new file mode 100644 index 000000000..1d8bfd976 --- /dev/null +++ b/examples/http-axum/cdk/bin/cdk.ts @@ -0,0 +1,21 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { CdkStack } from '../lib/cdk-stack'; + +const app = new cdk.App(); +new CdkStack(app, 'CdkStack', { + /* If you don't specify 'env', this stack will be environment-agnostic. + * Account/Region-dependent features and context lookups will not work, + * but a single synthesized template can be deployed anywhere. */ + + /* Uncomment the next line to specialize this stack for the AWS Account + * and Region that are implied by the current CLI configuration. */ + // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, + + /* Uncomment the next line if you know exactly what Account and Region you + * want to deploy the stack to. */ + // env: { account: '123456789012', region: 'us-east-1' }, + + /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ +}); \ No newline at end of file diff --git a/examples/http-axum/cdk/cdk.json b/examples/http-axum/cdk/cdk.json new file mode 100644 index 000000000..7dc211da3 --- /dev/null +++ b/examples/http-axum/cdk/cdk.json @@ -0,0 +1,65 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/cdk.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true + } +} diff --git a/examples/http-axum/cdk/lib/cdk-stack.ts b/examples/http-axum/cdk/lib/cdk-stack.ts new file mode 100644 index 000000000..a0cc27a64 --- /dev/null +++ b/examples/http-axum/cdk/lib/cdk-stack.ts @@ -0,0 +1,26 @@ +import { join } from 'path'; +import { CfnOutput, Stack, StackProps } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { RustFunction } from 'cargo-lambda-cdk'; +import { LambdaRestApi } from 'aws-cdk-lib/aws-apigateway' +import { FunctionUrlAuthType } from "aws-cdk-lib/aws-lambda"; + +export class CdkStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const handler = new RustFunction(this, 'Axum API', { + // Path to the http-axum root directory. + manifestPath: join(__dirname, '..', '..'), + }); + + if (process.env.ENABLE_LAMBDA_RUST_AXUM_FUNCTION_URL) { + const lambdaUrl = handler.addFunctionUrl({ + authType: FunctionUrlAuthType.NONE, + }); + new CfnOutput(this, 'Axum FunctionUrl ', { value: lambdaUrl.url }); + } + + new LambdaRestApi(this, 'axum', { handler }); + } +} diff --git a/examples/http-axum/cdk/package-lock.json b/examples/http-axum/cdk/package-lock.json new file mode 100644 index 000000000..4b8ceb1f6 --- /dev/null +++ b/examples/http-axum/cdk/package-lock.json @@ -0,0 +1,7447 @@ +{ + "name": "cdk", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "cdk", + "version": "0.1.0", + "dependencies": { + "aws-cdk-lib": "^2.262.0", + "cargo-lambda-cdk": "^0.0.35", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + }, + "bin": { + "cdk": "bin/cdk.js" + }, + "devDependencies": { + "@types/jest": "^29.5.11", + "@types/node": "20.11.14", + "aws-cdk": "2.126.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.2", + "ts-node": "^10.9.2", + "typescript": "~5.3.3" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.282", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.282.tgz", + "integrity": "sha512-7hKMi5tTxDcKGIMIOq14PnY0GBcugW33Uh/2YHDZiEwSxLeFOCYBwhR+BFXONb/EJeVI3RETFgailNZbkcKF6g==" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.2.tgz", + "integrity": "sha512-pDiuqH+qY3zM9lhhLjbKJ1tnKOHzQ2V4Wr/3qsxyKeKAkuPMI/BVGvZG1PbrikUw949cGVTfVEt4ETKKYnrj0Q==" + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "54.14.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-54.14.0.tgz", + "integrity": "sha512-JCZCzgp3SuXQVljaKqXnttHzcezEHt9Ag/YipK0XwUFD+Iz2T4jY7gUc3pA25Uq6pzY2n9DvO/nEU++dPXW4Rw==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "dependencies": { + "jsonschema": "^1.5.0", + "semver": "^7.8.5" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.8.5", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", + "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.9", + "@babel/parser": "^7.23.9", + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", + "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", + "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz", + "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", + "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", + "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/node": { + "version": "20.11.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.14.tgz", + "integrity": "sha512-w3yWCcwULefjP9DmDDsgUskrMoOy5Z8MiwKHr1FvqGPtx7CvJzQvxD7eKpxNtklQxLruxSXWddyeRtyud0RcXQ==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aws-cdk": { + "version": "2.126.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.126.0.tgz", + "integrity": "sha512-hEyy8UCEEUnkieH6JbJBN8XAbvuVZNdBmVQ8wHCqo8RSNqmpwM1qvLiyXV/2JvCqJJ0bl9uBiZ98Ytd5i3wW7g==", + "dev": true, + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 14.15.0" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.262.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.262.0.tgz", + "integrity": "sha512-6zRVoWRd8kQs9ZZ9xhERSm36W8uWS2vW3/g9Zx0xlhvRRiUwTc80bzfJkohKGdT/5AOW+wy6fSDvohavG94pcA==", + "bundleDependencies": [ + "@aws/cloudformation-validate", + "@balena/dockerignore", + "@aws-cdk/cloud-assembly-api", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "yaml", + "mime-types" + ], + "dependencies": { + "@aws-cdk/asset-awscli-v1": "2.2.282", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.2", + "@aws-cdk/cloud-assembly-api": "^2.2.6", + "@aws-cdk/cloud-assembly-schema": "^54.11.0", + "@aws/cloudformation-validate": "1.5.0-beta", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.3.6", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^10.2.5", + "punycode": "^2.3.1", + "semver": "^7.8.5", + "yaml": "1.10.3" + }, + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "constructs": "^10.5.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api": { + "version": "2.2.6", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "^1.5.0", + "semver": "^7.8.4" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "@aws-cdk/cloud-assembly-schema": ">=54.5.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@aws/cloudformation-validate": { + "version": "1.5.0-beta", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^22.15.0", + "npm": ">=10.5.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "4.0.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "5.0.7", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.3.6", + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.3.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.2.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "10.2.5", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.8.5", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.3", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz", + "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001580", + "electron-to-chromium": "^1.4.648", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001583", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001583.tgz", + "integrity": "sha512-acWTYaha8xfhA/Du/z4sNZjHUWjkiuoAi2LM+T/aL+kemKQgPT1xBb/YKjlQ0Qo8gvbHsGNplrEJ+9G3gL7i4Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/cargo-lambda-cdk": { + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/cargo-lambda-cdk/-/cargo-lambda-cdk-0.0.35.tgz", + "integrity": "sha512-989zAeN/SpXhE1BmjtCuLmTX6nC9qm4CmlVFFJO6ob368w9dZBamlI3jouc+NAX+yW8vbYOih8LUkUIXCWF6VA==", + "bundleDependencies": [ + "js-toml" + ], + "dependencies": { + "js-toml": "^1.0.2" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.215.0", + "constructs": "^10.0.5" + } + }, + "node_modules/cargo-lambda-cdk/node_modules/@babel/runtime-corejs3": { + "version": "7.26.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.30.2", + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/cargo-lambda-cdk/node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/cargo-lambda-cdk/node_modules/@chevrotain/gast": { + "version": "11.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/cargo-lambda-cdk/node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/cargo-lambda-cdk/node_modules/@chevrotain/types": { + "version": "11.0.3", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/cargo-lambda-cdk/node_modules/@chevrotain/utils": { + "version": "11.0.3", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/cargo-lambda-cdk/node_modules/chevrotain": { + "version": "11.0.3", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/cargo-lambda-cdk/node_modules/core-js-pure": { + "version": "3.39.0", + "hasInstallScript": true, + "inBundle": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cargo-lambda-cdk/node_modules/js-toml": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "chevrotain": "^11.0.3", + "xregexp": "^5.1.1" + } + }, + "node_modules/cargo-lambda-cdk/node_modules/lodash-es": { + "version": "4.17.21", + "inBundle": true, + "license": "MIT" + }, + "node_modules/cargo-lambda-cdk/node_modules/regenerator-runtime": { + "version": "0.14.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/cargo-lambda-cdk/node_modules/xregexp": { + "version": "5.1.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@babel/runtime-corejs3": "^7.16.5" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/constructs": { + "version": "10.5.1", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.5.1.tgz", + "integrity": "sha512-f/TfFXiS3G/yVIXDjOQn9oTlyu9Wo7Fxyjj7lb8r92iO81jR2uST+9MstxZTmDGx/CgIbxCXkFXgupnLTNxQZg==" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "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==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.656", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.656.tgz", + "integrity": "sha512-9AQB5eFTHyR3Gvt2t/NwR0le2jBSUNwCnMbUCejFWHD+so4tH40/dRLgoE+jxlPeWS43XJewyvCv+I8LPMl49Q==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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==", + "dev": true + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "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" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "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, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "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, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz", + "integrity": "sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "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, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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 + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "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, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", + "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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 + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-jest": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", + "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==", + "dev": true, + "dependencies": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/v8-to-istanbul": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@aws-cdk/asset-awscli-v1": { + "version": "2.2.282", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.282.tgz", + "integrity": "sha512-7hKMi5tTxDcKGIMIOq14PnY0GBcugW33Uh/2YHDZiEwSxLeFOCYBwhR+BFXONb/EJeVI3RETFgailNZbkcKF6g==" + }, + "@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.2.tgz", + "integrity": "sha512-pDiuqH+qY3zM9lhhLjbKJ1tnKOHzQ2V4Wr/3qsxyKeKAkuPMI/BVGvZG1PbrikUw949cGVTfVEt4ETKKYnrj0Q==" + }, + "@aws-cdk/cloud-assembly-schema": { + "version": "54.14.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-54.14.0.tgz", + "integrity": "sha512-JCZCzgp3SuXQVljaKqXnttHzcezEHt9Ag/YipK0XwUFD+Iz2T4jY7gUc3pA25Uq6pzY2n9DvO/nEU++dPXW4Rw==", + "requires": { + "jsonschema": "^1.5.0", + "semver": "^7.8.5" + }, + "dependencies": { + "jsonschema": { + "version": "1.5.0", + "bundled": true + }, + "semver": { + "version": "7.8.5", + "bundled": true + } + } + }, + "@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/compat-data": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "dev": true + }, + "@babel/core": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", + "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.9", + "@babel/parser": "^7.23.9", + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + } + }, + "@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "dev": true, + "requires": { + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.15" + } + }, + "@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true + }, + "@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true + }, + "@babel/helpers": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", + "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", + "dev": true, + "requires": { + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9" + } + }, + "@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "dev": true + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", + "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz", + "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/template": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", + "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9" + } + }, + "@babel/traverse": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", + "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9", + "debug": "^4.3.1", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "requires": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + } + }, + "@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "requires": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + } + }, + "@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3" + } + }, + "@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + } + }, + "@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + } + }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + } + }, + "@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7" + } + }, + "@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "requires": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "@types/node": { + "version": "20.11.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.14.tgz", + "integrity": "sha512-w3yWCcwULefjP9DmDDsgUskrMoOy5Z8MiwKHr1FvqGPtx7CvJzQvxD7eKpxNtklQxLruxSXWddyeRtyud0RcXQ==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + }, + "@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true + }, + "acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "aws-cdk": { + "version": "2.126.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.126.0.tgz", + "integrity": "sha512-hEyy8UCEEUnkieH6JbJBN8XAbvuVZNdBmVQ8wHCqo8RSNqmpwM1qvLiyXV/2JvCqJJ0bl9uBiZ98Ytd5i3wW7g==", + "dev": true, + "requires": { + "fsevents": "2.3.2" + } + }, + "aws-cdk-lib": { + "version": "2.262.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.262.0.tgz", + "integrity": "sha512-6zRVoWRd8kQs9ZZ9xhERSm36W8uWS2vW3/g9Zx0xlhvRRiUwTc80bzfJkohKGdT/5AOW+wy6fSDvohavG94pcA==", + "requires": { + "@aws-cdk/asset-awscli-v1": "2.2.282", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.2", + "@aws-cdk/cloud-assembly-api": "^2.2.6", + "@aws-cdk/cloud-assembly-schema": "^54.11.0", + "@aws/cloudformation-validate": "1.5.0-beta", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.3.6", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^10.2.5", + "punycode": "^2.3.1", + "semver": "^7.8.5", + "yaml": "1.10.3" + }, + "dependencies": { + "@aws-cdk/cloud-assembly-api": { + "version": "2.2.6", + "bundled": true, + "requires": { + "jsonschema": "^1.5.0", + "semver": "^7.8.4" + } + }, + "@aws/cloudformation-validate": { + "version": "1.5.0-beta", + "bundled": true + }, + "@balena/dockerignore": { + "version": "1.0.2", + "bundled": true + }, + "balanced-match": { + "version": "4.0.4", + "bundled": true + }, + "brace-expansion": { + "version": "5.0.7", + "bundled": true, + "requires": { + "balanced-match": "^4.0.2" + } + }, + "case": { + "version": "1.6.3", + "bundled": true + }, + "fs-extra": { + "version": "11.3.6", + "bundled": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "graceful-fs": { + "version": "4.2.11", + "bundled": true + }, + "ignore": { + "version": "5.3.2", + "bundled": true + }, + "jsonfile": { + "version": "6.2.1", + "bundled": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "jsonschema": { + "version": "1.5.0", + "bundled": true + }, + "mime-db": { + "version": "1.52.0", + "bundled": true + }, + "mime-types": { + "version": "2.1.35", + "bundled": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "10.2.5", + "bundled": true, + "requires": { + "brace-expansion": "^5.0.5" + } + }, + "punycode": { + "version": "2.3.1", + "bundled": true + }, + "semver": { + "version": "7.8.5", + "bundled": true + }, + "universalify": { + "version": "2.0.1", + "bundled": true + }, + "yaml": { + "version": "1.10.3", + "bundled": true + } + } + }, + "babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "requires": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "dependencies": { + "istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + } + } + } + }, + "babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "browserslist": { + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz", + "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001580", + "electron-to-chromium": "^1.4.648", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + } + }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001583", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001583.tgz", + "integrity": "sha512-acWTYaha8xfhA/Du/z4sNZjHUWjkiuoAi2LM+T/aL+kemKQgPT1xBb/YKjlQ0Qo8gvbHsGNplrEJ+9G3gL7i4Q==", + "dev": true + }, + "cargo-lambda-cdk": { + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/cargo-lambda-cdk/-/cargo-lambda-cdk-0.0.35.tgz", + "integrity": "sha512-989zAeN/SpXhE1BmjtCuLmTX6nC9qm4CmlVFFJO6ob368w9dZBamlI3jouc+NAX+yW8vbYOih8LUkUIXCWF6VA==", + "requires": { + "js-toml": "^1.0.2" + }, + "dependencies": { + "@babel/runtime-corejs3": { + "version": "7.26.0", + "bundled": true, + "requires": { + "core-js-pure": "^3.30.2", + "regenerator-runtime": "^0.14.0" + } + }, + "@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "bundled": true, + "requires": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "@chevrotain/gast": { + "version": "11.0.3", + "bundled": true, + "requires": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "bundled": true + }, + "@chevrotain/types": { + "version": "11.0.3", + "bundled": true + }, + "@chevrotain/utils": { + "version": "11.0.3", + "bundled": true + }, + "chevrotain": { + "version": "11.0.3", + "bundled": true, + "requires": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "core-js-pure": { + "version": "3.39.0", + "bundled": true + }, + "js-toml": { + "version": "1.0.2", + "bundled": true, + "requires": { + "chevrotain": "^11.0.3", + "xregexp": "^5.1.1" + } + }, + "lodash-es": { + "version": "4.17.21", + "bundled": true + }, + "regenerator-runtime": { + "version": "0.14.1", + "bundled": true + }, + "xregexp": { + "version": "5.1.1", + "bundled": true, + "requires": { + "@babel/runtime-corejs3": "^7.16.5" + } + } + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true + }, + "ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true + }, + "cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "constructs": { + "version": "10.5.1", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.5.1.tgz", + "integrity": "sha512-f/TfFXiS3G/yVIXDjOQn9oTlyu9Wo7Fxyjj7lb8r92iO81jR2uST+9MstxZTmDGx/CgIbxCXkFXgupnLTNxQZg==" + }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + } + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "requires": {} + }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.4.656", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.656.tgz", + "integrity": "sha512-9AQB5eFTHyR3Gvt2t/NwR0le2jBSUNwCnMbUCejFWHD+so4tH40/dRLgoE+jxlPeWS43XJewyvCv+I8LPMl49Q==", + "dev": true + }, + "emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true + }, + "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==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "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" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true + }, + "expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "requires": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "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 + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "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 + }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "requires": { + "hasown": "^2.0.0" + } + }, + "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==", + "dev": true + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "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 + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz", + "integrity": "sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, + "istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + } + }, + "jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "requires": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + } + }, + "jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + } + }, + "jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + } + }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + } + }, + "jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + } + }, + "jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + } + }, + "jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "requires": {} + }, + "jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true + }, + "jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + } + }, + "jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "requires": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + } + }, + "jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "dependencies": { + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + } + } + }, + "jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + } + }, + "jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + } + } + }, + "jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + } + }, + "jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "requires": { + "tmpl": "1.0.5" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "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 + }, + "minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "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 + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "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, + "requires": { + "path-key": "^3.0.0" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true + }, + "pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, + "pure-rand": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", + "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", + "dev": true + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "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 + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + } + }, + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "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 + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "ts-jest": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", + "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==", + "dev": true, + "requires": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + }, + "typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "v8-to-istanbul": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + } + }, + "walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "requires": { + "makeerror": "1.0.12" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/examples/http-axum/cdk/package.json b/examples/http-axum/cdk/package.json new file mode 100644 index 000000000..f4ce86a26 --- /dev/null +++ b/examples/http-axum/cdk/package.json @@ -0,0 +1,28 @@ +{ + "name": "cdk", + "version": "0.1.0", + "bin": { + "cdk": "bin/cdk.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk" + }, + "devDependencies": { + "@types/jest": "^29.5.11", + "@types/node": "20.11.14", + "aws-cdk": "2.126.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.2", + "ts-node": "^10.9.2", + "typescript": "~5.3.3" + }, + "dependencies": { + "aws-cdk-lib": "^2.262.0", + "cargo-lambda-cdk": "^0.0.35", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + } +} diff --git a/examples/http-axum/cdk/tsconfig.json b/examples/http-axum/cdk/tsconfig.json new file mode 100644 index 000000000..aaa7dc510 --- /dev/null +++ b/examples/http-axum/cdk/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "es2020", + "dom" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +} diff --git a/examples/http-axum/src/main.rs b/examples/http-axum/src/main.rs new file mode 100644 index 000000000..3b00fc57c --- /dev/null +++ b/examples/http-axum/src/main.rs @@ -0,0 +1,77 @@ +//! This is an example function that leverages the Lambda Rust runtime HTTP support +//! and the [axum](https://docs.rs/axum/latest/axum/index.html) web framework. The +//! runtime HTTP support is backed by the [tower::Service](https://docs.rs/tower-service/0.3.2/tower_service/trait.Service.html) +//! trait. Axum's applications are also backed by the `tower::Service` trait. That means +//! that it is fairly easy to build an Axum application and pass the resulting `Service` +//! implementation to the Lambda runtime to run as a Lambda function. By using Axum instead +//! of a basic `tower::Service` you get web framework niceties like routing, request component +//! extraction, validation, etc. +use axum::{ + extract::{Path, Query}, + http::StatusCode, + response::Json, + routing::{get, post}, + Router, +}; +use lambda_http::{run, tracing, Error}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::env::set_var; + +#[derive(Deserialize, Serialize)] +struct Params { + first: Option, + second: Option, +} + +async fn root() -> Json { + Json(json!({ "msg": "I am GET /" })) +} + +async fn get_foo() -> Json { + Json(json!({ "msg": "I am GET /foo" })) +} + +async fn post_foo() -> Json { + Json(json!({ "msg": "I am POST /foo" })) +} + +async fn post_foo_name(Path(name): Path) -> Json { + Json(json!({ "msg": format!("I am POST /foo/:name, name={name}") })) +} + +async fn get_parameters(Query(params): Query) -> Json { + Json(json!({ "request parameters": params })) +} + +/// Example on how to return status codes and data from an Axum function +async fn health_check() -> (StatusCode, String) { + let health = true; + match health { + true => (StatusCode::OK, "Healthy!".to_string()), + false => (StatusCode::INTERNAL_SERVER_ERROR, "Not healthy!".to_string()), + } +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + // If you use API Gateway stages, the Rust Runtime will include the stage name + // as part of the path that your application receives. + // Setting the following environment variable, you can remove the stage from the path. + // This variable only applies to API Gateway stages, + // you can remove it if you don't use them. + // i.e with: `GET /test-stage/todo/id/123` without: `GET /todo/id/123` + set_var("AWS_LAMBDA_HTTP_IGNORE_STAGE_IN_PATH", "true"); + + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); + + let app = Router::new() + .route("/", get(root)) + .route("/foo", get(get_foo).post(post_foo)) + .route("/foo/:name", post(post_foo_name)) + .route("/parameters", get(get_parameters)) + .route("/health/", get(health_check)); + + run(app).await +} diff --git a/examples/http-basic-lambda/Cargo.toml b/examples/http-basic-lambda/Cargo.toml index 1a2183304..2f2523894 100644 --- a/examples/http-basic-lambda/Cargo.toml +++ b/examples/http-basic-lambda/Cargo.toml @@ -3,18 +3,6 @@ name = "http-basic-lambda" version = "0.1.0" edition = "2021" - -# Use cargo-edit(https://github.com/killercup/cargo-edit#installation) -# to manage dependencies. -# Running `cargo add DEPENDENCY_NAME` will -# add the latest version of a dependency to the list, -# and it will keep the alphabetic ordering for you. - [dependencies] lambda_http = { path = "../../lambda-http" } -lambda_runtime = { path = "../../lambda-runtime" } tokio = { version = "1", features = ["macros"] } -tracing = { version = "0.1", features = ["log"] } -tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } - - diff --git a/examples/http-basic-lambda/README.md b/examples/http-basic-lambda/README.md index 5cae85fb9..498f8a500 100644 --- a/examples/http-basic-lambda/README.md +++ b/examples/http-basic-lambda/README.md @@ -3,9 +3,9 @@ ## Build & Deploy 1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) -2. Build the function with `cargo lambda build --release` +2. Build the function with `cargo lambda build --release` 3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` ## Build for ARM 64 -Build the function with `cargo lambda build --release --arm64` +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/http-basic-lambda/src/main.rs b/examples/http-basic-lambda/src/main.rs index df15ae6ce..87376d374 100644 --- a/examples/http-basic-lambda/src/main.rs +++ b/examples/http-basic-lambda/src/main.rs @@ -1,9 +1,9 @@ -use lambda_http::{run, service_fn, Body, Error, Request, Response}; +use lambda_http::{run, service_fn, tracing, Body, Error, Request, Response}; /// This is the main body for the function. /// Write your code inside it. /// There are some code examples in the Runtime repository: -/// - https://github.com/awslabs/aws-lambda-rust-runtime/tree/main/examples +/// - async fn function_handler(_event: Request) -> Result, Error> { // Extract some useful information from the request @@ -19,11 +19,8 @@ async fn function_handler(_event: Request) -> Result, Error> { #[tokio::main] async fn main() -> Result<(), Error> { - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - // disabling time is handy because CloudWatch will add the ingestion time. - .without_time() - .init(); + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); run(service_fn(function_handler)).await } diff --git a/examples/http-cors/Cargo.toml b/examples/http-cors/Cargo.toml index 9fd7f25ba..b9c9efa52 100644 --- a/examples/http-cors/Cargo.toml +++ b/examples/http-cors/Cargo.toml @@ -3,19 +3,7 @@ name = "http-cors" version = "0.1.0" edition = "2021" - -# Use cargo-edit(https://github.com/killercup/cargo-edit#installation) -# to manage dependencies. -# Running `cargo add DEPENDENCY_NAME` will -# add the latest version of a dependency to the list, -# and it will keep the alphabetic ordering for you. - [dependencies] lambda_http = { path = "../../lambda-http" } -lambda_runtime = { path = "../../lambda-runtime" } tokio = { version = "1", features = ["macros"] } -tower-http = { version = "0.3.3", features = ["cors"] } -tracing = { version = "0.1", features = ["log"] } -tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } - - +tower-http = { version = "0.6", features = ["cors"] } diff --git a/examples/http-cors/README.md b/examples/http-cors/README.md index 5cae85fb9..498f8a500 100644 --- a/examples/http-cors/README.md +++ b/examples/http-cors/README.md @@ -3,9 +3,9 @@ ## Build & Deploy 1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) -2. Build the function with `cargo lambda build --release` +2. Build the function with `cargo lambda build --release` 3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` ## Build for ARM 64 -Build the function with `cargo lambda build --release --arm64` +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/http-cors/src/main.rs b/examples/http-cors/src/main.rs index 7e1b21fa9..4c2c54b4c 100644 --- a/examples/http-cors/src/main.rs +++ b/examples/http-cors/src/main.rs @@ -1,17 +1,12 @@ use lambda_http::{ - http::Method, service_fn, tower::ServiceBuilder, Body, Error, IntoResponse, Request, RequestExt, Response, + http::Method, service_fn, tower::ServiceBuilder, tracing, Body, Error, IntoResponse, Request, RequestExt, Response, }; use tower_http::cors::{Any, CorsLayer}; #[tokio::main] async fn main() -> Result<(), Error> { - // The runtime logging can be enabled here by initializing `tracing` with `tracing-subscriber` - // While `tracing` is used internally, `log` can be used as well if preferred. - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - // disabling time is handy because CloudWatch will add the ingestion time. - .without_time() - .init(); + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); // Define a layer to inject CORS headers let cors_layer = CorsLayer::new() @@ -28,11 +23,16 @@ async fn main() -> Result<(), Error> { } async fn func(event: Request) -> Result, Error> { - Ok(match event.query_string_parameters().first("first_name") { - Some(first_name) => format!("Hello, {}!", first_name).into_response().await, - _ => Response::builder() - .status(400) - .body("Empty first name".into()) - .expect("failed to render response"), - }) + Ok( + match event + .query_string_parameters_ref() + .and_then(|params| params.first("first_name")) + { + Some(first_name) => format!("Hello, {first_name}!").into_response().await, + None => Response::builder() + .status(400) + .body("Empty first name".into()) + .expect("failed to render response"), + }, + ) } diff --git a/examples/http-dynamodb/Cargo.toml b/examples/http-dynamodb/Cargo.toml new file mode 100644 index 000000000..172b4ef1f --- /dev/null +++ b/examples/http-dynamodb/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "http-dynamodb" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde_json = "1.0.140" +serde = { version = "1.0.219", features = ["derive"] } +serde_dynamo = { version = "4.2.14", features = ["aws-sdk-dynamodb+1"] } +lambda_http = { path = "../../lambda-http" } +aws-sdk-dynamodb = "1.84.0" +aws-config = { version = "1.8.1", features = ["behavior-version-latest"] } +tokio = { version = "1.46.1", features = ["macros"] } diff --git a/examples/http-dynamodb/README.md b/examples/http-dynamodb/README.md new file mode 100644 index 000000000..072f2bf98 --- /dev/null +++ b/examples/http-dynamodb/README.md @@ -0,0 +1,17 @@ +# AWS Lambda Function example + +## Build & Deploy + +1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the function with `cargo lambda build --release` +3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` + +## Build for ARM 64 + +Build the function with `cargo lambda build --release --arm64` + +Setting up Dynamodb + +1. Log into your account. +2. Create a Dynamodb table with the name 'lambda_dyno_example' with the partition key of "username". +3. Create IAM role with the permissions for Lambda, Cloudwatch and Dynamodb. diff --git a/examples/http-dynamodb/src/main.rs b/examples/http-dynamodb/src/main.rs new file mode 100644 index 000000000..006678a1c --- /dev/null +++ b/examples/http-dynamodb/src/main.rs @@ -0,0 +1,83 @@ +use aws_sdk_dynamodb::Client; +use lambda_http::{run, service_fn, tracing, Body, Error, Request, Response}; +use serde::{Deserialize, Serialize}; +use serde_dynamo::to_item; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Item { + pub account_type: String, + pub age: String, + pub username: String, + pub first_name: String, + pub last_name: String, +} + +/// This is the main body for the function. +/// Write your code inside it. +/// You can see more examples in Runtime's repository: +/// - +async fn handle_request(db_client: &Client, event: Request) -> Result, Error> { + // Extract some useful information from the request + let body = event.body(); + let s = std::str::from_utf8(body).expect("invalid utf-8 sequence"); + //Log into Cloudwatch + tracing::info!(payload = %s, "JSON Payload received"); + + //Serialze JSON into struct. + //If JSON is incorrect, send back 400 with error. + let item = match serde_json::from_str::(s) { + Ok(item) => item, + Err(err) => { + let resp = Response::builder() + .status(400) + .header("content-type", "text/html") + .body(err.to_string().into()) + .map_err(Box::new)?; + return Ok(resp); + } + }; + + //Insert into the table. + add_item(db_client, item.clone(), "lambda_dyno_example").await?; + + //Deserialize into json to return in the Response + let j = serde_json::to_string(&item)?; + + //Send back a 200 - success + let resp = Response::builder() + .status(200) + .header("content-type", "text/html") + .body(j.into()) + .map_err(Box::new)?; + Ok(resp) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); + + //Get config from environment. + let config = aws_config::load_from_env().await; + //Create the DynamoDB client. + let client = Client::new(&config); + + run(service_fn(|event: Request| async { + handle_request(&client, event).await + })) + .await +} + +// Add an item to a table. +// snippet-start:[dynamodb.rust.add-item] +pub async fn add_item(client: &Client, item: Item, table: &str) -> Result<(), Error> { + let item = to_item(item)?; + + let request = client.put_item().table_name(table).set_item(Some(item)); + + tracing::info!("adding item to DynamoDB"); + + let _resp = request.send().await?; + + Ok(()) +} diff --git a/examples/http-query-parameters/Cargo.toml b/examples/http-query-parameters/Cargo.toml index 7aeb1189f..18f8e6cfb 100644 --- a/examples/http-query-parameters/Cargo.toml +++ b/examples/http-query-parameters/Cargo.toml @@ -3,18 +3,6 @@ name = "http-query-parameters" version = "0.1.0" edition = "2021" - -# Use cargo-edit(https://github.com/killercup/cargo-edit#installation) -# to manage dependencies. -# Running `cargo add DEPENDENCY_NAME` will -# add the latest version of a dependency to the list, -# and it will keep the alphabetic ordering for you. - [dependencies] lambda_http = { path = "../../lambda-http" } -lambda_runtime = { path = "../../lambda-runtime" } tokio = { version = "1", features = ["macros"] } -tracing = { version = "0.1", features = ["log"] } -tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } - - diff --git a/examples/http-query-parameters/README.md b/examples/http-query-parameters/README.md index 5cae85fb9..498f8a500 100644 --- a/examples/http-query-parameters/README.md +++ b/examples/http-query-parameters/README.md @@ -3,9 +3,9 @@ ## Build & Deploy 1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) -2. Build the function with `cargo lambda build --release` +2. Build the function with `cargo lambda build --release` 3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` ## Build for ARM 64 -Build the function with `cargo lambda build --release --arm64` +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/http-query-parameters/src/main.rs b/examples/http-query-parameters/src/main.rs index 03e4b9392..98893bea1 100644 --- a/examples/http-query-parameters/src/main.rs +++ b/examples/http-query-parameters/src/main.rs @@ -1,27 +1,29 @@ -use lambda_http::{run, service_fn, Error, IntoResponse, Request, RequestExt, Response}; +use lambda_http::{run, service_fn, tracing, Error, IntoResponse, Request, RequestExt, Response}; /// This is the main body for the function. /// Write your code inside it. /// You can see more examples in Runtime's repository: -/// - https://github.com/awslabs/aws-lambda-rust-runtime/tree/main/examples +/// - async fn function_handler(event: Request) -> Result { // Extract some useful information from the request - Ok(match event.query_string_parameters().first("first_name") { - Some(first_name) => format!("Hello, {}!", first_name).into_response().await, - _ => Response::builder() - .status(400) - .body("Empty first name".into()) - .expect("failed to render response"), - }) + Ok( + match event + .query_string_parameters_ref() + .and_then(|params| params.first("first_name")) + { + Some(first_name) => format!("Hello, {first_name}!").into_response().await, + None => Response::builder() + .status(400) + .body("Empty first name".into()) + .expect("failed to render response"), + }, + ) } #[tokio::main] async fn main() -> Result<(), Error> { - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - // disabling time is handy because CloudWatch will add the ingestion time. - .without_time() - .init(); + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); run(service_fn(function_handler)).await } diff --git a/examples/http-raw-path/Cargo.toml b/examples/http-raw-path/Cargo.toml index f4060428c..d1c5ccb82 100644 --- a/examples/http-raw-path/Cargo.toml +++ b/examples/http-raw-path/Cargo.toml @@ -3,18 +3,6 @@ name = "http-raw-path" version = "0.1.0" edition = "2021" - -# Use cargo-edit(https://github.com/killercup/cargo-edit#installation) -# to manage dependencies. -# Running `cargo add DEPENDENCY_NAME` will -# add the latest version of a dependency to the list, -# and it will keep the alphabetic ordering for you. - [dependencies] lambda_http = { path = "../../lambda-http" } -lambda_runtime = { path = "../../lambda-runtime" } tokio = { version = "1", features = ["macros"] } -tracing = { version = "0.1", features = ["log"] } -tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } - - diff --git a/examples/http-raw-path/README.md b/examples/http-raw-path/README.md index 5cae85fb9..498f8a500 100644 --- a/examples/http-raw-path/README.md +++ b/examples/http-raw-path/README.md @@ -3,9 +3,9 @@ ## Build & Deploy 1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) -2. Build the function with `cargo lambda build --release` +2. Build the function with `cargo lambda build --release` 3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` ## Build for ARM 64 -Build the function with `cargo lambda build --release --arm64` +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/http-raw-path/src/main.rs b/examples/http-raw-path/src/main.rs index f88b7b646..70b4df4d9 100644 --- a/examples/http-raw-path/src/main.rs +++ b/examples/http-raw-path/src/main.rs @@ -1,14 +1,9 @@ -use lambda_http::{service_fn, Error, IntoResponse, Request, RequestExt}; +use lambda_http::{service_fn, tracing, Error, IntoResponse, Request, RequestExt}; #[tokio::main] async fn main() -> Result<(), Error> { - // The runtime logging can be enabled here by initializing `tracing` with `tracing-subscriber` - // While `tracing` is used internally, `log` can be used as well if preferred. - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - // disabling time is handy because CloudWatch will add the ingestion time. - .without_time() - .init(); + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); lambda_http::run(service_fn(func)).await?; Ok(()) diff --git a/examples/http-shared-resource/Cargo.toml b/examples/http-shared-resource/Cargo.toml index 207f253bb..8f5a0e947 100644 --- a/examples/http-shared-resource/Cargo.toml +++ b/examples/http-shared-resource/Cargo.toml @@ -3,18 +3,6 @@ name = "http-shared-resource" version = "0.1.0" edition = "2021" - -# Use cargo-edit(https://github.com/killercup/cargo-edit#installation) -# to manage dependencies. -# Running `cargo add DEPENDENCY_NAME` will -# add the latest version of a dependency to the list, -# and it will keep the alphabetic ordering for you. - [dependencies] lambda_http = { path = "../../lambda-http" } -lambda_runtime = { path = "../../lambda-runtime" } tokio = { version = "1", features = ["macros"] } -tracing = { version = "0.1", features = ["log"] } -tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } - - diff --git a/examples/http-shared-resource/README.md b/examples/http-shared-resource/README.md index 5cae85fb9..498f8a500 100644 --- a/examples/http-shared-resource/README.md +++ b/examples/http-shared-resource/README.md @@ -3,9 +3,9 @@ ## Build & Deploy 1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) -2. Build the function with `cargo lambda build --release` +2. Build the function with `cargo lambda build --release` 3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` ## Build for ARM 64 -Build the function with `cargo lambda build --release --arm64` +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/http-shared-resource/src/main.rs b/examples/http-shared-resource/src/main.rs index 48bab4715..cff9e785a 100644 --- a/examples/http-shared-resource/src/main.rs +++ b/examples/http-shared-resource/src/main.rs @@ -1,4 +1,4 @@ -use lambda_http::{service_fn, Body, Error, IntoResponse, Request, RequestExt, Response}; +use lambda_http::{service_fn, tracing, Body, Error, IntoResponse, Request, RequestExt, Response}; struct SharedClient { name: &'static str, @@ -12,13 +12,8 @@ impl SharedClient { #[tokio::main] async fn main() -> Result<(), Error> { - // The runtime logging can be enabled here by initializing `tracing` with `tracing-subscriber` - // While `tracing` is used internally, `log` can be used as well if preferred. - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - // disabling time is handy because CloudWatch will add the ingestion time. - .without_time() - .init(); + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); // Create the "client" and a reference to it, so that we can pass this into the handler closure below. let shared_client = SharedClient { @@ -28,18 +23,29 @@ async fn main() -> Result<(), Error> { // Define a closure here that makes use of the shared client. let handler_func_closure = move |event: Request| async move { - Result::, Error>::Ok(match event.query_string_parameters().first("first_name") { - Some(first_name) => { - shared_client_ref - .response(event.lambda_context().request_id, first_name) - .into_response() - .await - } - _ => Response::builder() - .status(400) - .body("Empty first name".into()) - .expect("failed to render response"), - }) + Result::, Error>::Ok( + match event + .query_string_parameters_ref() + .and_then(|params| params.first("first_name")) + { + Some(first_name) => { + shared_client_ref + .response( + event + .lambda_context_ref() + .map(|ctx| ctx.request_id.clone()) + .unwrap_or_default(), + first_name, + ) + .into_response() + .await + } + None => Response::builder() + .status(400) + .body("Empty first name".into()) + .expect("failed to render response"), + }, + ) }; // Pass the closure to the runtime here. diff --git a/examples/http-snapstart/Cargo.toml b/examples/http-snapstart/Cargo.toml new file mode 100644 index 000000000..a2bad8b88 --- /dev/null +++ b/examples/http-snapstart/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "http-snapstart" +version = "0.1.0" +edition = "2021" + +[dependencies] +lambda_http = { path = "../../lambda-http" } +tokio = { version = "1", features = ["macros"] } diff --git a/examples/http-snapstart/Dockerfile b/examples/http-snapstart/Dockerfile new file mode 100644 index 000000000..0a72a338e --- /dev/null +++ b/examples/http-snapstart/Dockerfile @@ -0,0 +1,41 @@ +# Container image for the http-snapstart example. +# +# SnapStart for functions packaged as OCI images requires a custom base image +# whose runtime implements the restore lifecycle — which `lambda_runtime` now +# does. This image builds the example as the Lambda `bootstrap` on top of the +# AWS-provided base (`provided:al2023`), which supports SnapStart. +# +# Build from the REPOSITORY ROOT (the example uses path dependencies on the +# workspace crates, so the build context must include them): +# +# docker build -f examples/http-snapstart/Dockerfile -t http-snapstart . +# +# Then push to ECR and create the function with SnapStart enabled +# (PublishedVersions) and the architecture matching your build host. + +# ---- build stage ---- +FROM public.ecr.aws/lambda/provided:al2023 AS builder + +RUN dnf install -y gcc && \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" + +# Copy only the workspace crates this example depends on via path, then the +# example itself: lambda_http -> lambda_runtime -> lambda_runtime_api_client, +# plus lambda-events (the aws_lambda_events crate used by lambda_http). +COPY Cargo.* /build/ +COPY lambda-http /build/lambda-http +COPY lambda-runtime /build/lambda-runtime +COPY lambda-runtime-api-client /build/lambda-runtime-api-client +COPY lambda-events /build/lambda-events +COPY examples/http-snapstart /build/examples/http-snapstart + +WORKDIR /build/examples/http-snapstart +RUN cargo build --release + +# ---- runtime stage ---- +FROM public.ecr.aws/lambda/provided:al2023 + +COPY --from=builder /build/examples/http-snapstart/target/release/http-snapstart ${LAMBDA_RUNTIME_DIR}/bootstrap + +ENTRYPOINT [ "/var/runtime/bootstrap" ] diff --git a/examples/http-snapstart/README.md b/examples/http-snapstart/README.md new file mode 100644 index 000000000..ecf459993 --- /dev/null +++ b/examples/http-snapstart/README.md @@ -0,0 +1,59 @@ +# AWS Lambda SnapStart example (lambda_http) + +This example shows how to use AWS Lambda **SnapStart** with the `lambda_http` +crate. It wraps a shared connection pool in a `SnapStartResource` and registers +it via the [`lambda_http::runtime`] helper, so the runtime drains the pool before +the VM snapshot and reconnects it after each restore. The HTTP handler keeps the +usual `lambda_http` request/response ergonomics. + +SnapStart reduces cold-start latency by snapshotting the initialized execution +environment and restoring from it. Resources created during init (connections, +credentials, unique values) may be invalid after restore — `before_snapshot` / +`after_restore` are where you release and re-establish them. When SnapStart is +not enabled, the hooks are never called and the function behaves normally. + +If you don't need custom hooks, plain `lambda_http::run(...)` already gets +SnapStart support for free: the runtime calls `/restore/next` and rebuilds its +internal RAPID connection pool on restore without any extra code. + +## Why a container image? + +SnapStart for functions packaged as OCI images requires a custom base image +whose runtime implements the restore lifecycle — which `lambda_runtime` (used by +`lambda_http`) now does. This example ships a `Dockerfile` that builds the binary +as the Lambda `bootstrap` on top of `public.ecr.aws/lambda/provided:al2023`, +which supports SnapStart. + +## Build & Deploy + +Build the image from the **repository root** (the example depends on the +workspace crates via path, so the build context must include them): + +```sh +docker build -f examples/http-snapstart/Dockerfile -t http-snapstart . +``` + +Push it to ECR and create the function from the image: + +```sh +aws ecr create-repository --repository-name http-snapstart +docker tag http-snapstart:latest .dkr.ecr..amazonaws.com/http-snapstart:latest +docker push .dkr.ecr..amazonaws.com/http-snapstart:latest + +aws lambda create-function \ + --function-name http-snapstart \ + --package-type Image \ + --code ImageUri=.dkr.ecr..amazonaws.com/http-snapstart:latest \ + --role \ + --snap-start ApplyOn=PublishedVersions +``` + +SnapStart applies to **published versions**, so publish a version to trigger +snapshot creation, then expose it behind a function URL or API Gateway and invoke +it (e.g. `?name=world`). + +## Architecture + +The compiled `bootstrap` is architecture-specific. Build on (or for) the same +architecture as the target function — `provided:al2023` is available for both +`x86_64` and `arm64`. diff --git a/examples/http-snapstart/src/main.rs b/examples/http-snapstart/src/main.rs new file mode 100644 index 000000000..bcfffff61 --- /dev/null +++ b/examples/http-snapstart/src/main.rs @@ -0,0 +1,77 @@ +// This example demonstrates using SnapStart with the lambda_http crate. +// +// Use lambda_http::runtime() to get a Runtime on which you can register +// `SnapStartResource`s. This gives you access to the SnapStart lifecycle while +// keeping the lambda_http request/response ergonomics. +// +// For the simple case where no custom hooks are needed, just use +// lambda_http::run() — SnapStart works automatically. + +use lambda_http::{ + service_fn, tracing, BoxFuture, Body, Error, IntoResponse, Request, RequestExt, Response, SnapStartResource, +}; +use std::sync::Arc; +use tokio::sync::RwLock; + +struct DbPool { + connected: bool, +} + +impl DbPool { + async fn connect() -> Self { + tracing::info!("Establishing database connection pool"); + Self { connected: true } + } +} + +/// A `SnapStartResource` wrapper around the shared connection pool. The handler +/// and the resource share the same `Arc>`, so draining before the +/// snapshot and reconnecting after restore are visible to invocations. +struct PoolResource(Arc>); + +impl SnapStartResource for PoolResource { + fn before_snapshot(&self) -> BoxFuture<'_, Result<(), Error>> { + Box::pin(async move { + tracing::info!("Draining database connections before snapshot"); + self.0.write().await.connected = false; + Ok(()) + }) + } + + fn after_restore(&self) -> BoxFuture<'_, Result<(), Error>> { + Box::pin(async move { + tracing::info!("Re-establishing database connections after restore"); + self.0.write().await.connected = true; + Ok(()) + }) + } +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing::init_default_subscriber(); + + let pool = Arc::new(RwLock::new(DbPool::connect().await)); + + let pool_ref = pool.clone(); + let handler = service_fn(move |req: Request| { + let pool = pool_ref.clone(); + async move { + let pool = pool.read().await; + assert!(pool.connected, "Pool should be connected during invocation"); + + let name = req + .query_string_parameters_ref() + .and_then(|params| params.first("name")) + .unwrap_or("world"); + + Ok::, Error>(format!("Hello, {name}!").into_response().await) + } + }); + + lambda_http::runtime(handler) + .register_snapstart_resource(Arc::new(PoolResource(pool.clone()))) + .run() + .await?; + Ok(()) +} diff --git a/examples/http-tower-trace/Cargo.toml b/examples/http-tower-trace/Cargo.toml index 2b8f7a608..cf1f223bb 100644 --- a/examples/http-tower-trace/Cargo.toml +++ b/examples/http-tower-trace/Cargo.toml @@ -3,17 +3,7 @@ name = "http-tower-trace" version = "0.1.0" edition = "2021" - -# Use cargo-edit(https://github.com/killercup/cargo-edit#installation) -# to manage dependencies. -# Running `cargo add DEPENDENCY_NAME` will -# add the latest version of a dependency to the list, -# and it will keep the alphabetic ordering for you. - [dependencies] lambda_http = { path = "../../lambda-http" } -lambda_runtime = "0.5.1" tokio = { version = "1", features = ["macros"] } -tower-http = { version = "0.3.4", features = ["trace"] } -tracing = { version = "0.1", features = ["log"] } -tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } +tower-http = { version = "0.6", features = ["trace"] } diff --git a/examples/http-tower-trace/src/main.rs b/examples/http-tower-trace/src/main.rs index ec7020c12..7dd0c1d6a 100644 --- a/examples/http-tower-trace/src/main.rs +++ b/examples/http-tower-trace/src/main.rs @@ -1,7 +1,10 @@ -use lambda_http::{run, tower::ServiceBuilder, Error}; -use lambda_http::{Request, Response}; +use lambda_http::{ + run, + tower::ServiceBuilder, + tracing::{self, Level}, + Error, Request, Response, +}; use tower_http::trace::{DefaultOnRequest, DefaultOnResponse, TraceLayer}; -use tracing::Level; async fn handler(_req: Request) -> Result, Error> { Ok(Response::new("Success".into())) @@ -9,7 +12,8 @@ async fn handler(_req: Request) -> Result, Error> { #[tokio::main] async fn main() -> Result<(), Error> { - tracing_subscriber::fmt().without_time().init(); + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); let layer = TraceLayer::new_for_http() .on_request(DefaultOnRequest::new().level(Level::INFO)) diff --git a/examples/invocation-id-concurrent/Cargo.toml b/examples/invocation-id-concurrent/Cargo.toml new file mode 100644 index 000000000..3df86e683 --- /dev/null +++ b/examples/invocation-id-concurrent/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "invocation-id-concurrent" +version = "0.1.0" +edition = "2021" + +[dependencies] +lambda_runtime = { path = "../../lambda-runtime", features = ["concurrency-tokio"] } +serde = "1" +tokio = { version = "1", features = ["macros", "rt", "time"] } + +[dev-dependencies] +serde_json = "1.0" diff --git a/examples/invocation-id-concurrent/src/main.rs b/examples/invocation-id-concurrent/src/main.rs new file mode 100644 index 000000000..acca09a05 --- /dev/null +++ b/examples/invocation-id-concurrent/src/main.rs @@ -0,0 +1,103 @@ +// This example requires the following input to succeed: +// { "command": "do something" } + +use lambda_runtime::{service_fn, tracing, Diagnostic, Error, LambdaEvent}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize)] +struct Request { + #[serde(default, rename = "command")] + _command: String, + sleep: u32, +} + +#[derive(Serialize, Debug, PartialEq)] +struct Response { + from: String, +} + +#[derive(Debug)] +struct HandlerError(String); + +impl std::fmt::Display for HandlerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for Diagnostic { + fn from(e: HandlerError) -> Diagnostic { + Diagnostic { + error_type: "HandlerError".into(), + error_message: e.0, + } + } +} + + +/** + * Cross-wiring protection: duplicate request-id after timeout. + + Timeline: + t=0: Invoke A starts, handler sleeps 7s + t=5: A times out (timeout=5s). Batch 1 completes with timeout error. + t=5: Invoke B starts (same request-id), handler sleeps 4s + t=7: A's handler wakes up, posts stale /response/{same-id} + t=9: B's handler wakes up, posts correct /response/{same-id} + + With invocation-id: A's stale post at t=7 gets 410 Gone. B responds at t=9 correctly. + Without: A's stale response at t=7 is accepted for B (cross-wired). + */ + +#[tokio::main] +async fn main() -> Result<(), Error> { + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); + let max_concurrency = std::env::var("AWS_LAMBDA_MAX_CONCURRENCY").unwrap_or_else(|_| "not set".to_string()); + tracing::info!(AWS_LAMBDA_MAX_CONCURRENCY = %max_concurrency, "starting concurrent handler"); + + let func = service_fn(my_handler); + if let Err(err) = lambda_runtime::run_concurrent(func).await { + tracing::error!(error = %err, "run error"); + return Err(err); + } + Ok(()) +} + +pub(crate) async fn my_handler(event: LambdaEvent) -> Result { + if event.payload.sleep > 0 { + tokio::time::sleep(tokio::time::Duration::from_secs(event.payload.sleep.into())).await; + } + + Ok(Response { + from: event.payload._command, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use lambda_runtime::{Context, LambdaEvent}; + + #[tokio::test] + async fn handler_echoes_marker() { + let event = LambdaEvent { + payload: Request { + _command: "invoke-B".into(), + sleep: 0, + }, + context: Context::default(), + }; + + let result = my_handler(event).await.unwrap(); + + assert_eq!(result, Response { from: "invoke-B".into() }); + } + + #[test] + fn request_defaults_missing_command() { + let request: Request = serde_json::from_str(r#"{"sleep": 0}"#).unwrap(); + + assert_eq!(request._command, ""); + } +} diff --git a/examples/lambda-rds-iam-auth/.gitignore b/examples/lambda-rds-iam-auth/.gitignore new file mode 100644 index 000000000..fbbbb6ebd --- /dev/null +++ b/examples/lambda-rds-iam-auth/.gitignore @@ -0,0 +1,10 @@ +# Rust +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk diff --git a/examples/lambda-rds-iam-auth/Cargo.toml b/examples/lambda-rds-iam-auth/Cargo.toml new file mode 100644 index 000000000..68b0fe929 --- /dev/null +++ b/examples/lambda-rds-iam-auth/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "rds-iam-rust-lambda" +version = "0.1.0" +edition = "2021" + +[dependencies] +lambda_runtime = { path = "../../lambda-runtime" } +serde_json = "1.0.140" +aws-config = { version = "1.8.1", features = ["behavior-version-latest"] } +aws-credential-types = "1.2.3" +aws-sigv4 = "1.3.3" +url = "2.5.4" +tokio = { version = "1.46.1", features = ["full"] } +sqlx = { version = "0.8.6", features = ["tls-rustls", "postgres", "runtime-tokio"] } diff --git a/examples/lambda-rds-iam-auth/cdk/.gitignore b/examples/lambda-rds-iam-auth/cdk/.gitignore new file mode 100644 index 000000000..b2e6f363f --- /dev/null +++ b/examples/lambda-rds-iam-auth/cdk/.gitignore @@ -0,0 +1,134 @@ +# CDK +node_modules +cdk.json + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* diff --git a/examples/lambda-rds-iam-auth/cdk/README.md b/examples/lambda-rds-iam-auth/cdk/README.md new file mode 100644 index 000000000..4378fb427 --- /dev/null +++ b/examples/lambda-rds-iam-auth/cdk/README.md @@ -0,0 +1,8 @@ +# AWS Lambda Function that uses RDS's IAM Authnetication +This example shows how to build and deploy Rust Lambda Function and an RDS instance using AWS CDK and + +Build & Deploy +1. `npm install` +1. `npx cdk deploy` +1. Using the dev instance or using a local Postgres client: connect into the RDS instance as root and create the required Users with permissions `CREATE USER lambda; GRANT rds_iam TO lambda;` +1. Go to the Lambda Function in the AWS console and invoke the lambda function \ No newline at end of file diff --git a/examples/lambda-rds-iam-auth/cdk/app.ts b/examples/lambda-rds-iam-auth/cdk/app.ts new file mode 100644 index 000000000..127afad95 --- /dev/null +++ b/examples/lambda-rds-iam-auth/cdk/app.ts @@ -0,0 +1,105 @@ +import { join } from 'path'; +import * as cdk from 'aws-cdk-lib'; +import * as rds from 'aws-cdk-lib/aws-rds'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import { RustFunction } from '@cdklabs/aws-lambda-rust' + +class LambdaRDSStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // Create a VPC + const vpc = new ec2.Vpc(this, 'VPC'); + + // Admin DB user + const DB_ADMIN_USERNAME = 'root'; + const DB_USERNAME = 'lambda'; + + // Lambda DB user + const DB_NAME = 'foo'; + + // Create an RDS instance + const db = new rds.DatabaseInstance(this, 'Postgres', { + engine: rds.DatabaseInstanceEngine.POSTGRES, + vpc, + vpcSubnets: vpc.selectSubnets({ subnetType: ec2.SubnetType.PUBLIC }), + credentials: rds.Credentials.fromGeneratedSecret(DB_ADMIN_USERNAME), + iamAuthentication: true, + publiclyAccessible: true, + databaseName: DB_NAME, + deleteAutomatedBackups: true, + removalPolicy: cdk.RemovalPolicy.DESTROY + }) + + db.connections.allowFromAnyIpv4(ec2.Port.allTcp()) + + // RDS SSL Cert Lambda Layer alternative to loading the certificates at compile time + /* + const certLayer = new lambda.LayerVersion(this, 'CertLayer', { + description: 'SSL Certificate Layer', + code: lambda.Code.fromAsset('certs'), + compatibleArchitectures: [lambda.Architecture.X86_64, lambda.Architecture.ARM_64] + }); + */ + + const lambdaSG = new ec2.SecurityGroup(this, 'LambdaSG', { + securityGroupName: 'LambdaSG', + allowAllOutbound: true, + vpc: vpc, + }) + // create a rust lambda function + const rustLambdaFunction = new RustFunction(this, "lambda", { + entry: join(__dirname, '..', 'lambda'), + vpc: vpc, + securityGroups: [lambdaSG], + environment: { + DB_HOSTNAME: db.dbInstanceEndpointAddress, + DB_PORT: db.dbInstanceEndpointPort, + DB_NAME: DB_NAME, + DB_USERNAME: DB_USERNAME, + }, + bundling: { + forceDockerBundling: true, + }, + runtime: lambda.Runtime.PROVIDED_AL2023, + timeout: cdk.Duration.seconds(60), + }); + + // MySQL + /* + CREATE USER 'lambda' IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS'; + GRANT ALL PRIVILEGES ON foo.* TO 'lambda'; + ALTER USER 'lambda' REQUIRE SSL; + */ + + // Postgres + /* + CREATE USER db_userx; + GRANT rds_iam TO db_userx; + */ + db.grantConnect(rustLambdaFunction, DB_USERNAME); + db.connections.allowDefaultPortFrom(rustLambdaFunction); + + /* + Dev Instance for initialising the datbase with the above commands + */ + const devInstance = new ec2.Instance(this, 'dev', { + vpc, + vpcSubnets: vpc.selectSubnets({ subnetType: ec2.SubnetType.PUBLIC }), + machineImage: ec2.MachineImage.latestAmazonLinux2023(), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM) + }) + db.grantConnect(devInstance, DB_ADMIN_USERNAME); + db.grantConnect(devInstance, DB_USERNAME); + db.connections.allowDefaultPortFrom(devInstance); + + // Output the Lambda function ARN + new cdk.CfnOutput(this, 'LambdaFunctionConsole', { + value: `https://${this.region}.console.aws.amazon.com/lambda/home?region=${this.region}#/functions/${rustLambdaFunction.functionName}?tab=testing` + }); + } +} + +const app = new cdk.App(); +new LambdaRDSStack(app, 'LambdaRDSStack'); diff --git a/examples/lambda-rds-iam-auth/cdk/package.json b/examples/lambda-rds-iam-auth/cdk/package.json new file mode 100644 index 000000000..ebaf98c5b --- /dev/null +++ b/examples/lambda-rds-iam-auth/cdk/package.json @@ -0,0 +1,13 @@ +{ + "dependencies": { + "@cdklabs/aws-lambda-rust": "0.0.4", + "aws-cdk-lib": "^2.147.0", + "path": "^0.12.7", + "prettier": "^3.3.2", + "rust.aws-cdk-lambda": "^1.2.1", + "ts-node": "^10.9.2" + }, + "devDependencies": { + "@types/node": "^20.14.10" + } +} diff --git a/examples/lambda-rds-iam-auth/cdk/tsconfig.json b/examples/lambda-rds-iam-auth/cdk/tsconfig.json new file mode 100644 index 000000000..72067ecaf --- /dev/null +++ b/examples/lambda-rds-iam-auth/cdk/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "es2020", + "dom" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +} \ No newline at end of file diff --git a/examples/lambda-rds-iam-auth/src/global-bundle.pem b/examples/lambda-rds-iam-auth/src/global-bundle.pem new file mode 100644 index 000000000..de68d41a0 --- /dev/null +++ b/examples/lambda-rds-iam-auth/src/global-bundle.pem @@ -0,0 +1,3028 @@ +-----BEGIN CERTIFICATE----- +MIIEEjCCAvqgAwIBAgIJAM2ZN/+nPi27MA0GCSqGSIb3DQEBCwUAMIGVMQswCQYD +VQQGEwJVUzEQMA4GA1UEBwwHU2VhdHRsZTETMBEGA1UECAwKV2FzaGluZ3RvbjEi +MCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1h +em9uIFJEUzEmMCQGA1UEAwwdQW1hem9uIFJEUyBhZi1zb3V0aC0xIFJvb3QgQ0Ew +HhcNMTkxMDI4MTgwNTU4WhcNMjQxMDI2MTgwNTU4WjCBlTELMAkGA1UEBhMCVVMx +EDAOBgNVBAcMB1NlYXR0bGUxEzARBgNVBAgMCldhc2hpbmd0b24xIjAgBgNVBAoM +GUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4xEzARBgNVBAsMCkFtYXpvbiBSRFMx +JjAkBgNVBAMMHUFtYXpvbiBSRFMgYWYtc291dGgtMSBSb290IENBMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwR2351uPMZaJk2gMGT+1sk8HE9MQh2rc +/sCnbxGn2p1c7Oi9aBbd/GiFijeJb2BXvHU+TOq3d3Jjqepq8tapXVt4ojbTJNyC +J5E7r7KjTktKdLxtBE1MK25aY+IRJjtdU6vG3KiPKUT1naO3xs3yt0F76WVuFivd +9OHv2a+KHvPkRUWIxpmAHuMY9SIIMmEZtVE7YZGx5ah0iO4JzItHcbVR0y0PBH55 +arpFBddpIVHCacp1FUPxSEWkOpI7q0AaU4xfX0fe1BV5HZYRKpBOIp1TtZWvJD+X +jGUtL1BEsT5vN5g9MkqdtYrC+3SNpAk4VtpvJrdjraI/hhvfeXNnAwIDAQABo2Mw +YTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUEEi/ +WWMcBJsoGXg+EZwkQ0MscZQwHwYDVR0jBBgwFoAUEEi/WWMcBJsoGXg+EZwkQ0Ms +cZQwDQYJKoZIhvcNAQELBQADggEBAGDZ5js5Pc/gC58LJrwMPXFhJDBS8QuDm23C +FFUdlqucskwOS3907ErK1ZkmVJCIqFLArHqskFXMAkRZ2PNR7RjWLqBs+0znG5yH +hRKb4DXzhUFQ18UBRcvT6V6zN97HTRsEEaNhM/7k8YLe7P8vfNZ28VIoJIGGgv9D +wQBBvkxQ71oOmAG0AwaGD0ORGUfbYry9Dz4a4IcUsZyRWRMADixgrFv6VuETp26s +/+z+iqNaGWlELBKh3iQCT6Y/1UnkPLO42bxrCSyOvshdkYN58Q2gMTE1SVTqyo8G +Lw8lLAz9bnvUSgHzB3jRrSx6ggF/WRMRYlR++y6LXP4SAsSAaC0= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEEjCCAvqgAwIBAgIJAJYM4LxvTZA6MA0GCSqGSIb3DQEBCwUAMIGVMQswCQYD +VQQGEwJVUzEQMA4GA1UEBwwHU2VhdHRsZTETMBEGA1UECAwKV2FzaGluZ3RvbjEi +MCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1h +em9uIFJEUzEmMCQGA1UEAwwdQW1hem9uIFJEUyBldS1zb3V0aC0xIFJvb3QgQ0Ew +HhcNMTkxMDMwMjAyMDM2WhcNMjQxMDI4MjAyMDM2WjCBlTELMAkGA1UEBhMCVVMx +EDAOBgNVBAcMB1NlYXR0bGUxEzARBgNVBAgMCldhc2hpbmd0b24xIjAgBgNVBAoM +GUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4xEzARBgNVBAsMCkFtYXpvbiBSRFMx +JjAkBgNVBAMMHUFtYXpvbiBSRFMgZXUtc291dGgtMSBSb290IENBMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqM921jXCXeqpRNCS9CBPOe5N7gMaEt+D +s5uR3riZbqzRlHGiF1jZihkXfHAIQewDwy+Yz+Oec1aEZCQMhUHxZJPusuX0cJfj +b+UluFqHIijL2TfXJ3D0PVLLoNTQJZ8+GAPECyojAaNuoHbdVqxhOcznMsXIXVFq +yVLKDGvyKkJjai/iSPDrQMXufg3kWt0ISjNLvsG5IFXgP4gttsM8i0yvRd4QcHoo +DjvH7V3cS+CQqW5SnDrGnHToB0RLskE1ET+oNOfeN9PWOxQprMOX/zmJhnJQlTqD +QP7jcf7SddxrKFjuziFiouskJJyNDsMjt1Lf60+oHZhed2ogTeifGwIDAQABo2Mw +YTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUFBAF +cgJe/BBuZiGeZ8STfpkgRYQwHwYDVR0jBBgwFoAUFBAFcgJe/BBuZiGeZ8STfpkg +RYQwDQYJKoZIhvcNAQELBQADggEBAKAYUtlvDuX2UpZW9i1QgsjFuy/ErbW0dLHU +e/IcFtju2z6RLZ+uF+5A8Kme7IKG1hgt8s+w9TRVQS/7ukQzoK3TaN6XKXRosjtc +o9Rm4gYWM8bmglzY1TPNaiI4HC7546hSwJhubjN0bXCuj/0sHD6w2DkiGuwKNAef +yTu5vZhPkeNyXLykxkzz7bNp2/PtMBnzIp+WpS7uUDmWyScGPohKMq5PqvL59z+L +ZI3CYeMZrJ5VpXUg3fNNIz/83N3G0sk7wr0ohs/kHTP7xPOYB0zD7Ku4HA0Q9Swf +WX0qr6UQgTPMjfYDLffI7aEId0gxKw1eGYc6Cq5JAZ3ipi/cBFc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEEjCCAvqgAwIBAgIJANew34ehz5l8MA0GCSqGSIb3DQEBCwUAMIGVMQswCQYD +VQQGEwJVUzEQMA4GA1UEBwwHU2VhdHRsZTETMBEGA1UECAwKV2FzaGluZ3RvbjEi +MCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1h +em9uIFJEUzEmMCQGA1UEAwwdQW1hem9uIFJEUyBtZS1zb3V0aC0xIFJvb3QgQ0Ew +HhcNMTkwNTEwMjE0ODI3WhcNMjQwNTA4MjE0ODI3WjCBlTELMAkGA1UEBhMCVVMx +EDAOBgNVBAcMB1NlYXR0bGUxEzARBgNVBAgMCldhc2hpbmd0b24xIjAgBgNVBAoM +GUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4xEzARBgNVBAsMCkFtYXpvbiBSRFMx +JjAkBgNVBAMMHUFtYXpvbiBSRFMgbWUtc291dGgtMSBSb290IENBMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp7BYV88MukcY+rq0r79+C8UzkT30fEfT +aPXbx1d6M7uheGN4FMaoYmL+JE1NZPaMRIPTHhFtLSdPccInvenRDIatcXX+jgOk +UA6lnHQ98pwN0pfDUyz/Vph4jBR9LcVkBbe0zdoKKp+HGbMPRU0N2yNrog9gM5O8 +gkU/3O2csJ/OFQNnj4c2NQloGMUpEmedwJMOyQQfcUyt9CvZDfIPNnheUS29jGSw +ERpJe/AENu8Pxyc72jaXQuD+FEi2Ck6lBkSlWYQFhTottAeGvVFNCzKszCntrtqd +rdYUwurYsLTXDHv9nW2hfDUQa0mhXf9gNDOBIVAZugR9NqNRNyYLHQIDAQABo2Mw +YTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU54cf +DjgwBx4ycBH8+/r8WXdaiqYwHwYDVR0jBBgwFoAU54cfDjgwBx4ycBH8+/r8WXda +iqYwDQYJKoZIhvcNAQELBQADggEBAIIMTSPx/dR7jlcxggr+O6OyY49Rlap2laKA +eC/XI4ySP3vQkIFlP822U9Kh8a9s46eR0uiwV4AGLabcu0iKYfXjPkIprVCqeXV7 +ny9oDtrbflyj7NcGdZLvuzSwgl9SYTJp7PVCZtZutsPYlbJrBPHwFABvAkMvRtDB +hitIg4AESDGPoCl94sYHpfDfjpUDMSrAMDUyO6DyBdZH5ryRMAs3lGtsmkkNUrso +aTW6R05681Z0mvkRdb+cdXtKOSuDZPoe2wJJIaz3IlNQNSrB5TImMYgmt6iAsFhv +3vfTSTKrZDNTJn4ybG6pq1zWExoXsktZPylJly6R3RBwV6nwqBM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEBjCCAu6gAwIBAgIJAMc0ZzaSUK51MA0GCSqGSIb3DQEBCwUAMIGPMQswCQYD +VQQGEwJVUzEQMA4GA1UEBwwHU2VhdHRsZTETMBEGA1UECAwKV2FzaGluZ3RvbjEi +MCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1h +em9uIFJEUzEgMB4GA1UEAwwXQW1hem9uIFJEUyBSb290IDIwMTkgQ0EwHhcNMTkw +ODIyMTcwODUwWhcNMjQwODIyMTcwODUwWjCBjzELMAkGA1UEBhMCVVMxEDAOBgNV +BAcMB1NlYXR0bGUxEzARBgNVBAgMCldhc2hpbmd0b24xIjAgBgNVBAoMGUFtYXpv +biBXZWIgU2VydmljZXMsIEluYy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxIDAeBgNV +BAMMF0FtYXpvbiBSRFMgUm9vdCAyMDE5IENBMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEArXnF/E6/Qh+ku3hQTSKPMhQQlCpoWvnIthzX6MK3p5a0eXKZ +oWIjYcNNG6UwJjp4fUXl6glp53Jobn+tWNX88dNH2n8DVbppSwScVE2LpuL+94vY +0EYE/XxN7svKea8YvlrqkUBKyxLxTjh+U/KrGOaHxz9v0l6ZNlDbuaZw3qIWdD/I +6aNbGeRUVtpM6P+bWIoxVl/caQylQS6CEYUk+CpVyJSkopwJlzXT07tMoDL5WgX9 +O08KVgDNz9qP/IGtAcRduRcNioH3E9v981QO1zt/Gpb2f8NqAjUUCUZzOnij6mx9 +McZ+9cWX88CRzR0vQODWuZscgI08NvM69Fn2SQIDAQABo2MwYTAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUc19g2LzLA5j0Kxc0LjZa +pmD/vB8wHwYDVR0jBBgwFoAUc19g2LzLA5j0Kxc0LjZapmD/vB8wDQYJKoZIhvcN +AQELBQADggEBAHAG7WTmyjzPRIM85rVj+fWHsLIvqpw6DObIjMWokpliCeMINZFV +ynfgBKsf1ExwbvJNzYFXW6dihnguDG9VMPpi2up/ctQTN8tm9nDKOy08uNZoofMc +NUZxKCEkVKZv+IL4oHoeayt8egtv3ujJM6V14AstMQ6SwvwvA93EP/Ug2e4WAXHu +cbI1NAbUgVDqp+DRdfvZkgYKryjTWd/0+1fS8X1bBZVWzl7eirNVnHbSH2ZDpNuY +0SBd8dj5F6ld3t58ydZbrTHze7JJOd8ijySAp4/kiu9UfZWuTPABzDa/DSdz9Dk/ +zPW4CXXvhLmE02TA9/HeCw3KEHIwicNuEfw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEEDCCAvigAwIBAgIJAKFMXyltvuRdMA0GCSqGSIb3DQEBCwUAMIGUMQswCQYD +VQQGEwJVUzEQMA4GA1UEBwwHU2VhdHRsZTETMBEGA1UECAwKV2FzaGluZ3RvbjEi +MCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1h +em9uIFJEUzElMCMGA1UEAwwcQW1hem9uIFJEUyBCZXRhIFJvb3QgMjAxOSBDQTAe +Fw0xOTA4MTkxNzM4MjZaFw0yNDA4MTkxNzM4MjZaMIGUMQswCQYDVQQGEwJVUzEQ +MA4GA1UEBwwHU2VhdHRsZTETMBEGA1UECAwKV2FzaGluZ3RvbjEiMCAGA1UECgwZ +QW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEl +MCMGA1UEAwwcQW1hem9uIFJEUyBCZXRhIFJvb3QgMjAxOSBDQTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMkZdnIH9ndatGAcFo+DppGJ1HUt4x+zeO+0 +ZZ29m0sfGetVulmTlv2d5b66e+QXZFWpcPQMouSxxYTW08TbrQiZngKr40JNXftA +atvzBqIImD4II0ZX5UEVj2h98qe/ypW5xaDN7fEa5e8FkYB1TEemPaWIbNXqchcL +tV7IJPr3Cd7Z5gZJlmujIVDPpMuSiNaal9/6nT9oqN+JSM1fx5SzrU5ssg1Vp1vv +5Xab64uOg7wCJRB9R2GC9XD04odX6VcxUAGrZo6LR64ZSifupo3l+R5sVOc5i8NH +skdboTzU9H7+oSdqoAyhIU717PcqeDum23DYlPE2nGBWckE+eT8CAwEAAaNjMGEw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFK2hDBWl +sbHzt/EHd0QYOooqcFPhMB8GA1UdIwQYMBaAFK2hDBWlsbHzt/EHd0QYOooqcFPh +MA0GCSqGSIb3DQEBCwUAA4IBAQAO/718k8EnOqJDx6wweUscGTGL/QdKXUzTVRAx +JUsjNUv49mH2HQVEW7oxszfH6cPCaupNAddMhQc4C/af6GHX8HnqfPDk27/yBQI+ +yBBvIanGgxv9c9wBbmcIaCEWJcsLp3HzXSYHmjiqkViXwCpYfkoV3Ns2m8bp+KCO +y9XmcCKRaXkt237qmoxoh2sGmBHk2UlQtOsMC0aUQ4d7teAJG0q6pbyZEiPyKZY1 +XR/UVxMJL0Q4iVpcRS1kaNCMfqS2smbLJeNdsan8pkw1dvPhcaVTb7CvjhJtjztF +YfDzAI5794qMlWxwilKMmUvDlPPOTen8NNHkLwWvyFCH7Doh +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEFjCCAv6gAwIBAgIJAMzYZJ+R9NBVMA0GCSqGSIb3DQEBCwUAMIGXMQswCQYD +VQQGEwJVUzEQMA4GA1UEBwwHU2VhdHRsZTETMBEGA1UECAwKV2FzaGluZ3RvbjEi +MCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1h +em9uIFJEUzEoMCYGA1UEAwwfQW1hem9uIFJEUyBQcmV2aWV3IFJvb3QgMjAxOSBD +QTAeFw0xOTA4MjEyMjI5NDlaFw0yNDA4MjEyMjI5NDlaMIGXMQswCQYDVQQGEwJV +UzEQMA4GA1UEBwwHU2VhdHRsZTETMBEGA1UECAwKV2FzaGluZ3RvbjEiMCAGA1UE +CgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJE +UzEoMCYGA1UEAwwfQW1hem9uIFJEUyBQcmV2aWV3IFJvb3QgMjAxOSBDQTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM7kkS6vjgKKQTPynC2NjdN5aPPV +O71G0JJS/2ARVBVJd93JLiGovVJilfWYfwZCs4gTRSSjrUD4D4HyqCd6A+eEEtJq +M0DEC7i0dC+9WNTsPszuB206Jy2IUmxZMIKJAA1NHSbIMjB+b6/JhbSUi7nKdbR/ +brj83bF+RoSA+ogrgX7mQbxhmFcoZN9OGaJgYKsKWUt5Wqv627KkGodUK8mDepgD +S3ZfoRQRx3iceETpcmHJvaIge6+vyDX3d9Z22jmvQ4AKv3py2CmU2UwuhOltFDwB +0ddtb39vgwrJxaGfiMRHpEP1DfNLWHAnA69/pgZPwIggidS+iBPUhgucMp8CAwEA +AaNjMGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FGnTGpQuQ2H/DZlXMQijZEhjs7TdMB8GA1UdIwQYMBaAFGnTGpQuQ2H/DZlXMQij +ZEhjs7TdMA0GCSqGSIb3DQEBCwUAA4IBAQC3xz1vQvcXAfpcZlngiRWeqU8zQAMQ +LZPCFNv7PVk4pmqX+ZiIRo4f9Zy7TrOVcboCnqmP/b/mNq0gVF4O+88jwXJZD+f8 +/RnABMZcnGU+vK0YmxsAtYU6TIb1uhRFmbF8K80HHbj9vSjBGIQdPCbvmR2zY6VJ +BYM+w9U9hp6H4DVMLKXPc1bFlKA5OBTgUtgkDibWJKFOEPW3UOYwp9uq6pFoN0AO +xMTldqWFsOF3bJIlvOY0c/1EFZXu3Ns6/oCP//Ap9vumldYMUZWmbK+gK33FPOXV +8BQ6jNC29icv7lLDpRPwjibJBXX+peDR5UK4FdYcswWEB1Tix5X8dYu6 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZUxCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSYwJAYDVQQDDB1BbWF6b24gUkRTIGFmLXNvdXRoLTEgUm9vdCBDQTAeFw0xOTEw +MjgxODA2NTNaFw0yNDEwMjgxODA2NTNaMIGQMQswCQYDVQQGEwJVUzETMBEGA1UE +CAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9u +IFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEhMB8GA1UE +AwwYQW1hem9uIFJEUyBhZi1zb3V0aC0xIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAvtV1OqmFa8zCVQSKOvPUJERLVFtd4rZmDpImc5rIoeBk7w/P +9lcKUJjO8R/w1a2lJXx3oQ81tiY0Piw6TpT62YWVRMWrOw8+Vxq1dNaDSFp9I8d0 +UHillSSbOk6FOrPDp+R6AwbGFqUDebbN5LFFoDKbhNmH1BVS0a6YNKpGigLRqhka +cClPslWtPqtjbaP3Jbxl26zWzLo7OtZl98dR225pq8aApNBwmtgA7Gh60HK/cX0t +32W94n8D+GKSg6R4MKredVFqRTi9hCCNUu0sxYPoELuM+mHiqB5NPjtm92EzCWs+ ++vgWhMc6GxG+82QSWx1Vj8sgLqtE/vLrWddf5QIDAQABo2YwZDAOBgNVHQ8BAf8E +BAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUuLB4gYVJrSKJj/Gz +pqc6yeA+RcAwHwYDVR0jBBgwFoAUEEi/WWMcBJsoGXg+EZwkQ0MscZQwDQYJKoZI +hvcNAQELBQADggEBABauYOZxUhe9/RhzGJ8MsWCz8eKcyDVd4FCnY6Qh+9wcmYNT +LtnD88LACtJKb/b81qYzcB0Em6+zVJ3Z9jznfr6buItE6es9wAoja22Xgv44BTHL +rimbgMwpTt3uEMXDffaS0Ww6YWb3pSE0XYI2ISMWz+xRERRf+QqktSaL39zuiaW5 +tfZMre+YhohRa/F0ZQl3RCd6yFcLx4UoSPqQsUl97WhYzwAxZZfwvLJXOc4ATt3u +VlCUylNDkaZztDJc/yN5XQoK9W5nOt2cLu513MGYKbuarQr8f+gYU8S+qOyuSRSP +NRITzwCRVnsJE+2JmcRInn/NcanB7uOGqTvJ9+c= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZUxCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSYwJAYDVQQDDB1BbWF6b24gUkRTIGV1LXNvdXRoLTEgUm9vdCBDQTAeFw0xOTEw +MzAyMDIxMzBaFw0yNDEwMzAyMDIxMzBaMIGQMQswCQYDVQQGEwJVUzETMBEGA1UE +CAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9u +IFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEhMB8GA1UE +AwwYQW1hem9uIFJEUyBldS1zb3V0aC0xIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAtEyjYcajx6xImJn8Vz1zjdmL4ANPgQXwF7+tF7xccmNAZETb +bzb3I9i5fZlmrRaVznX+9biXVaGxYzIUIR3huQ3Q283KsDYnVuGa3mk690vhvJbB +QIPgKa5mVwJppnuJm78KqaSpi0vxyCPe3h8h6LLFawVyWrYNZ4okli1/U582eef8 +RzJp/Ear3KgHOLIiCdPDF0rjOdCG1MOlDLixVnPn9IYOciqO+VivXBg+jtfc5J+L +AaPm0/Yx4uELt1tkbWkm4BvTU/gBOODnYziITZM0l6Fgwvbwgq5duAtKW+h031lC +37rEvrclqcp4wrsUYcLAWX79ZyKIlRxcAdvEhQIDAQABo2YwZDAOBgNVHQ8BAf8E +BAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU7zPyc0azQxnBCe7D +b9KAadH1QSEwHwYDVR0jBBgwFoAUFBAFcgJe/BBuZiGeZ8STfpkgRYQwDQYJKoZI +hvcNAQELBQADggEBAFGaNiYxg7yC/xauXPlaqLCtwbm2dKyK9nIFbF/7be8mk7Q3 +MOA0of1vGHPLVQLr6bJJpD9MAbUcm4cPAwWaxwcNpxOjYOFDaq10PCK4eRAxZWwF +NJRIRmGsl8NEsMNTMCy8X+Kyw5EzH4vWFl5Uf2bGKOeFg0zt43jWQVOX6C+aL3Cd +pRS5MhmYpxMG8irrNOxf4NVFE2zpJOCm3bn0STLhkDcV/ww4zMzObTJhiIb5wSWn +EXKKWhUXuRt7A2y1KJtXpTbSRHQxE++69Go1tWhXtRiULCJtf7wF2Ksm0RR/AdXT +1uR1vKyH5KBJPX3ppYkQDukoHTFR0CpB+G84NLo= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZUxCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSYwJAYDVQQDDB1BbWF6b24gUkRTIG1lLXNvdXRoLTEgUm9vdCBDQTAeFw0xOTA1 +MTAyMTU4NDNaFw0yNTA2MDExMjAwMDBaMIGQMQswCQYDVQQGEwJVUzETMBEGA1UE +CAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9u +IFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEhMB8GA1UE +AwwYQW1hem9uIFJEUyBtZS1zb3V0aC0xIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAudOYPZH+ihJAo6hNYMB5izPVBe3TYhnZm8+X3IoaaYiKtsp1 +JJhkTT0CEejYIQ58Fh4QrMUyWvU8qsdK3diNyQRoYLbctsBPgxBR1u07eUJDv38/ +C1JlqgHmMnMi4y68Iy7ymv50QgAMuaBqgEBRI1R6Lfbyrb2YvH5txjJyTVMwuCfd +YPAtZVouRz0JxmnfsHyxjE+So56uOKTDuw++Ho4HhZ7Qveej7XB8b+PIPuroknd3 +FQB5RVbXRvt5ZcVD4F2fbEdBniF7FAF4dEiofVCQGQ2nynT7dZdEIPfPdH3n7ZmE +lAOmwHQ6G83OsiHRBLnbp+QZRgOsjkHJxT20bQIDAQABo2YwZDAOBgNVHQ8BAf8E +BAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUOEVDM7VomRH4HVdA +QvIMNq2tXOcwHwYDVR0jBBgwFoAU54cfDjgwBx4ycBH8+/r8WXdaiqYwDQYJKoZI +hvcNAQELBQADggEBAHhvMssj+Th8IpNePU6RH0BiL6o9c437R3Q4IEJeFdYL+nZz +PW/rELDPvLRUNMfKM+KzduLZ+l29HahxefejYPXtvXBlq/E/9czFDD4fWXg+zVou +uDXhyrV4kNmP4S0eqsAP/jQHPOZAMFA4yVwO9hlqmePhyDnszCh9c1PfJSBh49+b +4w7i/L3VBOMt8j3EKYvqz0gVfpeqhJwL4Hey8UbVfJRFJMJzfNHpePqtDRAY7yjV +PYquRaV2ab/E+/7VFkWMM4tazYz/qsYA2jSH+4xDHvYk8LnsbcrF9iuidQmEc5sb +FgcWaSKG4DJjcI5k7AJLWcXyTDt21Ci43LE+I9Q= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECDCCAvCgAwIBAgICVIYwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MDQxNzEz +MDRaFw0yNDA4MjIxNzA4NTBaMIGVMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEmMCQGA1UEAwwdQW1h +em9uIFJEUyBhcC1zb3V0aC0xIDIwMTkgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDUYOz1hGL42yUCrcsMSOoU8AeD/3KgZ4q7gP+vAz1WnY9K/kim +eWN/2Qqzlo3+mxSFQFyD4MyV3+CnCPnBl9Sh1G/F6kThNiJ7dEWSWBQGAB6HMDbC +BaAsmUc1UIz8sLTL3fO+S9wYhA63Wun0Fbm/Rn2yk/4WnJAaMZcEtYf6e0KNa0LM +p/kN/70/8cD3iz3dDR8zOZFpHoCtf0ek80QqTich0A9n3JLxR6g6tpwoYviVg89e +qCjQ4axxOkWWeusLeTJCcY6CkVyFvDAKvcUl1ytM5AiaUkXblE7zDFXRM4qMMRdt +lPm8d3pFxh0fRYk8bIKnpmtOpz3RIctDrZZxAgMBAAGjZjBkMA4GA1UdDwEB/wQE +AwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBT99wKJftD3jb4sHoHG +i3uGlH6W6TAfBgNVHSMEGDAWgBRzX2DYvMsDmPQrFzQuNlqmYP+8HzANBgkqhkiG +9w0BAQsFAAOCAQEAZ17hhr3dII3hUfuHQ1hPWGrpJOX/G9dLzkprEIcCidkmRYl+ +hu1Pe3caRMh/17+qsoEErmnVq5jNY9X1GZL04IZH8YbHc7iRHw3HcWAdhN8633+K +jYEB2LbJ3vluCGnCejq9djDb6alOugdLMJzxOkHDhMZ6/gYbECOot+ph1tQuZXzD +tZ7prRsrcuPBChHlPjmGy8M9z8u+kF196iNSUGC4lM8vLkHM7ycc1/ZOwRq9aaTe +iOghbQQyAEe03MWCyDGtSmDfr0qEk+CHN+6hPiaL8qKt4s+V9P7DeK4iW08ny8Ox +AVS7u0OK/5+jKMAMrKwpYrBydOjTUTHScocyNw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEBzCCAu+gAwIBAgICQ2QwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MDUxODQ2 +MjlaFw0yNDA4MjIxNzA4NTBaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzElMCMGA1UEAwwcQW1h +em9uIFJEUyBzYS1lYXN0LTEgMjAxOSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAMMvR+ReRnOzqJzoaPipNTt1Z2VA968jlN1+SYKUrYM3No+Vpz0H +M6Tn0oYB66ByVsXiGc28ulsqX1HbHsxqDPwvQTKvO7SrmDokoAkjJgLocOLUAeld +5AwvUjxGRP6yY90NV7X786MpnYb2Il9DIIaV9HjCmPt+rjy2CZjS0UjPjCKNfB8J +bFjgW6GGscjeyGb/zFwcom5p4j0rLydbNaOr9wOyQrtt3ZQWLYGY9Zees/b8pmcc +Jt+7jstZ2UMV32OO/kIsJ4rMUn2r/uxccPwAc1IDeRSSxOrnFKhW3Cu69iB3bHp7 +JbawY12g7zshE4I14sHjv3QoXASoXjx4xgMCAwEAAaNmMGQwDgYDVR0PAQH/BAQD +AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFI1Fc/Ql2jx+oJPgBVYq +ccgP0pQ8MB8GA1UdIwQYMBaAFHNfYNi8ywOY9CsXNC42WqZg/7wfMA0GCSqGSIb3 +DQEBCwUAA4IBAQB4VVVabVp70myuYuZ3vltQIWqSUMhkaTzehMgGcHjMf9iLoZ/I +93KiFUSGnek5cRePyS9wcpp0fcBT3FvkjpUdCjVtdttJgZFhBxgTd8y26ImdDDMR +4+BUuhI5msvjL08f+Vkkpu1GQcGmyFVPFOy/UY8iefu+QyUuiBUnUuEDd49Hw0Fn +/kIPII6Vj82a2mWV/Q8e+rgN8dIRksRjKI03DEoP8lhPlsOkhdwU6Uz9Vu6NOB2Q +Ls1kbcxAc7cFSyRVJEhh12Sz9d0q/CQSTFsVJKOjSNQBQfVnLz1GwO/IieUEAr4C +jkTntH0r1LX5b/GwN4R887LvjAEdTbg1his7 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECDCCAvCgAwIBAgIDAIkHMA0GCSqGSIb3DQEBCwUAMIGPMQswCQYDVQQGEwJV +UzEQMA4GA1UEBwwHU2VhdHRsZTETMBEGA1UECAwKV2FzaGluZ3RvbjEiMCAGA1UE +CgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJE +UzEgMB4GA1UEAwwXQW1hem9uIFJEUyBSb290IDIwMTkgQ0EwHhcNMTkwOTA2MTc0 +MDIxWhcNMjQwODIyMTcwODUwWjCBlDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldh +c2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUxIjAgBgNVBAoMGUFtYXpvbiBXZWIg +U2VydmljZXMsIEluYy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxJTAjBgNVBAMMHEFt +YXpvbiBSRFMgdXMtd2VzdC0xIDIwMTkgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDD2yzbbAl77OofTghDMEf624OvU0eS9O+lsdO0QlbfUfWa1Kd6 +0WkgjkLZGfSRxEHMCnrv4UPBSK/Qwn6FTjkDLgemhqBtAnplN4VsoDL+BkRX4Wwq +/dSQJE2b+0hm9w9UMVGFDEq1TMotGGTD2B71eh9HEKzKhGzqiNeGsiX4VV+LJzdH +uM23eGisNqmd4iJV0zcAZ+Gbh2zK6fqTOCvXtm7Idccv8vZZnyk1FiWl3NR4WAgK +AkvWTIoFU3Mt7dIXKKClVmvssG8WHCkd3Xcb4FHy/G756UZcq67gMMTX/9fOFM/v +l5C0+CHl33Yig1vIDZd+fXV1KZD84dEJfEvHAgMBAAGjZjBkMA4GA1UdDwEB/wQE +AwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBR+ap20kO/6A7pPxo3+ +T3CfqZpQWjAfBgNVHSMEGDAWgBRzX2DYvMsDmPQrFzQuNlqmYP+8HzANBgkqhkiG +9w0BAQsFAAOCAQEAHCJky2tPjPttlDM/RIqExupBkNrnSYnOK4kr9xJ3sl8UF2DA +PAnYsjXp3rfcjN/k/FVOhxwzi3cXJF/2Tjj39Bm/OEfYTOJDNYtBwB0VVH4ffa/6 +tZl87jaIkrxJcreeeHqYMnIxeN0b/kliyA+a5L2Yb0VPjt9INq34QDc1v74FNZ17 +4z8nr1nzg4xsOWu0Dbjo966lm4nOYIGBRGOKEkHZRZ4mEiMgr3YLkv8gSmeitx57 +Z6dVemNtUic/LVo5Iqw4n3TBS0iF2C1Q1xT/s3h+0SXZlfOWttzSluDvoMv5PvCd +pFjNn+aXLAALoihL1MJSsxydtsLjOBro5eK0Vw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEDDCCAvSgAwIBAgICOFAwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTAxNzQ2 +MjFaFw0yNDA4MjIxNzA4NTBaMIGZMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEqMCgGA1UEAwwhQW1h +em9uIFJEUyBhcC1ub3J0aGVhc3QtMiAyMDE5IENBMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAzU72e6XbaJbi4HjJoRNjKxzUEuChKQIt7k3CWzNnmjc5 +8I1MjCpa2W1iw1BYVysXSNSsLOtUsfvBZxi/1uyMn5ZCaf9aeoA9UsSkFSZBjOCN +DpKPCmfV1zcEOvJz26+1m8WDg+8Oa60QV0ou2AU1tYcw98fOQjcAES0JXXB80P2s +3UfkNcnDz+l4k7j4SllhFPhH6BQ4lD2NiFAP4HwoG6FeJUn45EPjzrydxjq6v5Fc +cQ8rGuHADVXotDbEhaYhNjIrsPL+puhjWfhJjheEw8c4whRZNp6gJ/b6WEes/ZhZ +h32DwsDsZw0BfRDUMgUn8TdecNexHUw8vQWeC181hwIDAQABo2YwZDAOBgNVHQ8B +Af8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUwW9bWgkWkr0U +lrOsq2kvIdrECDgwHwYDVR0jBBgwFoAUc19g2LzLA5j0Kxc0LjZapmD/vB8wDQYJ +KoZIhvcNAQELBQADggEBAEugF0Gj7HVhX0ehPZoGRYRt3PBuI2YjfrrJRTZ9X5wc +9T8oHmw07mHmNy1qqWvooNJg09bDGfB0k5goC2emDiIiGfc/kvMLI7u+eQOoMKj6 +mkfCncyRN3ty08Po45vTLBFZGUvtQmjM6yKewc4sXiASSBmQUpsMbiHRCL72M5qV +obcJOjGcIdDTmV1BHdWT+XcjynsGjUqOvQWWhhLPrn4jWe6Xuxll75qlrpn3IrIx +CRBv/5r7qbcQJPOgwQsyK4kv9Ly8g7YT1/vYBlR3cRsYQjccw5ceWUj2DrMVWhJ4 +prf+E3Aa4vYmLLOUUvKnDQ1k3RGNu56V0tonsQbfsaM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgICEzUwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTAyMDUy +MjVaFw0yNDA4MjIxNzA4NTBaMIGXMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEoMCYGA1UEAwwfQW1h +em9uIFJEUyBjYS1jZW50cmFsLTEgMjAxOSBDQTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAOxHqdcPSA2uBjsCP4DLSlqSoPuQ/X1kkJLusVRKiQE2zayB +viuCBt4VB9Qsh2rW3iYGM+usDjltGnI1iUWA5KHcvHszSMkWAOYWLiMNKTlg6LCp +XnE89tvj5dIH6U8WlDvXLdjB/h30gW9JEX7S8supsBSci2GxEzb5mRdKaDuuF/0O +qvz4YE04pua3iZ9QwmMFuTAOYzD1M72aOpj+7Ac+YLMM61qOtU+AU6MndnQkKoQi +qmUN2A9IFaqHFzRlSdXwKCKUA4otzmz+/N3vFwjb5F4DSsbsrMfjeHMo6o/nb6Nh +YDb0VJxxPee6TxSuN7CQJ2FxMlFUezcoXqwqXD0CAwEAAaNmMGQwDgYDVR0PAQH/ +BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFDGGpon9WfIpsggE +CxHq8hZ7E2ESMB8GA1UdIwQYMBaAFHNfYNi8ywOY9CsXNC42WqZg/7wfMA0GCSqG +SIb3DQEBCwUAA4IBAQAvpeQYEGZvoTVLgV9rd2+StPYykMsmFjWQcyn3dBTZRXC2 +lKq7QhQczMAOhEaaN29ZprjQzsA2X/UauKzLR2Uyqc2qOeO9/YOl0H3qauo8C/W9 +r8xqPbOCDLEXlOQ19fidXyyEPHEq5WFp8j+fTh+s8WOx2M7IuC0ANEetIZURYhSp +xl9XOPRCJxOhj7JdelhpweX0BJDNHeUFi0ClnFOws8oKQ7sQEv66d5ddxqqZ3NVv +RbCvCtEutQMOUMIuaygDlMn1anSM8N7Wndx8G6+Uy67AnhjGx7jw/0YPPxopEj6x +JXP8j0sJbcT9K/9/fPVLNT25RvQ/93T2+IQL4Ca2 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEBzCCAu+gAwIBAgICYpgwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTExNzMx +NDhaFw0yNDA4MjIxNzA4NTBaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzElMCMGA1UEAwwcQW1h +em9uIFJEUyBldS13ZXN0LTEgMjAxOSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAMk3YdSZ64iAYp6MyyKtYJtNzv7zFSnnNf6vv0FB4VnfITTMmOyZ +LXqKAT2ahZ00hXi34ewqJElgU6eUZT/QlzdIu359TEZyLVPwURflL6SWgdG01Q5X +O++7fSGcBRyIeuQWs9FJNIIqK8daF6qw0Rl5TXfu7P9dBc3zkgDXZm2DHmxGDD69 +7liQUiXzoE1q2Z9cA8+jirDioJxN9av8hQt12pskLQumhlArsMIhjhHRgF03HOh5 +tvi+RCfihVOxELyIRTRpTNiIwAqfZxxTWFTgfn+gijTmd0/1DseAe82aYic8JbuS +EMbrDduAWsqrnJ4GPzxHKLXX0JasCUcWyMECAwEAAaNmMGQwDgYDVR0PAQH/BAQD +AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFPLtsq1NrwJXO13C9eHt +sLY11AGwMB8GA1UdIwQYMBaAFHNfYNi8ywOY9CsXNC42WqZg/7wfMA0GCSqGSIb3 +DQEBCwUAA4IBAQAnWBKj5xV1A1mYd0kIgDdkjCwQkiKF5bjIbGkT3YEFFbXoJlSP +0lZZ/hDaOHI8wbLT44SzOvPEEmWF9EE7SJzkvSdQrUAWR9FwDLaU427ALI3ngNHy +lGJ2hse1fvSRNbmg8Sc9GBv8oqNIBPVuw+AJzHTacZ1OkyLZrz1c1QvwvwN2a+Jd +vH0V0YIhv66llKcYDMUQJAQi4+8nbRxXWv6Gq3pvrFoorzsnkr42V3JpbhnYiK+9 +nRKd4uWl62KRZjGkfMbmsqZpj2fdSWMY1UGyN1k+kDmCSWYdrTRDP0xjtIocwg+A +J116n4hV/5mbA0BaPiS2krtv17YAeHABZcvz +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgICV2YwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTExOTM2 +MjBaFw0yNDA4MjIxNzA4NTBaMIGXMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEoMCYGA1UEAwwfQW1h +em9uIFJEUyBldS1jZW50cmFsLTEgMjAxOSBDQTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAMEx54X2pHVv86APA0RWqxxRNmdkhAyp2R1cFWumKQRofoFv +n+SPXdkpIINpMuEIGJANozdiEz7SPsrAf8WHyD93j/ZxrdQftRcIGH41xasetKGl +I67uans8d+pgJgBKGb/Z+B5m+UsIuEVekpvgpwKtmmaLFC/NCGuSsJoFsRqoa6Gh +m34W6yJoY87UatddCqLY4IIXaBFsgK9Q/wYzYLbnWM6ZZvhJ52VMtdhcdzeTHNW0 +5LGuXJOF7Ahb4JkEhoo6TS2c0NxB4l4MBfBPgti+O7WjR3FfZHpt18A6Zkq6A2u6 +D/oTSL6c9/3sAaFTFgMyL3wHb2YlW0BPiljZIqECAwEAAaNmMGQwDgYDVR0PAQH/ +BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFOcAToAc6skWffJa +TnreaswAfrbcMB8GA1UdIwQYMBaAFHNfYNi8ywOY9CsXNC42WqZg/7wfMA0GCSqG +SIb3DQEBCwUAA4IBAQA1d0Whc1QtspK496mFWfFEQNegLh0a9GWYlJm+Htcj5Nxt +DAIGXb+8xrtOZFHmYP7VLCT5Zd2C+XytqseK/+s07iAr0/EPF+O2qcyQWMN5KhgE +cXw2SwuP9FPV3i+YAm11PBVeenrmzuk9NrdHQ7TxU4v7VGhcsd2C++0EisrmquWH +mgIfmVDGxphwoES52cY6t3fbnXmTkvENvR+h3rj+fUiSz0aSo+XZUGHPgvuEKM/W +CBD9Smc9CBoBgvy7BgHRgRUmwtABZHFUIEjHI5rIr7ZvYn+6A0O6sogRfvVYtWFc +qpyrW1YX8mD0VlJ8fGKM3G+aCOsiiPKDV/Uafrm+ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECDCCAvCgAwIBAgICGAcwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTIxODE5 +NDRaFw0yNDA4MjIxNzA4NTBaMIGVMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEmMCQGA1UEAwwdQW1h +em9uIFJEUyBldS1ub3J0aC0xIDIwMTkgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCiIYnhe4UNBbdBb/nQxl5giM0XoVHWNrYV5nB0YukA98+TPn9v +Aoj1RGYmtryjhrf01Kuv8SWO+Eom95L3zquoTFcE2gmxCfk7bp6qJJ3eHOJB+QUO +XsNRh76fwDzEF1yTeZWH49oeL2xO13EAx4PbZuZpZBttBM5zAxgZkqu4uWQczFEs +JXfla7z2fvWmGcTagX10O5C18XaFroV0ubvSyIi75ue9ykg/nlFAeB7O0Wxae88e +uhiBEFAuLYdqWnsg3459NfV8Yi1GnaitTym6VI3tHKIFiUvkSiy0DAlAGV2iiyJE +q+DsVEO4/hSINJEtII4TMtysOsYPpINqeEzRAgMBAAGjZjBkMA4GA1UdDwEB/wQE +AwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRR0UpnbQyjnHChgmOc +hnlc0PogzTAfBgNVHSMEGDAWgBRzX2DYvMsDmPQrFzQuNlqmYP+8HzANBgkqhkiG +9w0BAQsFAAOCAQEAKJD4xVzSf4zSGTBJrmamo86jl1NHQxXUApAZuBZEc8tqC6TI +T5CeoSr9CMuVC8grYyBjXblC4OsM5NMvmsrXl/u5C9dEwtBFjo8mm53rOOIm1fxl +I1oYB/9mtO9ANWjkykuLzWeBlqDT/i7ckaKwalhLODsRDO73vRhYNjsIUGloNsKe +pxw3dzHwAZx4upSdEVG4RGCZ1D0LJ4Gw40OfD69hfkDfRVVxKGrbEzqxXRvovmDc +tKLdYZO/6REoca36v4BlgIs1CbUXJGLSXUwtg7YXGLSVBJ/U0+22iGJmBSNcoyUN +cjPFD9JQEhDDIYYKSGzIYpvslvGc4T5ISXFiuQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEBzCCAu+gAwIBAgICZIEwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTIyMTMy +MzJaFw0yNDA4MjIxNzA4NTBaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzElMCMGA1UEAwwcQW1h +em9uIFJEUyBldS13ZXN0LTIgMjAxOSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBALGiwqjiF7xIjT0Sx7zB3764K2T2a1DHnAxEOr+/EIftWKxWzT3u +PFwS2eEZcnKqSdRQ+vRzonLBeNLO4z8aLjQnNbkizZMBuXGm4BqRm1Kgq3nlLDQn +7YqdijOq54SpShvR/8zsO4sgMDMmHIYAJJOJqBdaus2smRt0NobIKc0liy7759KB +6kmQ47Gg+kfIwxrQA5zlvPLeQImxSoPi9LdbRoKvu7Iot7SOa+jGhVBh3VdqndJX +7tm/saj4NE375csmMETFLAOXjat7zViMRwVorX4V6AzEg1vkzxXpA9N7qywWIT5Y +fYaq5M8i6vvLg0CzrH9fHORtnkdjdu1y+0MCAwEAAaNmMGQwDgYDVR0PAQH/BAQD +AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFFOhOx1yt3Z7mvGB9jBv +2ymdZwiOMB8GA1UdIwQYMBaAFHNfYNi8ywOY9CsXNC42WqZg/7wfMA0GCSqGSIb3 +DQEBCwUAA4IBAQBehqY36UGDvPVU9+vtaYGr38dBbp+LzkjZzHwKT1XJSSUc2wqM +hnCIQKilonrTIvP1vmkQi8qHPvDRtBZKqvz/AErW/ZwQdZzqYNFd+BmOXaeZWV0Q +oHtDzXmcwtP8aUQpxN0e1xkWb1E80qoy+0uuRqb/50b/R4Q5qqSfJhkn6z8nwB10 +7RjLtJPrK8igxdpr3tGUzfAOyiPrIDncY7UJaL84GFp7WWAkH0WG3H8Y8DRcRXOU +mqDxDLUP3rNuow3jnGxiUY+gGX5OqaZg4f4P6QzOSmeQYs6nLpH0PiN00+oS1BbD +bpWdZEttILPI+vAYkU4QuBKKDjJL6HbSd+cn +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECDCCAvCgAwIBAgIDAIVCMA0GCSqGSIb3DQEBCwUAMIGPMQswCQYDVQQGEwJV +UzEQMA4GA1UEBwwHU2VhdHRsZTETMBEGA1UECAwKV2FzaGluZ3RvbjEiMCAGA1UE +CgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJE +UzEgMB4GA1UEAwwXQW1hem9uIFJEUyBSb290IDIwMTkgQ0EwHhcNMTkwOTEzMTcw +NjQxWhcNMjQwODIyMTcwODUwWjCBlDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldh +c2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUxIjAgBgNVBAoMGUFtYXpvbiBXZWIg +U2VydmljZXMsIEluYy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxJTAjBgNVBAMMHEFt +YXpvbiBSRFMgdXMtZWFzdC0yIDIwMTkgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDE+T2xYjUbxOp+pv+gRA3FO24+1zCWgXTDF1DHrh1lsPg5k7ht +2KPYzNc+Vg4E+jgPiW0BQnA6jStX5EqVh8BU60zELlxMNvpg4KumniMCZ3krtMUC +au1NF9rM7HBh+O+DYMBLK5eSIVt6lZosOb7bCi3V6wMLA8YqWSWqabkxwN4w0vXI +8lu5uXXFRemHnlNf+yA/4YtN4uaAyd0ami9+klwdkZfkrDOaiy59haOeBGL8EB/c +dbJJlguHH5CpCscs3RKtOOjEonXnKXldxarFdkMzi+aIIjQ8GyUOSAXHtQHb3gZ4 +nS6Ey0CMlwkB8vUObZU9fnjKJcL5QCQqOfwvAgMBAAGjZjBkMA4GA1UdDwEB/wQE +AwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBQUPuRHohPxx4VjykmH +6usGrLL1ETAfBgNVHSMEGDAWgBRzX2DYvMsDmPQrFzQuNlqmYP+8HzANBgkqhkiG +9w0BAQsFAAOCAQEAUdR9Vb3y33Yj6X6KGtuthZ08SwjImVQPtknzpajNE5jOJAh8 +quvQnU9nlnMO85fVDU1Dz3lLHGJ/YG1pt1Cqq2QQ200JcWCvBRgdvH6MjHoDQpqZ +HvQ3vLgOGqCLNQKFuet9BdpsHzsctKvCVaeBqbGpeCtt3Hh/26tgx0rorPLw90A2 +V8QSkZJjlcKkLa58N5CMM8Xz8KLWg3MZeT4DmlUXVCukqK2RGuP2L+aME8dOxqNv +OnOz1zrL5mR2iJoDpk8+VE/eBDmJX40IJk6jBjWoxAO/RXq+vBozuF5YHN1ujE92 +tO8HItgTp37XT8bJBAiAnt5mxw+NLSqtxk2QdQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEDDCCAvSgAwIBAgICY4kwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTMyMDEx +NDJaFw0yNDA4MjIxNzA4NTBaMIGZMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEqMCgGA1UEAwwhQW1h +em9uIFJEUyBhcC1zb3V0aGVhc3QtMSAyMDE5IENBMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAr5u9OuLL/OF/fBNUX2kINJLzFl4DnmrhnLuSeSnBPgbb +qddjf5EFFJBfv7IYiIWEFPDbDG5hoBwgMup5bZDbas+ZTJTotnnxVJTQ6wlhTmns +eHECcg2pqGIKGrxZfbQhlj08/4nNAPvyYCTS0bEcmQ1emuDPyvJBYDDLDU6AbCB5 +6Z7YKFQPTiCBblvvNzchjLWF9IpkqiTsPHiEt21sAdABxj9ityStV3ja/W9BfgxH +wzABSTAQT6FbDwmQMo7dcFOPRX+hewQSic2Rn1XYjmNYzgEHisdUsH7eeXREAcTw +61TRvaLH8AiOWBnTEJXPAe6wYfrcSd1pD0MXpoB62wIDAQABo2YwZDAOBgNVHQ8B +Af8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUytwMiomQOgX5 +Ichd+2lDWRUhkikwHwYDVR0jBBgwFoAUc19g2LzLA5j0Kxc0LjZapmD/vB8wDQYJ +KoZIhvcNAQELBQADggEBACf6lRDpfCD7BFRqiWM45hqIzffIaysmVfr+Jr+fBTjP +uYe/ba1omSrNGG23bOcT9LJ8hkQJ9d+FxUwYyICQNWOy6ejicm4z0C3VhphbTPqj +yjpt9nG56IAcV8BcRJh4o/2IfLNzC/dVuYJV8wj7XzwlvjysenwdrJCoLadkTr1h +eIdG6Le07sB9IxrGJL9e04afk37h7c8ESGSE4E+oS4JQEi3ATq8ne1B9DQ9SasXi +IRmhNAaISDzOPdyLXi9N9V9Lwe/DHcja7hgLGYx3UqfjhLhOKwp8HtoZORixAmOI +HfILgNmwyugAbuZoCazSKKBhQ0wgO0WZ66ZKTMG8Oho= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEBzCCAu+gAwIBAgICUYkwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTYxODIx +MTVaFw0yNDA4MjIxNzA4NTBaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzElMCMGA1UEAwwcQW1h +em9uIFJEUyB1cy13ZXN0LTIgMjAxOSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBANCEZBZyu6yJQFZBJmSUZfSZd3Ui2gitczMKC4FLr0QzkbxY+cLa +uVONIOrPt4Rwi+3h/UdnUg917xao3S53XDf1TDMFEYp4U8EFPXqCn/GXBIWlU86P +PvBN+gzw3nS+aco7WXb+woTouvFVkk8FGU7J532llW8o/9ydQyDIMtdIkKTuMfho +OiNHSaNc+QXQ32TgvM9A/6q7ksUoNXGCP8hDOkSZ/YOLiI5TcdLh/aWj00ziL5bj +pvytiMZkilnc9dLY9QhRNr0vGqL0xjmWdoEXz9/OwjmCihHqJq+20MJPsvFm7D6a +2NKybR9U+ddrjb8/iyLOjURUZnj5O+2+OPcCAwEAAaNmMGQwDgYDVR0PAQH/BAQD +AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFEBxMBdv81xuzqcK5TVu +pHj+Aor8MB8GA1UdIwQYMBaAFHNfYNi8ywOY9CsXNC42WqZg/7wfMA0GCSqGSIb3 +DQEBCwUAA4IBAQBZkfiVqGoJjBI37aTlLOSjLcjI75L5wBrwO39q+B4cwcmpj58P +3sivv+jhYfAGEbQnGRzjuFoyPzWnZ1DesRExX+wrmHsLLQbF2kVjLZhEJMHF9eB7 +GZlTPdTzHErcnuXkwA/OqyXMpj9aghcQFuhCNguEfnROY9sAoK2PTfnTz9NJHL+Q +UpDLEJEUfc0GZMVWYhahc0x38ZnSY2SKacIPECQrTI0KpqZv/P+ijCEcMD9xmYEb +jL4en+XKS1uJpw5fIU5Sj0MxhdGstH6S84iAE5J3GM3XHklGSFwwqPYvuTXvANH6 +uboynxRgSae59jIlAK6Jrr6GWMwQRbgcaAlW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEDDCCAvSgAwIBAgICEkYwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTYxOTUz +NDdaFw0yNDA4MjIxNzA4NTBaMIGZMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEqMCgGA1UEAwwhQW1h +em9uIFJEUyBhcC1zb3V0aGVhc3QtMiAyMDE5IENBMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAufodI2Flker8q7PXZG0P0vmFSlhQDw907A6eJuF/WeMo +GHnll3b4S6nC3oRS3nGeRMHbyU2KKXDwXNb3Mheu+ox+n5eb/BJ17eoj9HbQR1cd +gEkIciiAltf8gpMMQH4anP7TD+HNFlZnP7ii3geEJB2GGXSxgSWvUzH4etL67Zmn +TpGDWQMB0T8lK2ziLCMF4XAC/8xDELN/buHCNuhDpxpPebhct0T+f6Arzsiswt2j +7OeNeLLZwIZvVwAKF7zUFjC6m7/VmTQC8nidVY559D6l0UhhU0Co/txgq3HVsMOH +PbxmQUwJEKAzQXoIi+4uZzHFZrvov/nDTNJUhC6DqwIDAQABo2YwZDAOBgNVHQ8B +Af8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUwaZpaCme+EiV +M5gcjeHZSTgOn4owHwYDVR0jBBgwFoAUc19g2LzLA5j0Kxc0LjZapmD/vB8wDQYJ +KoZIhvcNAQELBQADggEBAAR6a2meCZuXO2TF9bGqKGtZmaah4pH2ETcEVUjkvXVz +sl+ZKbYjrun+VkcMGGKLUjS812e7eDF726ptoku9/PZZIxlJB0isC/0OyixI8N4M +NsEyvp52XN9QundTjkl362bomPnHAApeU0mRbMDRR2JdT70u6yAzGLGsUwMkoNnw +1VR4XKhXHYGWo7KMvFrZ1KcjWhubxLHxZWXRulPVtGmyWg/MvE6KF+2XMLhojhUL ++9jB3Fpn53s6KMx5tVq1x8PukHmowcZuAF8k+W4gk8Y68wIwynrdZrKRyRv6CVtR +FZ8DeJgoNZT3y/GT254VqMxxfuy2Ccb/RInd16tEvVk= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEDDCCAvSgAwIBAgICOYIwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTcyMDA1 +MjlaFw0yNDA4MjIxNzA4NTBaMIGZMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEqMCgGA1UEAwwhQW1h +em9uIFJEUyBhcC1ub3J0aGVhc3QtMyAyMDE5IENBMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA4dMak8W+XW8y/2F6nRiytFiA4XLwePadqWebGtlIgyCS +kbug8Jv5w7nlMkuxOxoUeD4WhI6A9EkAn3r0REM/2f0aYnd2KPxeqS2MrtdxxHw1 +xoOxk2x0piNSlOz6yog1idsKR5Wurf94fvM9FdTrMYPPrDabbGqiBMsZZmoHLvA3 +Z+57HEV2tU0Ei3vWeGIqnNjIekS+E06KhASxrkNU5vi611UsnYZlSi0VtJsH4UGV +LhnHl53aZL0YFO5mn/fzuNG/51qgk/6EFMMhaWInXX49Dia9FnnuWXwVwi6uX1Wn +7kjoHi5VtmC8ZlGEHroxX2DxEr6bhJTEpcLMnoQMqwIDAQABo2YwZDAOBgNVHQ8B +Af8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUsUI5Cb3SWB8+ +gv1YLN/ABPMdxSAwHwYDVR0jBBgwFoAUc19g2LzLA5j0Kxc0LjZapmD/vB8wDQYJ +KoZIhvcNAQELBQADggEBAJAF3E9PM1uzVL8YNdzb6fwJrxxqI2shvaMVmC1mXS+w +G0zh4v2hBZOf91l1EO0rwFD7+fxoI6hzQfMxIczh875T6vUXePKVOCOKI5wCrDad +zQbVqbFbdhsBjF4aUilOdtw2qjjs9JwPuB0VXN4/jY7m21oKEOcnpe36+7OiSPjN +xngYewCXKrSRqoj3mw+0w/+exYj3Wsush7uFssX18av78G+ehKPIVDXptOCP/N7W +8iKVNeQ2QGTnu2fzWsGUSvMGyM7yqT+h1ILaT//yQS8er511aHMLc142bD4D9VSy +DgactwPDTShK/PXqhvNey9v/sKXm4XatZvwcc8KYlW4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEDDCCAvSgAwIBAgICcEUwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTgxNjU2 +MjBaFw0yNDA4MjIxNzA4NTBaMIGZMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEqMCgGA1UEAwwhQW1h +em9uIFJEUyBhcC1ub3J0aGVhc3QtMSAyMDE5IENBMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAndtkldmHtk4TVQAyqhAvtEHSMb6pLhyKrIFved1WO3S7 ++I+bWwv9b2W/ljJxLq9kdT43bhvzonNtI4a1LAohS6bqyirmk8sFfsWT3akb+4Sx +1sjc8Ovc9eqIWJCrUiSvv7+cS7ZTA9AgM1PxvHcsqrcUXiK3Jd/Dax9jdZE1e15s +BEhb2OEPE+tClFZ+soj8h8Pl2Clo5OAppEzYI4LmFKtp1X/BOf62k4jviXuCSst3 +UnRJzE/CXtjmN6oZySVWSe0rQYuyqRl6//9nK40cfGKyxVnimB8XrrcxUN743Vud +QQVU0Esm8OVTX013mXWQXJHP2c0aKkog8LOga0vobQIDAQABo2YwZDAOBgNVHQ8B +Af8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQULmoOS1mFSjj+ +snUPx4DgS3SkLFYwHwYDVR0jBBgwFoAUc19g2LzLA5j0Kxc0LjZapmD/vB8wDQYJ +KoZIhvcNAQELBQADggEBAAkVL2P1M2/G9GM3DANVAqYOwmX0Xk58YBHQu6iiQg4j +b4Ky/qsZIsgT7YBsZA4AOcPKQFgGTWhe9pvhmXqoN3RYltN8Vn7TbUm/ZVDoMsrM +gwv0+TKxW1/u7s8cXYfHPiTzVSJuOogHx99kBW6b2f99GbP7O1Sv3sLq4j6lVvBX +Fiacf5LAWC925nvlTzLlBgIc3O9xDtFeAGtZcEtxZJ4fnGXiqEnN4539+nqzIyYq +nvlgCzyvcfRAxwltrJHuuRu6Maw5AGcd2Y0saMhqOVq9KYKFKuD/927BTrbd2JVf +2sGWyuPZPCk3gq+5pCjbD0c6DkhcMGI6WwxvM5V/zSM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEBzCCAu+gAwIBAgICJDQwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTgxNzAz +MTVaFw0yNDA4MjIxNzA4NTBaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzElMCMGA1UEAwwcQW1h +em9uIFJEUyBldS13ZXN0LTMgMjAxOSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL9bL7KE0n02DLVtlZ2PL+g/BuHpMYFq2JnE2RgompGurDIZdjmh +1pxfL3nT+QIVMubuAOy8InRfkRxfpxyjKYdfLJTPJG+jDVL+wDcPpACFVqoV7Prg +pVYEV0lc5aoYw4bSeYFhdzgim6F8iyjoPnObjll9mo4XsHzSoqJLCd0QC+VG9Fw2 +q+GDRZrLRmVM2oNGDRbGpGIFg77aRxRapFZa8SnUgs2AqzuzKiprVH5i0S0M6dWr +i+kk5epmTtkiDHceX+dP/0R1NcnkCPoQ9TglyXyPdUdTPPRfKCq12dftqll+u4mV +ARdN6WFjovxax8EAP2OAUTi1afY+1JFMj+sCAwEAAaNmMGQwDgYDVR0PAQH/BAQD +AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFLfhrbrO5exkCVgxW0x3 +Y2mAi8lNMB8GA1UdIwQYMBaAFHNfYNi8ywOY9CsXNC42WqZg/7wfMA0GCSqGSIb3 +DQEBCwUAA4IBAQAigQ5VBNGyw+OZFXwxeJEAUYaXVoP/qrhTOJ6mCE2DXUVEoJeV +SxScy/TlFA9tJXqmit8JH8VQ/xDL4ubBfeMFAIAo4WzNWDVoeVMqphVEcDWBHsI1 +AETWzfsapRS9yQekOMmxg63d/nV8xewIl8aNVTHdHYXMqhhik47VrmaVEok1UQb3 +O971RadLXIEbVd9tjY5bMEHm89JsZDnDEw1hQXBb67Elu64OOxoKaHBgUH8AZn/2 +zFsL1ynNUjOhCSAA15pgd1vjwc0YsBbAEBPcHBWYBEyME6NLNarjOzBl4FMtATSF +wWCKRGkvqN8oxYhwR2jf2rR5Mu4DWkK5Q8Ep +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEBzCCAu+gAwIBAgICJVUwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTkxODE2 +NTNaFw0yNDA4MjIxNzA4NTBaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzElMCMGA1UEAwwcQW1h +em9uIFJEUyB1cy1lYXN0LTEgMjAxOSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAM3i/k2u6cqbMdcISGRvh+m+L0yaSIoOXjtpNEoIftAipTUYoMhL +InXGlQBVA4shkekxp1N7HXe1Y/iMaPEyb3n+16pf3vdjKl7kaSkIhjdUz3oVUEYt +i8Z/XeJJ9H2aEGuiZh3kHixQcZczn8cg3dA9aeeyLSEnTkl/npzLf//669Ammyhs +XcAo58yvT0D4E0D/EEHf2N7HRX7j/TlyWvw/39SW0usiCrHPKDLxByLojxLdHzso +QIp/S04m+eWn6rmD+uUiRteN1hI5ncQiA3wo4G37mHnUEKo6TtTUh+sd/ku6a8HK +glMBcgqudDI90s1OpuIAWmuWpY//8xEG2YECAwEAAaNmMGQwDgYDVR0PAQH/BAQD +AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFPqhoWZcrVY9mU7tuemR +RBnQIj1jMB8GA1UdIwQYMBaAFHNfYNi8ywOY9CsXNC42WqZg/7wfMA0GCSqGSIb3 +DQEBCwUAA4IBAQB6zOLZ+YINEs72heHIWlPZ8c6WY8MDU+Be5w1M+BK2kpcVhCUK +PJO4nMXpgamEX8DIiaO7emsunwJzMSvavSPRnxXXTKIc0i/g1EbiDjnYX9d85DkC +E1LaAUCmCZBVi9fIe0H2r9whIh4uLWZA41oMnJx/MOmo3XyMfQoWcqaSFlMqfZM4 +0rNoB/tdHLNuV4eIdaw2mlHxdWDtF4oH+HFm+2cVBUVC1jXKrFv/euRVtsTT+A6i +h2XBHKxQ1Y4HgAn0jACP2QSPEmuoQEIa57bEKEcZsBR8SDY6ZdTd2HLRIApcCOSF +MRM8CKLeF658I0XgF8D5EsYoKPsA+74Z+jDH +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEETCCAvmgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSUwIwYDVQQDDBxBbWF6b24gUkRTIEJldGEgUm9vdCAyMDE5IENBMB4XDTE5MDgy +MDE3MTAwN1oXDTI0MDgxOTE3MzgyNlowgZkxCzAJBgNVBAYTAlVTMRMwEQYDVQQI +DApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0dGxlMSIwIAYDVQQKDBlBbWF6b24g +V2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMSowKAYDVQQD +DCFBbWF6b24gUkRTIEJldGEgdXMtZWFzdC0xIDIwMTkgQ0EwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDTNCOlotQcLP8TP82U2+nk0bExVuuMVOgFeVMx +vbUHZQeIj9ikjk+jm6eTDnnkhoZcmJiJgRy+5Jt69QcRbb3y3SAU7VoHgtraVbxF +QDh7JEHI9tqEEVOA5OvRrDRcyeEYBoTDgh76ROco2lR+/9uCvGtHVrMCtG7BP7ZB +sSVNAr1IIRZZqKLv2skKT/7mzZR2ivcw9UeBBTUf8xsfiYVBvMGoEsXEycjYdf6w +WV+7XS7teNOc9UgsFNN+9AhIBc1jvee5E//72/4F8pAttAg/+mmPUyIKtekNJ4gj +OAR2VAzGx1ybzWPwIgOudZFHXFduxvq4f1hIRPH0KbQ/gkRrAgMBAAGjZjBkMA4G +A1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBTkvpCD +6C43rar9TtJoXr7q8dkrrjAfBgNVHSMEGDAWgBStoQwVpbGx87fxB3dEGDqKKnBT +4TANBgkqhkiG9w0BAQsFAAOCAQEAJd9fOSkwB3uVdsS+puj6gCER8jqmhd3g/J5V +Zjk9cKS8H0e8pq/tMxeJ8kpurPAzUk5RkCspGt2l0BSwmf3ahr8aJRviMX6AuW3/ +g8aKplTvq/WMNGKLXONa3Sq8591J+ce8gtOX/1rDKmFI4wQ/gUzOSYiT991m7QKS +Fr6HMgFuz7RNJbb3Fy5cnurh8eYWA7mMv7laiLwTNsaro5qsqErD5uXuot6o9beT +a+GiKinEur35tNxAr47ax4IRubuIzyfCrezjfKc5raVV2NURJDyKP0m0CCaffAxE +qn2dNfYc3v1D8ypg3XjHlOzRo32RB04o8ALHMD9LSwsYDLpMag== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEFzCCAv+gAwIBAgICFSUwDQYJKoZIhvcNAQELBQAwgZcxCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSgwJgYDVQQDDB9BbWF6b24gUkRTIFByZXZpZXcgUm9vdCAyMDE5IENBMB4XDTE5 +MDgyMTIyMzk0N1oXDTI0MDgyMTIyMjk0OVowgZwxCzAJBgNVBAYTAlVTMRMwEQYD +VQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0dGxlMSIwIAYDVQQKDBlBbWF6 +b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMS0wKwYD +VQQDDCRBbWF6b24gUkRTIFByZXZpZXcgdXMtZWFzdC0yIDIwMTkgQ0EwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD0dB/U7qRnSf05wOi7m10Pa2uPMTJv +r6U/3Y17a5prq5Zr4++CnSUYarG51YuIf355dKs+7Lpzs782PIwCmLpzAHKWzix6 +pOaTQ+WZ0+vUMTxyqgqWbsBgSCyP7pVBiyqnmLC/L4az9XnscrbAX4pNaoJxsuQe +mzBo6yofjQaAzCX69DuqxFkVTRQnVy7LCFkVaZtjNAftnAHJjVgQw7lIhdGZp9q9 +IafRt2gteihYfpn+EAQ/t/E4MnhrYs4CPLfS7BaYXBycEKC5Muj1l4GijNNQ0Efo +xG8LSZz7SNgUvfVwiNTaqfLP3AtEAWiqxyMyh3VO+1HpCjT7uNBFtmF3AgMBAAGj +ZjBkMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQW +BBQtinkdrj+0B2+qdXngV2tgHnPIujAfBgNVHSMEGDAWgBRp0xqULkNh/w2ZVzEI +o2RIY7O03TANBgkqhkiG9w0BAQsFAAOCAQEAtJdqbCxDeMc8VN1/RzCabw9BIL/z +73Auh8eFTww/sup26yn8NWUkfbckeDYr1BrXa+rPyLfHpg06kwR8rBKyrs5mHwJx +bvOzXD/5WTdgreB+2Fb7mXNvWhenYuji1MF+q1R2DXV3I05zWHteKX6Dajmx+Uuq +Yq78oaCBSV48hMxWlp8fm40ANCL1+gzQ122xweMFN09FmNYFhwuW+Ao+Vv90ZfQG +PYwTvN4n/gegw2TYcifGZC2PNX74q3DH03DXe5fvNgRW5plgz/7f+9mS+YHd5qa9 +tYTPUvoRbi169ou6jicsMKUKPORHWhiTpSCWR1FMMIbsAcsyrvtIsuaGCQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIQdOCSuA9psBpQd8EI368/0DANBgkqhkiG9w0BAQsFADCB +lzELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdB +bWF6b24gUkRTIHNhLWVhc3QtMSBSb290IENBIFJTQTIwNDggRzExEDAOBgNVBAcM +B1NlYXR0bGUwIBcNMjEwNTE5MTgwNjI2WhgPMjA2MTA1MTkxOTA2MjZaMIGXMQsw +CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET +MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpv +biBSRFMgc2EtZWFzdC0xIFJvb3QgQ0EgUlNBMjA0OCBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN6ftL6w8v3dB2yW +LjCxSP1D7ZsOTeLZOSCz1Zv0Gkd0XLhil5MdHOHBvwH/DrXqFU2oGzCRuAy+aZis +DardJU6ChyIQIciXCO37f0K23edhtpXuruTLLwUwzeEPdcnLPCX+sWEn9Y5FPnVm +pCd6J8edH2IfSGoa9LdErkpuESXdidLym/w0tWG/O2By4TabkNSmpdrCL00cqI+c +prA8Bx1jX8/9sY0gpAovtuFaRN+Ivg3PAnWuhqiSYyQ5nC2qDparOWuDiOhpY56E +EgmTvjwqMMjNtExfYx6Rv2Ndu50TriiNKEZBzEtkekwXInTupmYTvc7U83P/959V +UiQ+WSMCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU4uYHdH0+ +bUeh81Eq2l5/RJbW+vswDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4IB +AQBhxcExJ+w74bvDknrPZDRgTeMLYgbVJjx2ExH7/Ac5FZZWcpUpFwWMIJJxtewI +AnhryzM3tQYYd4CG9O+Iu0+h/VVfW7e4O3joWVkxNMb820kQSEwvZfA78aItGwOY +WSaFNVRyloVicZRNJSyb1UL9EiJ9ldhxm4LTT0ax+4ontI7zTx6n6h8Sr6r/UOvX +d9T5aUUENWeo6M9jGupHNn3BobtL7BZm2oS8wX8IVYj4tl0q5T89zDi2x0MxbsIV +5ZjwqBQ5JWKv7ASGPb+z286RjPA9R2knF4lJVZrYuNV90rHvI/ECyt/JrDqeljGL +BLl1W/UsvZo6ldLIpoMbbrb5 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEBDCCAuygAwIBAgIQUfVbqapkLYpUqcLajpTJWzANBgkqhkiG9w0BAQsFADCB +mjELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTMwMQYDVQQDDCpB +bWF6b24gUkRTIG1lLWNlbnRyYWwtMSBSb290IENBIFJTQTIwNDggRzExEDAOBgNV +BAcMB1NlYXR0bGUwIBcNMjIwNTA2MjMyMDA5WhgPMjA2MjA1MDcwMDIwMDlaMIGa +MQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5j +LjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMzAxBgNVBAMMKkFt +YXpvbiBSRFMgbWUtY2VudHJhbC0xIFJvb3QgQ0EgUlNBMjA0OCBHMTEQMA4GA1UE +BwwHU2VhdHRsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJIeovu3 +ewI9FVitXMQzvkh34aQ6WyI4NO3YepfJaePiv3cnyFGYHN2S1cR3UQcLWgypP5va +j6bfroqwGbCbZZcb+6cyOB4ceKO9Ws1UkcaGHnNDcy5gXR7aCW2OGTUfinUuhd2d +5bOGgV7JsPbpw0bwJ156+MwfOK40OLCWVbzy8B1kITs4RUPNa/ZJnvIbiMu9rdj4 +8y7GSFJLnKCjlOFUkNI5LcaYvI1+ybuNgphT3nuu5ZirvTswGakGUT/Q0J3dxP0J +pDfg5Sj/2G4gXiaM0LppVOoU5yEwVewhQ250l0eQAqSrwPqAkdTg9ng360zqCFPE +JPPcgI1tdGUgneECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +/2AJVxWdZxc8eJgdpbwpW7b0f7IwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +CwUAA4IBAQBYm63jTu2qYKJ94gKnqc+oUgqmb1mTXmgmp/lXDbxonjszJDOXFbri +3CCO7xB2sg9bd5YWY8sGKHaWmENj3FZpCmoefbUx++8D7Mny95Cz8R32rNcwsPTl +ebpd9A/Oaw5ug6M0x/cNr0qzF8Wk9Dx+nFEimp8RYQdKvLDfNFZHjPa1itnTiD8M +TorAqj+VwnUGHOYBsT/0NY12tnwXdD+ATWfpEHdOXV+kTMqFFwDyhfgRVNpTc+os +ygr8SwhnSCpJPB/EYl2S7r+tgAbJOkuwUvGT4pTqrzDQEhwE7swgepnHC87zhf6l +qN6mVpSnQKQLm6Ob5TeCEFgcyElsF5bH +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrjCCAjSgAwIBAgIRAOxu0I1QuMAhIeszB3fJIlkwCgYIKoZIzj0EAwMwgZYx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEvMC0GA1UEAwwmQW1h +em9uIFJEUyB1cy13ZXN0LTIgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjEwNTI0MjIwNjU5WhgPMjEyMTA1MjQyMzA2NTlaMIGWMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExLzAtBgNVBAMMJkFtYXpvbiBS +RFMgdXMtd2VzdC0yIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQHDAdTZWF0dGxl +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEz4bylRcGqqDWdP7gQIIoTHdBK6FNtKH1 +4SkEIXRXkYDmRvL9Bci1MuGrwuvrka5TDj4b7e+csY0llEzHpKfq6nJPFljoYYP9 +uqHFkv77nOpJJ633KOr8IxmeHW5RXgrZo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0G +A1UdDgQWBBQQikVz8wmjd9eDFRXzBIU8OseiGzAOBgNVHQ8BAf8EBAMCAYYwCgYI +KoZIzj0EAwMDaAAwZQIwf06Mcrpw1O0EBLBBrp84m37NYtOkE/0Z0O+C7D41wnXi +EQdn6PXUVgdD23Gj82SrAjEAklhKs+liO1PtN15yeZR1Io98nFve+lLptaLakZcH ++hfFuUtCqMbaI8CdvJlKnPqT +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGCTCCA/GgAwIBAgIRALyWMTyCebLZOGcZZQmkmfcwDQYJKoZIhvcNAQEMBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1ub3J0aGVhc3QtMyBSb290IENBIFJTQTQwOTYgRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjEwNTI0MjAyODAzWhgPMjEyMTA1MjQyMTI4MDNa +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgYXAtbm9ydGhlYXN0LTMgUm9vdCBDQSBSU0E0MDk2IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA +wGFiyDyCrGqgdn4fXG12cxKAAfVvhMea1mw5h9CVRoavkPqhzQpAitSOuMB9DeiP +wQyqcsiGl/cTEau4L+AUBG8b9v26RlY48exUYBXj8CieYntOT9iNw5WtdYJa3kF/ +JxgI+HDMzE9cmHDs5DOO3S0uwZVyra/xE1ymfSlpOeUIOTpHRJv97CBUEpaZMUW5 +Sr6GruuOwFVpO5FX3A/jQlcS+UN4GjSRgDUJuqg6RRQldEZGCVCCmodbByvI2fGm +reGpsPJD54KkmAX08nOR8e5hkGoHxq0m2DLD4SrOFmt65vG47qnuwplWJjtk9B3Z +9wDoopwZLBOtlkPIkUllWm1P8EuHC1IKOA+wSP6XdT7cy8S77wgyHzR0ynxv7q/l +vlZtH30wnNqFI0y9FeogD0TGMCHcnGqfBSicJXPy9T4fU6f0r1HwqKwPp2GArwe7 +dnqLTj2D7M9MyVtFjEs6gfGWXmu1y5uDrf+CszurE8Cycoma+OfjjuVQgWOCy7Nd +jJswPxAroTzVfpgoxXza4ShUY10woZu0/J+HmNmqK7lh4NS75q1tz75in8uTZDkV +be7GK+SEusTrRgcf3tlgPjSTWG3veNzFDF2Vn1GLJXmuZfhdlVQDBNXW4MNREExS +dG57kJjICpT+r8X+si+5j51gRzkSnMYs7VHulpxfcwECAwEAAaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQU4JWOpDBmUBuWKvGPZelw87ezhL8wDgYDVR0P +AQH/BAQDAgGGMA0GCSqGSIb3DQEBDAUAA4ICAQBRNLMql7itvXSEFQRAnyOjivHz +l5IlWVQjAbOUr6ogZcwvK6YpxNAFW5zQr8F+fdkiypLz1kk5irx9TIpff0BWC9hQ +/odMPO8Gxn8+COlSvc+dLsF2Dax3Hvz0zLeKMo+cYisJOzpdR/eKd0/AmFdkvQoM +AOK9n0yYvVJU2IrSgeJBiiCarpKSeAktEVQ4rvyacQGr+QAPkkjRwm+5LHZKK43W +nNnggRli9N/27qYtc5bgr3AaQEhEXMI4RxPRXCLsod0ehMGWyRRK728a+6PMMJAJ +WHOU0x7LCEMPP/bvpLj3BdvSGqNor4ZtyXEbwREry1uzsgODeRRns5acPwTM6ff+ +CmxO2NZ0OktIUSYRmf6H/ZFlZrIhV8uWaIwEJDz71qvj7buhQ+RFDZ9CNL64C0X6 +mf0zJGEpddjANHaaVky+F4gYMtEy2K2Lcm4JGTdyIzUoIe+atzCnRp0QeIcuWtF+ +s8AjDYCVFNypcMmqbRmNpITSnOoCHSRuVkY3gutVoYyMLbp8Jm9SJnCIlEWTA6Rm +wADOMGZJVn5/XRTRuetVOB3KlQDjs9OO01XN5NzGSZO2KT9ngAUfh9Eqhf1iRWSP +nZlRbQ2NRCuY/oJ5N59mLGxnNJSE7giEKEBRhTQ/XEPIUYAUPD5fca0arKRJwbol +l9Se1Hsq0ZU5f+OZKQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGATCCA+mgAwIBAgIRAK7vlRrGVEePJpW1VHMXdlIwDQYJKoZIhvcNAQEMBQAw +gZgxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwo +QW1hem9uIFJEUyBhZi1zb3V0aC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UE +BwwHU2VhdHRsZTAgFw0yMTA1MTkxOTI4NDNaGA8yMTIxMDUxOTIwMjg0M1owgZgx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwoQW1h +em9uIFJEUyBhZi1zb3V0aC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UEBwwH +U2VhdHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMZiHOQC6x4o +eC7vVOMCGiN5EuLqPYHdceFPm4h5k/ZejXTf7kryk6aoKZKsDIYihkaZwXVS7Y/y +7Ig1F1ABi2jD+CYprj7WxXbhpysmN+CKG7YC3uE4jSvfvUnpzionkQbjJsRJcrPO +cZJM4FVaVp3mlHHtvnM+K3T+ni4a38nAd8xrv1na4+B8ZzZwWZXarfg8lJoGskSn +ou+3rbGQ0r+XlUP03zWujHoNlVK85qUIQvDfTB7n3O4s1XNGvkfv3GNBhYRWJYlB +4p8T+PFN8wG+UOByp1gV7BD64RnpuZ8V3dRAlO6YVAmINyG5UGrPzkIbLtErUNHO +4iSp4UqYvztDqJWWHR/rA84ef+I9RVwwZ8FQbjKq96OTnPrsr63A5mXTC9dXKtbw +XNJPQY//FEdyM3K8sqM0IdCzxCA1MXZ8+QapWVjwyTjUwFvL69HYky9H8eAER59K +5I7u/CWWeCy2R1SYUBINc3xxLr0CGGukcWPEZW2aPo5ibW5kepU1P/pzdMTaTfao +F42jSFXbc7gplLcSqUgWwzBnn35HLTbiZOFBPKf6vRRu8aRX9atgHw/EjCebi2xP +xIYr5Ub8u0QVHIqcnF1/hVzO/Xz0chj3E6VF/yTXnsakm+W1aM2QkZbFGpga+LMy +mFCtdPrELjea2CfxgibaJX1Q4rdEpc8DAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFDSaycEyuspo/NOuzlzblui8KotFMA4GA1UdDwEB/wQEAwIB +hjANBgkqhkiG9w0BAQwFAAOCAgEAbosemjeTRsL9o4v0KadBUNS3V7gdAH+X4vH2 +Ee1Jc91VOGLdd/s1L9UX6bhe37b9WjUD69ur657wDW0RzxMYgQdZ27SUl0tEgGGp +cCmVs1ky3zEN+Hwnhkz+OTmIg1ufq0W2hJgJiluAx2r1ib1GB+YI3Mo3rXSaBYUk +bgQuujYPctf0PA153RkeICE5GI3OaJ7u6j0caYEixBS3PDHt2MJWexITvXGwHWwc +CcrC05RIrTUNOJaetQw8smVKYOfRImEzLLPZ5kf/H3Cbj8BNAFNsa10wgvlPuGOW +XLXqzNXzrG4V3sjQU5YtisDMagwYaN3a6bBf1wFwFIHQoAPIgt8q5zaQ9WI+SBns +Il6rd4zfvjq/BPmt0uI7rVg/cgbaEg/JDL2neuM9CJAzmKxYxLQuHSX2i3Fy4Y1B +cnxnRQETCRZNPGd00ADyxPKVoYBC45/t+yVusArFt+2SVLEGiFBr23eG2CEZu+HS +nDEgIfQ4V3YOTUNa86wvbAss1gbbnT/v1XCnNGClEWCWNCSRjwV2ZmQ/IVTmNHPo +7axTTBBJbKJbKzFndCnuxnDXyytdYRgFU7Ly3sa27WS2KFyFEDebLFRHQEfoYqCu +IupSqBSbXsR3U10OTjc9z6EPo1nuV6bdz+gEDthmxKa1NI+Qb1kvyliXQHL2lfhr +5zT5+Bs= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF/zCCA+egAwIBAgIRAOLV6zZcL4IV2xmEneN1GwswDQYJKoZIhvcNAQEMBQAw +gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn +QW1hem9uIFJEUyB1cy13ZXN0LTEgUm9vdCBDQSBSU0E0MDk2IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUxOTE5MDg1OFoYDzIxMjEwNTE5MjAwODU4WjCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIHVzLXdlc3QtMSBSb290IENBIFJTQTQwOTYgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC7koAKGXXlLixN +fVjhuqvz0WxDeTQfhthPK60ekRpftkfE5QtnYGzeovaUAiS58MYVzqnnTACDwcJs +IGTFE6Wd7sB6r8eI/3CwI1pyJfxepubiQNVAQG0zJETOVkoYKe/5KnteKtnEER3X +tCBRdV/rfbxEDG9ZAsYfMl6zzhEWKF88G6xhs2+VZpDqwJNNALvQuzmTx8BNbl5W +RUWGq9CQ9GK9GPF570YPCuURW7kl35skofudE9bhURNz51pNoNtk2Z3aEeRx3ouT +ifFJlzh+xGJRHqBG7nt5NhX8xbg+vw4xHCeq1aAe6aVFJ3Uf9E2HzLB4SfIT9bRp +P7c9c0ySGt+3n+KLSHFf/iQ3E4nft75JdPjeSt0dnyChi1sEKDi0tnWGiXaIg+J+ +r1ZtcHiyYpCB7l29QYMAdD0TjfDwwPayLmq//c20cPmnSzw271VwqjUT0jYdrNAm +gV+JfW9t4ixtE3xF2jaUh/NzL3bAmN5v8+9k/aqPXlU1BgE3uPwMCjrfn7V0I7I1 +WLpHyd9jF3U/Ysci6H6i8YKgaPiOfySimQiDu1idmPld659qerutUSemQWmPD3bE +dcjZolmzS9U0Ujq/jDF1YayN3G3xvry1qWkTci0qMRMu2dZu30Herugh9vsdTYkf +00EqngPbqtIVLDrDjEQLqPcb8QvWFQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBQBqg8Za/L0YMHURGExHfvPyfLbOTAOBgNVHQ8BAf8EBAMCAYYw +DQYJKoZIhvcNAQEMBQADggIBACAGPMa1QL7P/FIO7jEtMelJ0hQlQepKnGtbKz4r +Xq1bUX1jnLvnAieR9KZmeQVuKi3g3CDU6b0mDgygS+FL1KDDcGRCSPh238Ou8KcG +HIxtt3CMwMHMa9gmdcMlR5fJF9vhR0C56KM2zvyelUY51B/HJqHwGvWuexryXUKa +wq1/iK2/d9mNeOcjDvEIj0RCMI8dFQCJv3PRCTC36XS36Tzr6F47TcTw1c3mgKcs +xpcwt7ezrXMUunzHS4qWAA5OGdzhYlcv+P5GW7iAA7TDNrBF+3W4a/6s9v2nQAnX +UvXd9ul0ob71377UhZbJ6SOMY56+I9cJOOfF5QvaL83Sz29Ij1EKYw/s8TYdVqAq ++dCyQZBkMSnDFLVe3J1KH2SUSfm3O98jdPORQrUlORQVYCHPls19l2F6lCmU7ICK +hRt8EVSpXm4sAIA7zcnR2nU00UH8YmMQLnx5ok9YGhuh3Ehk6QlTQLJux6LYLskd +9YHOLGW/t6knVtV78DgPqDeEx/Wu/5A8R0q7HunpWxr8LCPBK6hksZnOoUhhb8IP +vl46Ve5Tv/FlkyYr1RTVjETmg7lb16a8J0At14iLtpZWmwmuv4agss/1iBVMXfFk ++ZGtx5vytWU5XJmsfKA51KLsMQnhrLxb3X3zC+JRCyJoyc8++F3YEcRi2pkRYE3q +Hing +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID/zCCAuegAwIBAgIRAI+asxQA/MB1cGyyrC0MPpkwDQYJKoZIhvcNAQELBQAw +gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn +QW1hem9uIFJEUyBjYS13ZXN0LTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIzMDkxMzIwMjEzNFoYDzIwNjMwOTEzMjEyMTMzWjCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIGNhLXdlc3QtMSBSb290IENBIFJTQTIwNDggRzExEDAOBgNVBAcMB1Nl +YXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDMHvQITTZcfl2O +yfzRIAPKwzzlc8eXWdXef7VUsbezg3lm9RC+vArO4JuAzta/aLw1D94wPSRm9JXX +NkP3obO6Ql80/0doooU6BAPceD0xmEWC4aCFT/5KWsD6Sy2/Rjwq3NKBTwzxLwYK +GqVsBp8AdrzDTmdRETC+Dg2czEo32mTDAA1uMgqrz6xxeTYroj8NTSTp6jfE6C0n +YgzYmVQCEIjHqI49j7k3jfT3P2skCVKGJwQzoZnerFacKzXsDB18uIqU7NaMc2cX +kOd0gRqpyKOzAHU2m5/S4jw4UHdkoI3E7nkayuen8ZPKH2YqWtTXUrXGhSTT34nX +yiFgu+vTAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHzz1NTd +TOm9zAv4d8l6XCFKSdJfMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOC +AQEAodBvd0cvXQYhFBef2evnuI9XA+AC/Q9P1nYtbp5MPA4aFhy5v9rjW8wwJX14 +l+ltd2o3tz8PFDBZ1NX2ooiWVlZthQxKn1/xDVKsTXHbYUXItPQ3jI5IscB5IML8 +oCzAbkoLXsSPNOVFP5P4l4cZEMqHGRnBag7hLJZvmvzZSBnz+ioC2jpjVluF8kDX +fQGNjqPECik68CqbSV0SaQ0cgEoYTDjwON5ZLBeS8sxR2abE/gsj4VFYl5w/uEBd +w3Tt9uGfIy+wd2tNj6isGC6PcbPMjA31jd+ifs2yNzigqkcYTTWFtnvh4a8xiecm +GHu2EgH0Jqzz500N7L3uQdPkdg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgIRANxgyBbnxgTEOpDul2ZnC0UwDQYJKoZIhvcNAQELBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1zb3V0aGVhc3QtMyBSb290IENBIFJTQTIwNDggRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjEwNjEwMTgxOTA3WhgPMjA2MTA2MTAxOTE5MDda +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgYXAtc291dGhlYXN0LTMgUm9vdCBDQSBSU0EyMDQ4IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +xnwSDAChrMkfk5TA4Dk8hKzStDlSlONzmd3fTG0Wqr5+x3EmFT6Ksiu/WIwEl9J2 +K98UI7vYyuZfCxUKb1iMPeBdVGqk0zb92GpURd+Iz/+K1ps9ZLeGBkzR8mBmAi1S +OfpwKiTBzIv6E8twhEn4IUpHsdcuX/2Y78uESpJyM8O5CpkG0JaV9FNEbDkJeBUQ +Ao2qqNcH4R0Qcr5pyeqA9Zto1RswgL06BQMI9dTpfwSP5VvkvcNUaLl7Zv5WzLQE +JzORWePvdPzzvWEkY/3FPjxBypuYwssKaERW0fkPDmPtykktP9W/oJolKUFI6pXp +y+Y6p6/AVdnQD2zZjW5FhQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBT+jEKs96LC+/X4BZkUYUkzPfXdqTAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI +hvcNAQELBQADggEBAIGQqgqcQ6XSGkmNebzR6DhadTbfDmbYeN5N0Vuzv+Tdmufb +tMGjdjnYMg4B+IVnTKQb+Ox3pL9gbX6KglGK8HupobmIRtwKVth+gYYz3m0SL/Nk +haWPYzOm0x3tJm8jSdufJcEob4/ATce9JwseLl76pSWdl5A4lLjnhPPKudUDfH+1 +BLNUi3lxpp6GkC8aWUPtupnhZuXddolTLOuA3GwTZySI44NfaFRm+o83N1jp+EwD +6e94M4cTRzjUv6J3MZmSbdtQP/Tk1uz2K4bQZGP0PZC3bVpqiesdE/xr+wbu8uHr +cM1JXH0AmXf1yIkTgyWzmvt0k1/vgcw5ixAqvvE= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEATCCAumgAwIBAgIRAMhw98EQU18mIji+unM2YH8wDQYJKoZIhvcNAQELBQAw +gZgxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwo +QW1hem9uIFJEUyBhcC1zb3V0aC0yIFJvb3QgQ0EgUlNBMjA0OCBHMTEQMA4GA1UE +BwwHU2VhdHRsZTAgFw0yMjA2MDYyMTQyMjJaGA8yMDYyMDYwNjIyNDIyMlowgZgx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwoQW1h +em9uIFJEUyBhcC1zb3V0aC0yIFJvb3QgQ0EgUlNBMjA0OCBHMTEQMA4GA1UEBwwH +U2VhdHRsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIeeRoLfTm+7 +vqm7ZlFSx+1/CGYHyYrOOryM4/Z3dqYVHFMgWTR7V3ziO8RZ6yUanrRcWVX3PZbF +AfX0KFE8OgLsXEZIX8odSrq86+/Th5eZOchB2fDBsUB7GuN2rvFBbM8lTI9ivVOU +lbuTnYyb55nOXN7TpmH2bK+z5c1y9RVC5iQsNAl6IJNvSN8VCqXh31eK5MlKB4DT ++Y3OivCrSGsjM+UR59uZmwuFB1h+icE+U0p9Ct3Mjq3MzSX5tQb6ElTNGlfmyGpW +Kh7GQ5XU1KaKNZXoJ37H53woNSlq56bpVrKI4uv7ATpdpFubOnSLtpsKlpLdR3sy +Ws245200pC8CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUp0ki +6+eWvsnBjQhMxwMW5pwn7DgwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUA +A4IBAQB2V8lv0aqbYQpj/bmVv/83QfE4vOxKCJAHv7DQ35cJsTyBdF+8pBczzi3t +3VNL5IUgW6WkyuUOWnE0eqAFOUVj0yTS1jSAtfl3vOOzGJZmWBbqm9BKEdu1D8O6 +sB8bnomwiab2tNDHPmUslpdDqdabbkWwNWzLJ97oGFZ7KNODMEPXWKWNxg33iHfS +/nlmnrTVI3XgaNK9qLZiUrxu9Yz5gxi/1K+sG9/Dajd32ZxjRwDipOLiZbiXQrsd +qzIMY4GcWf3g1gHL5mCTfk7dG22h/rhPyGV0svaDnsb+hOt6sv1McMN6Y3Ou0mtM +/UaAXojREmJmTSCNvs2aBny3/2sy +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrjCCAjSgAwIBAgIRAMnRxsKLYscJV8Qv5pWbL7swCgYIKoZIzj0EAwMwgZYx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEvMC0GA1UEAwwmQW1h +em9uIFJEUyBzYS1lYXN0LTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjEwNTE5MTgxNjAxWhgPMjEyMTA1MTkxOTE2MDFaMIGWMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExLzAtBgNVBAMMJkFtYXpvbiBS +RFMgc2EtZWFzdC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQHDAdTZWF0dGxl +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEjFOCZgTNVKxLKhUxffiDEvTLFhrmIqdO +dKqVdgDoELEzIHWDdC+19aDPitbCYtBVHl65ITu/9pn6mMUl5hhUNtfZuc6A+Iw1 +sBe0v0qI3y9Q9HdQYrGgeHDh8M5P7E2ho0IwQDAPBgNVHRMBAf8EBTADAQH/MB0G +A1UdDgQWBBS5L7/8M0TzoBZk39Ps7BkfTB4yJTAOBgNVHQ8BAf8EBAMCAYYwCgYI +KoZIzj0EAwMDaAAwZQIwI43O0NtWKTgnVv9z0LO5UMZYgSve7GvGTwqktZYCMObE +rUI4QerXM9D6JwLy09mqAjEAypfkdLyVWtaElVDUyHFkihAS1I1oUxaaDrynLNQK +Ou/Ay+ns+J+GyvyDUjBpVVW1 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF/jCCA+agAwIBAgIQR71Z8lTO5Sj+as2jB7IWXzANBgkqhkiG9w0BAQwFADCB +lzELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdB +bWF6b24gUkRTIHVzLXdlc3QtMiBSb290IENBIFJTQTQwOTYgRzExEDAOBgNVBAcM +B1NlYXR0bGUwIBcNMjEwNTI0MjIwMzIwWhgPMjEyMTA1MjQyMzAzMjBaMIGXMQsw +CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET +MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpv +biBSRFMgdXMtd2VzdC0yIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAM977bHIs1WJijrS +XQMfUOhmlJjr2v0K0UjPl52sE1TJ76H8umo1yR4T7Whkd9IwBHNGKXCJtJmMr9zp +fB38eLTu+5ydUAXdFuZpRMKBWwPVe37AdJRKqn5beS8HQjd3JXAgGKUNNuE92iqF +qi2fIqFMpnJXWo0FIW6s2Dl2zkORd7tH0DygcRi7lgVxCsw1BJQhFJon3y+IV8/F +bnbUXSNSDUnDW2EhvWSD8L+t4eiXYsozhDAzhBvojpxhPH9OB7vqFYw5qxFx+G0t +lSLX5iWi1jzzc3XyGnB6WInZDVbvnvJ4BGZ+dTRpOCvsoMIn9bz4EQTvu243c7aU +HbS/kvnCASNt+zk7C6lbmaq0AGNztwNj85Opn2enFciWZVnnJ/4OeefUWQxD0EPp +SjEd9Cn2IHzkBZrHCg+lWZJQBKbUVS0lLIMSsLQQ6WvR38jY7D2nxM1A93xWxwpt +ZtQnYRCVXH6zt2OwDAFePInWwxUjR5t/wu3XxPgpSfrmTi3WYtr1wFypAJ811e/P +yBtswWUQ6BNJQvy+KnOEeGfOwmtdDFYR+GOCfvCihzrKJrxOtHIieehR5Iw3cbXG +sm4pDzfMUVvDDz6C2M6PRlJhhClbatHCjik9hxFYEsAlqtVVK9pxaz9i8hOqSFQq +kJSQsgWw+oM/B2CyjcSqkSQEu8RLAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8w +HQYDVR0OBBYEFPmrdxpRRgu3IcaB5BTqlprcKdTsMA4GA1UdDwEB/wQEAwIBhjAN +BgkqhkiG9w0BAQwFAAOCAgEAVdlxWjPvVKky3kn8ZizeM4D+EsLw9dWLau2UD/ls +zwDCFoT6euagVeCknrn+YEl7g20CRYT9iaonGoMUPuMR/cdtPL1W/Rf40PSrGf9q +QuxavWiHLEXOQTCtCaVZMokkvjuuLNDXyZnstgECuiZECTwhexUF4oiuhyGk9o01 +QMaiz4HX4lgk0ozALUvEzaNd9gWEwD2qe+rq9cQMTVq3IArUkvTIftZUaVUMzr0O +ed1+zAsNa9nJhURJ/6anJPJjbQgb5qA1asFcp9UaMT1ku36U3gnR1T/BdgG2jX3X +Um0UcaGNVPrH1ukInWW743pxWQb7/2sumEEMVh+jWbB18SAyLI4WIh4lkurdifzS +IuTFp8TEx+MouISFhz/vJDWZ84tqoLVjkEcP6oDypq9lFoEzHDJv3V1CYcIgOusT +k1jm9P7BXdTG7TYzUaTb9USb6bkqkD9EwJAOSs7DI94aE6rsSws2yAHavjAMfuMZ +sDAZvkqS2Qg2Z2+CI6wUZn7mzkJXbZoqRjDvChDXEB1mIhzVXhiNW/CR5WKVDvlj +9v1sdGByh2pbxcLQtVaq/5coM4ANgphoNz3pOYUPWHS+JUrIivBZ+JobjXcxr3SN +9iDzcu5/FVVNbq7+KN/nvPMngT+gduEN5m+EBjm8GukJymFG0m6BENRA0QSDqZ7k +zDY= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgIRAK5EYG3iHserxMqgg+0EFjgwDQYJKoZIhvcNAQELBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1ub3J0aGVhc3QtMyBSb290IENBIFJTQTIwNDggRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjEwNTI0MjAyMzE2WhgPMjA2MTA1MjQyMTIzMTZa +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgYXAtbm9ydGhlYXN0LTMgUm9vdCBDQSBSU0EyMDQ4IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +s1L6TtB84LGraLHVC+rGPhLBW2P0oN/91Rq3AnYwqDOuTom7agANwEjvLq7dSRG/ +sIfZsSV/ABTgArZ5sCmLjHFZAo8Kd45yA9byx20RcYtAG8IZl+q1Cri+s0XefzyO +U6mlfXZkVe6lzjlfXBkrlE/+5ifVbJK4dqOS1t9cWIpgKqv5fbE6Qbq4LVT+5/WM +Vd2BOljuBMGMzdZubqFKFq4mzTuIYfnBm7SmHlZfTdfBYPP1ScNuhpjuzw4n3NCR +EdU6dQv04Q6th4r7eiOCwbWI9LkmVbvBe3ylhH63lApC7MiiPYLlB13xBubVHVhV +q1NHoNTi+zA3MN9HWicRxQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBSuxoqm0/wjNiZLvqv+JlQwsDvTPDAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI +hvcNAQELBQADggEBAFfTK/j5kv90uIbM8VaFdVbr/6weKTwehafT0pAk1bfLVX+7 +uf8oHgYiyKTTl0DFQicXejghXTeyzwoEkWSR8c6XkhD5vYG3oESqmt/RGvvoxz11 +rHHy7yHYu7RIUc3VQG60c4qxXv/1mWySGwVwJrnuyNT9KZXPevu3jVaWOVHEILaK +HvzQ2YEcWBPmde/zEseO2QeeGF8FL45Q1d66wqIP4nNUd2pCjeTS5SpB0MMx7yi9 +ki1OH1pv8tOuIdimtZ7wkdB8+JSZoaJ81b8sRrydRwJyvB88rftuI3YB4WwGuONT +ZezUPsmaoK69B0RChB0ofDpAaviF9V3xOWvVZfo= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGDzCCA/egAwIBAgIRAI0sMNG2XhaBMRN3zD7ZyoEwDQYJKoZIhvcNAQEMBQAw +gZ8xCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE4MDYGA1UEAwwv +QW1hem9uIFJEUyBQcmV2aWV3IHVzLWVhc3QtMiBSb290IENBIFJTQTQwOTYgRzEx +EDAOBgNVBAcMB1NlYXR0bGUwIBcNMjEwNTE4MjA1NzUwWhgPMjEyMTA1MTgyMTU3 +NTBaMIGfMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNl +cywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExODA2BgNV +BAMML0FtYXpvbiBSRFMgUHJldmlldyB1cy1lYXN0LTIgUm9vdCBDQSBSU0E0MDk2 +IEcxMRAwDgYDVQQHDAdTZWF0dGxlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAh/otSiCu4Uw3hu7OJm0PKgLsLRqBmUS6jihcrkxfN2SHmp2zuRflkweU +BhMkebzL+xnNvC8okzbgPWtUxSmDnIRhE8J7bvSKFlqs/tmEdiI/LMqe/YIKcdsI +20UYmvyLIjtDaJIh598SHHlF9P8DB5jD8snJfhxWY+9AZRN+YVTltgQAAgayxkWp +M1BbvxpOnz4CC00rE0eqkguXIUSuobb1vKqdKIenlYBNxm2AmtgvQfpsBIQ0SB+8 +8Zip8Ef5rtjSw5J3s2Rq0aYvZPfCVIsKYepIboVwXtD7E9J31UkB5onLBQlaHaA6 +XlH4srsMmrew5d2XejQGy/lGZ1nVWNsKO0x/Az2QzY5Kjd6AlXZ8kq6H68hscA5i +OMbNlXzeEQsZH0YkId3+UsEns35AAjZv4qfFoLOu8vDotWhgVNT5DfdbIWZW3ZL8 +qbmra3JnCHuaTwXMnc25QeKgVq7/rG00YB69tCIDwcf1P+tFJWxvaGtV0g2NthtB +a+Xo09eC0L53gfZZ3hZw1pa3SIF5dIZ6RFRUQ+lFOux3Q/I3u+rYstYw7Zxc4Zeo +Y8JiedpQXEAnbw2ECHix/L6mVWgiWCiDzBnNLLdbmXjJRnafNSndSfFtHCnY1SiP +aCrNpzwZIJejoV1zDlWAMO+gyS28EqzuIq3WJK/TFE7acHkdKIcCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUrmV1YASnuudfmqAZP4sKGTvScaEw +DgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBDAUAA4ICAQBGpEKeQoPvE85tN/25 +qHFkys9oHDl93DZ62EnOqAUKLd6v0JpCyEiop4nlrJe+4KrBYVBPyKOJDcIqE2Sp +3cvgJXLhY4i46VM3Qxe8yuYF1ElqBpg3jJVj/sCQnYz9dwoAMWIJFaDWOvmU2E7M +MRaKx+sPXFkIjiDA6Bv0m+VHef7aedSYIY7IDltEQHuXoqNacGrYo3I50R+fZs88 +/mB3e/V7967e99D6565yf9Lcjw4oQf2Hy7kl/6P9AuMz0LODnGITwh2TKk/Zo3RU +Vgq25RDrT4xJK6nFHyjUF6+4cOBxVpimmFw/VP1zaXT8DN5r4HyJ9p4YuSK8ha5N +2pJc/exvU8Nv2+vS/efcDZWyuEdZ7eh1IJWQZlOZKIAONfRDRTpeQHJ3zzv3QVYy +t78pYp/eWBHyVIfEE8p2lFKD4279WYe+Uvdb8c4Jm4TJwqkSJV8ifID7Ub80Lsir +lPAU3OCVTBeVRFPXT2zpC4PB4W6KBSuj6OOcEu2y/HgWcoi7Cnjvp0vFTUhDFdus +Wz3ucmJjfVsrkEO6avDKu4SwdbVHsk30TVAwPd6srIdi9U6MOeOQSOSE4EsrrS7l +SVmu2QIDUVFpm8QAHYplkyWIyGkupyl3ashH9mokQhixIU/Pzir0byePxHLHrwLu +1axqeKpI0F5SBUPsaVNYY2uNFg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECDCCAvCgAwIBAgIQCREfzzVyDTMcNME+gWnTCTANBgkqhkiG9w0BAQsFADCB +nDELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTUwMwYDVQQDDCxB +bWF6b24gUkRTIGFwLXNvdXRoZWFzdC0yIFJvb3QgQ0EgUlNBMjA0OCBHMTEQMA4G +A1UEBwwHU2VhdHRsZTAgFw0yMTA1MjQyMDQyMzNaGA8yMDYxMDUyNDIxNDIzM1ow +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1zb3V0aGVhc3QtMiBSb290IENBIFJTQTIwNDggRzExEDAO +BgNVBAcMB1NlYXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDL +1MT6br3L/4Pq87DPXtcjlXN3cnbNk2YqRAZHJayStTz8VtsFcGPJOpk14geRVeVk +e9uKFHRbcyr/RM4owrJTj5X4qcEuATYZbo6ou/rW2kYzuWFZpFp7lqm0vasV4Z9F +fChlhwkNks0UbM3G+psCSMNSoF19ERunj7w2c4E62LwujkeYLvKGNepjnaH10TJL +2krpERd+ZQ4jIpObtRcMH++bTrvklc+ei8W9lqrVOJL+89v2piN3Ecdd389uphst +qQdb1BBVXbhUrtuGHgVf7zKqN1SkCoktoWxVuOprVWhSvr7akaWeq0UmlvbEsujU +vADqxGMcJFyCzxx3CkJjAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFFk8UJmlhoxFT3PP12PvhvazHjT4MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG +9w0BAQsFAAOCAQEAfFtr2lGoWVXmWAsIo2NYre7kzL8Xb9Tx7desKxCCz5HOOvIr +8JMB1YK6A7IOvQsLJQ/f1UnKRh3X3mJZjKIywfrMSh0FiDf+rjcEzXxw2dGtUem4 +A+WMvIA3jwxnJ90OQj5rQ8bg3iPtE6eojzo9vWQGw/Vu48Dtw1DJo9210Lq/6hze +hPhNkFh8fMXNT7Q1Wz/TJqJElyAQGNOXhyGpHKeb0jHMMhsy5UNoW5hLeMS5ffao +TBFWEJ1gVfxIU9QRxSh+62m46JIg+dwDlWv8Aww14KgepspRbMqDuaM2cinoejv6 +t3dyOyHHrsOyv3ffZUKtQhQbQr+sUcL89lARsg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID/zCCAuegAwIBAgIRAIJLTMpzGNxqHZ4t+c1MlCIwDQYJKoZIhvcNAQELBQAw +gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn +QW1hem9uIFJEUyBhcC1lYXN0LTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyNTIxMzAzM1oYDzIwNjEwNTI1MjIzMDMzWjCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIGFwLWVhc3QtMSBSb290IENBIFJTQTIwNDggRzExEDAOBgNVBAcMB1Nl +YXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtdHut0ZhJ9Nn2 +MpVafFcwHdoEzx06okmmhjJsNy4l9QYVeh0UUoek0SufRNMRF4d5ibzpgZol0Y92 +/qKWNe0jNxhEj6sXyHsHPeYtNBPuDMzThfbvsLK8z7pBP7vVyGPGuppqW/6m4ZBB +lcc9fsf7xpZ689iSgoyjiT6J5wlVgmCx8hFYc/uvcRtfd8jAHvheug7QJ3zZmIye +V4htOW+fRVWnBjf40Q+7uTv790UAqs0Zboj4Yil+hER0ibG62y1g71XcCyvcVpto +2/XW7Y9NCgMNqQ7fGN3wR1gjtSYPd7DO32LTzYhutyvfbpAZjsAHnoObmoljcgXI +QjfBcCFpAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJI3aWLg +CS5xqU5WYVaeT5s8lpO0MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOC +AQEAUwATpJOcGVOs3hZAgJwznWOoTzOVJKfrqBum7lvkVH1vBwxBl9CahaKj3ZOt +YYp2qJzhDUWludL164DL4ZjS6eRedLRviyy5cRy0581l1MxPWTThs27z+lCC14RL +PJZNVYYdl7Jy9Q5NsQ0RBINUKYlRY6OqGDySWyuMPgno2GPbE8aynMdKP+f6G/uE +YHOf08gFDqTsbyfa70ztgVEJaRooVf5JJq4UQtpDvVswW2reT96qi6tXPKHN5qp3 +3wI0I1Mp4ePmiBKku2dwYzPfrJK/pQlvu0Gu5lKOQ65QdotwLAAoaFqrf9za1yYs +INUkHLWIxDds+4OHNYcerGp5Dw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGCTCCA/GgAwIBAgIRAIO6ldra1KZvNWJ0TA1ihXEwDQYJKoZIhvcNAQEMBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1zb3V0aGVhc3QtMSBSb290IENBIFJTQTQwOTYgRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjEwNTIxMjE0NTA1WhgPMjEyMTA1MjEyMjQ1MDVa +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgYXAtc291dGhlYXN0LTEgUm9vdCBDQSBSU0E0MDk2IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA +sDN52Si9pFSyZ1ruh3xAN0nVqEs960o2IK5CPu/ZfshFmzAwnx/MM8EHt/jMeZtj +SM58LADAsNDL01ELpFZATjgZQ6xNAyXRXE7RiTRUvNkK7O3o2qAGbLnJq/UqF7Sw +LRnB8V6hYOv+2EjVnohtGCn9SUFGZtYDjWXsLd4ML4Zpxv0a5LK7oEC7AHzbUR7R +jsjkrXqSv7GE7bvhSOhMkmgxgj1F3J0b0jdQdtyyj109aO0ATUmIvf+Bzadg5AI2 +A9UA+TUcGeebhpHu8AP1Hf56XIlzPpaQv3ZJ4vzoLaVNUC7XKzAl1dlvCl7Klg/C +84qmbD/tjZ6GHtzpLKgg7kQEV7mRoXq8X4wDX2AFPPQl2fv+Kbe+JODqm5ZjGegm +uskABBi8IFv1hYx9jEulZPxC6uD/09W2+niFm3pirnlWS83BwVDTUBzF+CooUIMT +jhWkIIZGDDgMJTzouBHfoSJtS1KpUZi99m2WyVs21MNKHeWAbs+zmI6TO5iiMC+T +uB8spaOiHFO1573Fmeer4sy3YA6qVoqVl6jjTQqOdy3frAMbCkwH22/crV8YA+08 +hLeHXrMK+6XUvU+EtHAM3VzcrLbuYJUI2XJbzTj5g0Eb8I8JWsHvWHR5K7Z7gceR +78AzxQmoGEfV6KABNWKsgoCQnfb1BidDJIe3BsI0A6UCAwEAAaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUABp0MlB14MSHgAcuNSOhs3MOlUcwDgYDVR0P +AQH/BAQDAgGGMA0GCSqGSIb3DQEBDAUAA4ICAQCv4CIOBSQi/QR9NxdRgVAG/pAh +tFJhV7OWb/wqwsNKFDtg6tTxwaahdCfWpGWId15OUe7G9LoPiKiwM9C92n0ZeHRz +4ewbrQVo7Eu1JI1wf0rnZJISL72hVYKmlvaWaacHhWxvsbKLrB7vt6Cknxa+S993 +Kf8i2Psw8j5886gaxhiUtzMTBwoDWak8ZaK7m3Y6C6hXQk08+3pnIornVSFJ9dlS +PAqt5UPwWmrEfF+0uIDORlT+cvrAwgSp7nUF1q8iasledycZ/BxFgQqzNwnkBDwQ +Z/aM52ArGsTzfMhkZRz9HIEhz1/0mJw8gZtDVQroD8778h8zsx2SrIz7eWQ6uWsD +QEeSWXpcheiUtEfzkDImjr2DLbwbA23c9LoexUD10nwohhoiQQg77LmvBVxeu7WU +E63JqaYUlOLOzEmNJp85zekIgR8UTkO7Gc+5BD7P4noYscI7pPOL5rP7YLg15ZFi +ega+G53NTckRXz4metsd8XFWloDjZJJq4FfD60VuxgXzoMNT9wpFTNSH42PR2s9L +I1vcl3w8yNccs9se2utM2nLsItZ3J0m/+QSRiw9hbrTYTcM9sXki0DtH2kyIOwYf +lOrGJDiYOIrXSQK36H0gQ+8omlrUTvUj4msvkXuQjlfgx6sgp2duOAfnGxE7uHnc +UhnJzzoe6M+LfGHkVQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICuDCCAj2gAwIBAgIQSAG6j2WHtWUUuLGJTPb1nTAKBggqhkjOPQQDAzCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIGFwLW5vcnRoZWFzdC0yIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyMDE2MzgyNloYDzIxMjEwNTIwMTczODI2WjCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIGFwLW5vcnRoZWFzdC0yIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE2eqwU4FOzW8RV1W381Bd +olhDOrqoMqzWli21oDUt7y8OnXM/lmAuOS6sr8Nt61BLVbONdbr+jgCYw75KabrK +ZGg3siqvMOgabIKkKuXO14wtrGyGDt7dnKXg5ERGYOZlo0IwQDAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBS1Acp2WYxOcblv5ikZ3ZIbRCCW+zAOBgNVHQ8BAf8E +BAMCAYYwCgYIKoZIzj0EAwMDaQAwZgIxAJL84J08PBprxmsAKPTotBuVI3MyW1r8 +xQ0i8lgCQUf8GcmYjQ0jI4oZyv+TuYJAcwIxAP9Xpzq0Docxb+4N1qVhpiOfWt1O +FnemFiy9m1l+wv6p3riQMPV7mBVpklmijkIv3Q== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgIRALZLcqCVIJ25maDPE3sbPCIwDQYJKoZIhvcNAQELBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1zb3V0aGVhc3QtMSBSb290IENBIFJTQTIwNDggRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjEwNTIxMjEzOTM5WhgPMjA2MTA1MjEyMjM5Mzla +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgYXAtc291dGhlYXN0LTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +ypKc+6FfGx6Gl6fQ78WYS29QoKgQiur58oxR3zltWeg5fqh9Z85K5S3UbRSTqWWu +Xcfnkz0/FS07qHX+nWAGU27JiQb4YYqhjZNOAq8q0+ptFHJ6V7lyOqXBq5xOzO8f ++0DlbJSsy7GEtJp7d7QCM3M5KVY9dENVZUKeJwa8PC5StvwPx4jcLeZRJC2rAVDG +SW7NAInbATvr9ssSh03JqjXb+HDyywiqoQ7EVLtmtXWimX+0b3/2vhqcH5jgcKC9 +IGFydrjPbv4kwMrKnm6XlPZ9L0/3FMzanXPGd64LQVy51SI4d5Xymn0Mw2kMX8s6 +Nf05OsWcDzJ1n6/Q1qHSxQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBRmaIc8eNwGP7i6P7AJrNQuK6OpFzAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI +hvcNAQELBQADggEBAIBeHfGwz3S2zwIUIpqEEI5/sMySDeS+3nJR+woWAHeO0C8i +BJdDh+kzzkP0JkWpr/4NWz84/IdYo1lqASd1Kopz9aT1+iROXaWr43CtbzjXb7/X +Zv7eZZFC8/lS5SROq42pPWl4ekbR0w8XGQElmHYcWS41LBfKeHCUwv83ATF0XQ6I +4t+9YSqZHzj4vvedrvcRInzmwWJaal9s7Z6GuwTGmnMsN3LkhZ+/GD6oW3pU/Pyh +EtWqffjsLhfcdCs3gG8x9BbkcJPH5aPAVkPn4wc8wuXg6xxb9YGsQuY930GWTYRf +schbgjsuqznW4HHakq4WNhs1UdTSTKkRdZz7FUQ= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIRAM2zAbhyckaqRim63b+Tib8wDQYJKoZIhvcNAQELBQAw +gZ8xCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE4MDYGA1UEAwwv +QW1hem9uIFJEUyBQcmV2aWV3IHVzLWVhc3QtMiBSb290IENBIFJTQTIwNDggRzEx +EDAOBgNVBAcMB1NlYXR0bGUwIBcNMjEwNTE4MjA0OTQ1WhgPMjA2MTA1MTgyMTQ5 +NDVaMIGfMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNl +cywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExODA2BgNV +BAMML0FtYXpvbiBSRFMgUHJldmlldyB1cy1lYXN0LTIgUm9vdCBDQSBSU0EyMDQ4 +IEcxMRAwDgYDVQQHDAdTZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA1ybjQMH1MkbvfKsWJaCTXeCSN1SG5UYid+Twe+TjuSqaXWonyp4WRR5z +tlkqq+L2MWUeQQAX3S17ivo/t84mpZ3Rla0cx39SJtP3BiA2BwfUKRjhPwOjmk7j +3zrcJjV5k1vSeLNOfFFSlwyDiVyLAE61lO6onBx+cRjelu0egMGq6WyFVidTdCmT +Q9Zw3W6LTrnPvPmEyjHy2yCHzH3E50KSd/5k4MliV4QTujnxYexI2eR8F8YQC4m3 +DYjXt/MicbqA366SOoJA50JbgpuVv62+LSBu56FpzY12wubmDZsdn4lsfYKiWxUy +uc83a2fRXsJZ1d3whxrl20VFtLFHFQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBRC0ytKmDYbfz0Bz0Psd4lRQV3aNTAOBgNVHQ8BAf8EBAMCAYYw +DQYJKoZIhvcNAQELBQADggEBAGv8qZu4uaeoF6zsbumauz6ea6tdcWt+hGFuwGrb +tRbI85ucAmVSX06x59DJClsb4MPhL1XmqO3RxVMIVVfRwRHWOsZQPnXm8OYQ2sny +rYuFln1COOz1U/KflZjgJmxbn8x4lYiTPZRLarG0V/OsCmnLkQLPtEl/spMu8Un7 +r3K8SkbWN80gg17Q8EV5mnFwycUx9xsTAaFItuG0en9bGsMgMmy+ZsDmTRbL+lcX +Fq8r4LT4QjrFz0shrzCwuuM4GmcYtBSxlacl+HxYEtAs5k10tmzRf6OYlY33tGf6 +1tkYvKryxDPF/EDgGp/LiBwx6ixYMBfISoYASt4V/ylAlHA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICtTCCAjqgAwIBAgIRAK9BSZU6nIe6jqfODmuVctYwCgYIKoZIzj0EAwMwgZkx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEyMDAGA1UEAwwpQW1h +em9uIFJEUyBjYS1jZW50cmFsLTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcM +B1NlYXR0bGUwIBcNMjEwNTIxMjIxMzA5WhgPMjEyMTA1MjEyMzEzMDlaMIGZMQsw +CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET +MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMjAwBgNVBAMMKUFtYXpv +biBSRFMgY2EtY2VudHJhbC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQHDAdT +ZWF0dGxlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEUkEERcgxneT5H+P+fERcbGmf +bVx+M7rNWtgWUr6w+OBENebQA9ozTkeSg4c4M+qdYSObFqjxITdYxT1z/nHz1gyx +OKAhLjWu+nkbRefqy3RwXaWT680uUaAP6ccnkZOMo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBSN6fxlg0s5Wny08uRBYZcQ3TUoyzAOBgNVHQ8BAf8EBAMC +AYYwCgYIKoZIzj0EAwMDaQAwZgIxAORaz+MBVoFBTmZ93j2G2vYTwA6T5hWzBWrx +CrI54pKn5g6At56DBrkjrwZF5T1enAIxAJe/LZ9xpDkAdxDgGJFN8gZYLRWc0NRy +Rb4hihy5vj9L+w9uKc9VfEBIFuhT7Z3ljg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIQB/57HSuaqUkLaasdjxUdPjANBgkqhkiG9w0BAQsFADCB +mDELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChB +bWF6b24gUkRTIGFwLXNvdXRoLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUxOTE3NDAzNFoYDzIwNjEwNTE5MTg0MDM0WjCBmDEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChBbWF6 +b24gUkRTIGFwLXNvdXRoLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQHDAdT +ZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtbkaoVsUS76o +TgLFmcnaB8cswBk1M3Bf4IVRcwWT3a1HeJSnaJUqWHCJ+u3ip/zGVOYl0gN1MgBb +MuQRIJiB95zGVcIa6HZtx00VezDTr3jgGWRHmRjNVCCHGmxOZWvJjsIE1xavT/1j +QYV/ph4EZEIZ/qPq7e3rHohJaHDe23Z7QM9kbyqp2hANG2JtU/iUhCxqgqUHNozV +Zd0l5K6KnltZQoBhhekKgyiHqdTrH8fWajYl5seD71bs0Axowb+Oh0rwmrws3Db2 +Dh+oc2PwREnjHeca9/1C6J2vhY+V0LGaJmnnIuOANrslx2+bgMlyhf9j0Bv8AwSi +dSWsobOhNQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQb7vJT +VciLN72yJGhaRKLn6Krn2TAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD +ggEBAAxEj8N9GslReAQnNOBpGl8SLgCMTejQ6AW/bapQvzxrZrfVOZOYwp/5oV0f +9S1jcGysDM+DrmfUJNzWxq2Y586R94WtpH4UpJDGqZp+FuOVJL313te4609kopzO +lDdmd+8z61+0Au93wB1rMiEfnIMkOEyt7D2eTFJfJRKNmnPrd8RjimRDlFgcLWJA +3E8wca67Lz/G0eAeLhRHIXv429y8RRXDtKNNz0wA2RwURWIxyPjn1fHjA9SPDkeW +E1Bq7gZj+tBnrqz+ra3yjZ2blss6Ds3/uRY6NYqseFTZWmQWT7FolZEnT9vMUitW +I0VynUbShVpGf6946e0vgaaKw20= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIQGyUVTaVjYJvWhroVEiHPpDANBgkqhkiG9w0BAQsFADCB +lzELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdB +bWF6b24gUkRTIHVzLXdlc3QtMSBSb290IENBIFJTQTIwNDggRzExEDAOBgNVBAcM +B1NlYXR0bGUwIBcNMjEwNTE5MTkwNDA2WhgPMjA2MTA1MTkyMDA0MDZaMIGXMQsw +CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET +MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpv +biBSRFMgdXMtd2VzdC0xIFJvb3QgQ0EgUlNBMjA0OCBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANhyXpJ0t4nigRDZ +EwNtFOem1rM1k8k5XmziHKDvDk831p7QsX9ZOxl/BT59Pu/P+6W6SvasIyKls1sW +FJIjFF+6xRQcpoE5L5evMgN/JXahpKGeQJPOX9UEXVW5B8yi+/dyUitFT7YK5LZA +MqWBN/LtHVPa8UmE88RCDLiKkqiv229tmwZtWT7nlMTTCqiAHMFcryZHx0pf9VPh +x/iPV8p2gBJnuPwcz7z1kRKNmJ8/cWaY+9w4q7AYlAMaq/rzEqDaN2XXevdpsYAK +TMMj2kji4x1oZO50+VPNfBl5ZgJc92qz1ocF95SAwMfOUsP8AIRZkf0CILJYlgzk +/6u6qZECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm5jfcS9o ++LwL517HpB6hG+PmpBswDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4IB +AQAcQ6lsqxi63MtpGk9XK8mCxGRLCad51+MF6gcNz6i6PAqhPOoKCoFqdj4cEQTF +F8dCfa3pvfJhxV6RIh+t5FCk/y6bWT8Ls/fYKVo6FhHj57bcemWsw/Z0XnROdVfK +Yqbc7zvjCPmwPHEqYBhjU34NcY4UF9yPmlLOL8uO1JKXa3CAR0htIoW4Pbmo6sA4 +6P0co/clW+3zzsQ92yUCjYmRNeSbdXbPfz3K/RtFfZ8jMtriRGuO7KNxp8MqrUho +HK8O0mlSUxGXBZMNicfo7qY8FD21GIPH9w5fp5oiAl7lqFzt3E3sCLD3IiVJmxbf +fUwpGd1XZBBSdIxysRLM6j48 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrTCCAjOgAwIBAgIQU+PAILXGkpoTcpF200VD/jAKBggqhkjOPQQDAzCBljEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMS8wLQYDVQQDDCZBbWF6 +b24gUkRTIGFwLWVhc3QtMSBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTAgFw0yMTA1MjUyMTQ1MTFaGA8yMTIxMDUyNTIyNDUxMVowgZYxCzAJBgNV +BAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYD +VQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEvMC0GA1UEAwwmQW1hem9uIFJE +UyBhcC1lYXN0LTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1NlYXR0bGUw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAT3tFKE8Kw1sGQAvNLlLhd8OcGhlc7MiW/s +NXm3pOiCT4vZpawKvHBzD76Kcv+ZZzHRxQEmG1/muDzZGlKR32h8AAj+NNO2Wy3d +CKTtYMiVF6Z2zjtuSkZQdjuQbe4eQ7qjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFAiSQOp16Vv0Ohpvqcbd2j5RmhYNMA4GA1UdDwEB/wQEAwIBhjAKBggq +hkjOPQQDAwNoADBlAjBVsi+5Ape0kOhMt/WFkANkslD4qXA5uqhrfAtH29Xzz2NV +tR7akiA771OaIGB/6xsCMQCZt2egCtbX7J0WkuZ2KivTh66jecJr5DHvAP4X2xtS +F/5pS+AUhcKTEGjI9jDH3ew= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICuDCCAj2gAwIBAgIQT5mGlavQzFHsB7hV6Mmy6TAKBggqhkjOPQQDAzCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIGFwLXNvdXRoZWFzdC0yIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyNDIwNTAxNVoYDzIxMjEwNTI0MjE1MDE1WjCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIGFwLXNvdXRoZWFzdC0yIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEcm4BBBjYK7clwm0HJRWS +flt3iYwoJbIXiXn9c1y3E+Vb7bmuyKhS4eO8mwO4GefUcXObRfoHY2TZLhMJLVBQ +7MN2xDc0RtZNj07BbGD3VAIFRTDX0mH9UNYd0JQM3t/Oo0IwQDAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBRrd5ITedfAwrGo4FA9UaDaGFK3rjAOBgNVHQ8BAf8E +BAMCAYYwCgYIKoZIzj0EAwMDaQAwZgIxAPBNqmVv1IIA3EZyQ6XuVf4gj79/DMO8 +bkicNS1EcBpUqbSuU4Zwt2BYc8c/t7KVOQIxAOHoWkoKZPiKyCxfMtJpCZySUG+n +sXgB/LOyWE5BJcXUfm+T1ckeNoWeUUMOLmnJjg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgIRAJcDeinvdNrDQBeJ8+t38WQwDQYJKoZIhvcNAQELBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1zb3V0aGVhc3QtNCBSb290IENBIFJTQTIwNDggRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjIwNTI1MTY0OTE2WhgPMjA2MjA1MjUxNzQ5MTZa +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgYXAtc291dGhlYXN0LTQgUm9vdCBDQSBSU0EyMDQ4IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +k8DBNkr9tMoIM0NHoFiO7cQfSX0cOMhEuk/CHt0fFx95IBytx7GHCnNzpM27O5z6 +x6iRhfNnx+B6CrGyCzOjxvPizneY+h+9zfvNz9jj7L1I2uYMuiNyOKR6FkHR46CT +1CiArfVLLPaTqgD/rQjS0GL2sLHS/0dmYipzynnZcs613XT0rAWdYDYgxDq7r/Yi +Xge5AkWQFkMUq3nOYDLCyGGfQqWKkwv6lZUHLCDKf+Y0Uvsrj8YGCI1O8mF0qPCQ +lmlfaDvbuBu1AV+aabmkvyFj3b8KRIlNLEtQ4N8KGYR2Jdb82S4YUGIOAt4wuuFt +1B7AUDLk3V/u+HTWiwfoLQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBSNpcjz6ArWBtAA+Gz6kyyZxrrgdDAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI +hvcNAQELBQADggEBAGJEd7UgOzHYIcQRSF7nSYyjLROyalaIV9AX4WXW/Cqlul1c +MblP5etDZm7A/thliZIWAuyqv2bNicmS3xKvNy6/QYi1YgxZyy/qwJ3NdFl067W0 +t8nGo29B+EVK94IPjzFHWShuoktIgp+dmpijB7wkTIk8SmIoe9yuY4+hzgqk+bo4 +ms2SOXSN1DoQ75Xv+YmztbnZM8MuWhL1T7hA4AMorzTQLJ9Pof8SpSdMHeDsHp0R +01jogNFkwy25nw7cL62nufSuH2fPYGWXyNDg+y42wKsKWYXLRgUQuDVEJ2OmTFMB +T0Vf7VuNijfIA9hkN2d3K53m/9z5WjGPSdOjGhg= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIQRiwspKyrO0xoxDgSkqLZczANBgkqhkiG9w0BAQsFADCB +lzELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdB +bWF6b24gUkRTIHVzLXdlc3QtMiBSb290IENBIFJTQTIwNDggRzExEDAOBgNVBAcM +B1NlYXR0bGUwIBcNMjEwNTI0MjE1OTAwWhgPMjA2MTA1MjQyMjU5MDBaMIGXMQsw +CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET +MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpv +biBSRFMgdXMtd2VzdC0yIFJvb3QgQ0EgUlNBMjA0OCBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL53Jk3GsKiu+4bx +jDfsevWbwPCNJ3H08Zp7GWhvI3Tgi39opfHYv2ku2BKFjK8N2L6RvNPSR8yplv5j +Y0tK0U+XVNl8o0ibhqRDhbTuh6KL8CFINWYzAajuxFS+CF0U6c1Q3tXLBdALxA7l +FlXJ71QrP06W31kRe7kvgrvO7qWU3/OzUf9qYw4LSiR1/VkvvRCTqcVNw09clw/M +Jbw6FSgweN65M9j7zPbjGAXSHkXyxH1Erin2fa+B9PE4ZDgX9cp2C1DHewYJQL/g +SepwwcudVNRN1ibKH7kpMrgPnaNIVNx5sXVsTjk6q2ZqYw3SVHegltJpLy/cZReP +mlivF2kCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUmTcQd6o1 +CuS65MjBrMwQ9JJjmBwwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4IB +AQAKSDSIzl956wVddPThf2VAzI8syw9ngSwsEHZvxVGHBvu5gg618rDyguVCYX9L +4Kw/xJrk6S3qxOS2ZDyBcOpsrBskgahDFIunzoRP3a18ARQVq55LVgfwSDQiunch +Bd05cnFGLoiLkR5rrkgYaP2ftn3gRBRaf0y0S3JXZ2XB3sMZxGxavYq9mfiEcwB0 +LMTMQ1NYzahIeG6Jm3LqRqR8HkzP/Ztq4dT2AtSLvFebbNMiWqeqT7OcYp94HTYT +zqrtaVdUg9bwyAUCDgy0GV9RHDIdNAOInU/4LEETovrtuBU7Z1q4tcHXvN6Hd1H8 +gMb0mCG5I393qW5hFsA/diFb +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgIRAPQAvihfjBg/JDbj6U64K98wDQYJKoZIhvcNAQELBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1ub3J0aGVhc3QtMiBSb290IENBIFJTQTIwNDggRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjEwNTIwMTYyODQxWhgPMjA2MTA1MjAxNzI4NDFa +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgYXAtbm9ydGhlYXN0LTIgUm9vdCBDQSBSU0EyMDQ4IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +vJ9lgyksCxkBlY40qOzI1TCj/Q0FVGuPL/Z1Mw2YN0l+41BDv0FHApjTUkIKOeIP +nwDwpXTa3NjYbk3cOZ/fpH2rYJ++Fte6PNDGPgKppVCUh6x3jiVZ1L7wOgnTdK1Q +Trw8440IDS5eLykRHvz8OmwvYDl0iIrt832V0QyOlHTGt6ZJ/aTQKl12Fy3QBLv7 +stClPzvHTrgWqVU6uidSYoDtzHbU7Vda7YH0wD9IUoMBf7Tu0rqcE4uH47s2XYkc +SdLEoOg/Ngs7Y9B1y1GCyj3Ux7hnyvCoRTw014QyNB7dTatFMDvYlrRDGG14KeiU +UL7Vo/+EejWI31eXNLw84wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBQkgTWFsNg6wA3HbbihDQ4vpt1E2zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI +hvcNAQELBQADggEBAGz1Asiw7hn5WYUj8RpOCzpE0h/oBZcnxP8wulzZ5Xd0YxWO +0jYUcUk3tTQy1QvoY+Q5aCjg6vFv+oFBAxkib/SmZzp4xLisZIGlzpJQuAgRkwWA +6BVMgRS+AaOMQ6wKPgz1x4v6T0cIELZEPq3piGxvvqkcLZKdCaeC3wCS6sxuafzZ +4qA3zMwWuLOzRftgX2hQto7d/2YkRXga7jSvQl3id/EI+xrYoH6zIWgjdU1AUaNq +NGT7DIo47vVMfnd9HFZNhREsd4GJE83I+JhTqIxiKPNxrKgESzyADmNPt0gXDnHo +tbV1pMZz5HpJtjnP/qVZhEK5oB0tqlKPv9yx074= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICuTCCAj6gAwIBAgIRAKp1Rn3aL/g/6oiHVIXtCq8wCgYIKoZIzj0EAwMwgZsx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE0MDIGA1UEAwwrQW1h +em9uIFJEUyBhcC1ub3J0aGVhc3QtMyBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UE +BwwHU2VhdHRsZTAgFw0yMTA1MjQyMDMyMTdaGA8yMTIxMDUyNDIxMzIxN1owgZsx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE0MDIGA1UEAwwrQW1h +em9uIFJEUyBhcC1ub3J0aGVhc3QtMyBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UE +BwwHU2VhdHRsZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABGTYWPILeBJXfcL3Dz4z +EWMUq78xB1HpjBwHoTURYfcMd5r96BTVG6yaUBWnAVCMeeD6yTG9a1eVGNhG14Hk +ZAEjgLiNB7RRbEG5JZ/XV7W/vODh09WCst2y9SLKsdgeAaNCMEAwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUoE0qZHmDCDB+Bnm8GUa/evpfPwgwDgYDVR0PAQH/ +BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYCMQCnil5MMwhY3qoXv0xvcKZGxGPaBV15 +0CCssCKn0oVtdJQfJQ3Jrf3RSaEyijXIJsoCMQC35iJi4cWoNX3N/qfgnHohW52O +B5dg0DYMqy5cNZ40+UcAanRMyqNQ6P7fy3umGco= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICtzCCAj2gAwIBAgIQPXnDTPegvJrI98qz8WxrMjAKBggqhkjOPQQDAzCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIEJldGEgdXMtZWFzdC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUxODIxNDAxMloYDzIxMjEwNTE4MjI0MDEyWjCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIEJldGEgdXMtZWFzdC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEI0sR7gwutK5AB46hM761 +gcLTGBIYlURSEoM1jcBwy56CL+3CJKZwLLyJ7qoOKfWbu5GsVLUTWS8MV6Nw33cx +2KQD2svb694wi+Px2f4n9+XHkEFQw8BbiodDD7RZA70fo0IwQDAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTQSioOvnVLEMXwNSDg+zgln/vAkjAOBgNVHQ8BAf8E +BAMCAYYwCgYIKoZIzj0EAwMDaAAwZQIxAMwu1hqm5Bc98uE/E0B5iMYbBQ4kpMxO +tP8FTfz5UR37HUn26nXE0puj6S/Ffj4oJgIwXI7s2c26tFQeqzq6u3lrNJHp5jC9 +Uxlo/hEJOLoDj5jnpxo8dMAtCNoQPaHdfL0P +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF/jCCA+agAwIBAgIQEM1pS+bWfBJeu/6j1yIIFzANBgkqhkiG9w0BAQwFADCB +lzELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdB +bWF6b24gUkRTIGNhLXdlc3QtMSBSb290IENBIFJTQTQwOTYgRzExEDAOBgNVBAcM +B1NlYXR0bGUwIBcNMjMwOTE5MjIwMTM5WhgPMjEyMzA5MTkyMzAxMzlaMIGXMQsw +CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET +MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpv +biBSRFMgY2Etd2VzdC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Pyp8p5z6HnlGB +daOj78gZ3ABufxnBFiu5NdFiGoMrS+eY//xxr2iKbnynJAzjmn5A6VKMNxtbuYIZ +WKAzDb/HrWlIYD2w7ZVBXpylfPhiz3jLNsl03WdPNnEruCcivhY2QMewEVtzjPU0 +ofdbZlO2KpF3biv1gjPuIuE7AUyQAbWnWTlrzETAVWLboJJRRqxASSkFUHNLXod7 +ow02FwlAhcnCp9gSe1SKRDrpvvEvYQBAFB7owfnoQzOGDdd87RGyYfyuW8aFI2Z0 +LHNvsA0dTafO4Rh986c72kDL7ijICQdr5OTgZR2OnuESLk1DSK4xYJ4fA6jb5dJ5 ++xsI6tCPykWCW98aO/pha35OsrVNifL/5cH5pdv/ecgQGdffJB+Vdj6f/ZMwR6s/ +Rm37cQ9l3tU8eu/qpzsFjLq1ZUzDaVDWgMW9t49+q/zjhdmbPOabZDao7nHXrVRw +rwPHWCmEY4OmH6ikEKQW3AChFjOdSg4me/J0Jr5l5jKggLPHWbNLRO8qTTK6N8qk +ui3aJDi+XQfsTPARXIw4UFErArNImTsoZVyqfX7I4shp0qZbEhP6kRAbfPljw5kW +Yat7ZlXqDanjsreqbLTaOU10P0rC0/4Ctv5cLSKCrzRLWtpXxhKa2wJTQ74G6fAZ +1oUA79qg3F8nyM+ZzDsfNI854+PNAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8w +HQYDVR0OBBYEFLRWiDabEQZNkzEPUCr1ZVJV6xpwMA4GA1UdDwEB/wQEAwIBhjAN +BgkqhkiG9w0BAQwFAAOCAgEATkVVzkkGBjEtLGDtERi+fSpIV0MxwAsA4PAeBBmb +myxo90jz6kWkKM1Wm4BkZM8/mq5VbxPef1kxHfb5CHksCL6SgG5KujfIvht+KT2a +MRJB+III3CbcTy0HtwCX5AlPIbXWydhQFoJTW/OkpecUWoyFM6SqYeYZx1itJpxl +sXshLjYOvw+QgvxRsDxqUfkcaC/N2yhu/30Zo2P8msJfAFry2UmA/TBrWOQKVQxl +Ee/yWgp4U/bC/GZnjWnWDTwkRFGQtI4wjxbVuX6V4FTLCT7kIoHBhG+zOSduJRn3 +Axej7gkEXEVc/PAnwp/kSJ/b0/JONLWdjGUFkyiMn1yJlhJ2sg39vepBN5r6yVYU +nJWoZAuupRpoIKfmC3/cZanXqYbYl4yxzX/PMB4kAACfdxGxLawjnnBjSzaWokXs +YVh2TjWpUMwLOi0RB2mtPUjHdDLKtjOTZ1zHZnR/wVp9BmVI1BXYnz5PAqU5XqeD +EmanyaAuFCeyol1EtbQhgtysThQ+vwYAXMm2iKzJxq0hik8wyG8X55FhnGEOGV3u +xxq7odd3/8BXkc3dGdBPQtH+k5glaQyPnAsLVAIUvyzTmy58saL+nJnQY4mmRrwV +1jJA7nnkaklI/L5fvfCg0W+TMinCOAGd+GQ4hK2SAsJLtcqiBgPf2wJHO8wiwUh9 +Luw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrjCCAjWgAwIBAgIQGKVv+5VuzEZEBzJ+bVfx2zAKBggqhkjOPQQDAzCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIGFwLXNvdXRoLTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjEwNTE5MTc1MDU5WhgPMjEyMTA1MTkxODUwNTlaMIGXMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpvbiBS +RFMgYXAtc291dGgtMSBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwHU2VhdHRs +ZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMqdLJ0tZF/DGFZTKZDrGRJZID8ivC2I +JRCYTWweZKCKSCAzoiuGGHzJhr5RlLHQf/QgmFcgXsdmO2n3CggzhA4tOD9Ip7Lk +P05eHd2UPInyPCHRgmGjGb0Z+RdQ6zkitKNCMEAwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUC1yhRgVqU5bR8cGzOUCIxRpl4EYwDgYDVR0PAQH/BAQDAgGGMAoG +CCqGSM49BAMDA2cAMGQCMG0c/zLGECRPzGKJvYCkpFTCUvdP4J74YP0v/dPvKojL +t/BrR1Tg4xlfhaib7hPc7wIwFvgqHes20CubQnZmswbTKLUrgSUW4/lcKFpouFd2 +t2/ewfi/0VhkeUW+IiHhOMdU +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGCTCCA/GgAwIBAgIRAOXxJuyXVkbfhZCkS/dOpfEwDQYJKoZIhvcNAQEMBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1ub3J0aGVhc3QtMSBSb290IENBIFJTQTQwOTYgRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjEwNTI1MjE1OTEwWhgPMjEyMTA1MjUyMjU5MTBa +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgYXAtbm9ydGhlYXN0LTEgUm9vdCBDQSBSU0E0MDk2IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA +xiP4RDYm4tIS12hGgn1csfO8onQDmK5SZDswUpl0HIKXOUVVWkHNlINkVxbdqpqH +FhbyZmNN6F/EWopotMDKe1B+NLrjNQf4zefv2vyKvPHJXhxoKmfyuTd5Wk8k1F7I +lNwLQzznB+ElhrLIDJl9Ro8t31YBBNFRGAGEnxyACFGcdkjlsa52UwfYrwreEg2l +gW5AzqHgjFfj9QRLydeU/n4bHm0F1adMsV7P3rVwilcUlqsENDwXnWyPEyv3sw6F +wNemLEs1129mB77fwvySb+lLNGsnzr8w4wdioZ74co+T9z2ca+eUiP+EQccVw1Is +D4Fh57IjPa6Wuc4mwiUYKkKY63+38aCfEWb0Qoi+zW+mE9nek6MOQ914cN12u5LX +dBoYopphRO5YmubSN4xcBy405nIdSdbrAVWwxXnVVyjqjknmNeqQsPZaxAhdoKhV +AqxNr8AUAdOAO6Sz3MslmcLlDXFihrEEOeUbpg/m1mSUUHGbu966ajTG1FuEHHwS +7WB52yxoJo/tHvt9nAWnh3uH5BHmS8zn6s6CGweWKbX5yICnZ1QFR1e4pogxX39v +XD6YcNOO+Vn+HY4nXmjgSYVC7l+eeP8eduMg1xJujzjrbmrXU+d+cBObgdTOAlpa +JFHaGwYw1osAwPCo9cZ2f04yitBfj9aPFia8ASKldakCAwEAAaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUqKS+ltlior0SyZKYAkJ/efv55towDgYDVR0P +AQH/BAQDAgGGMA0GCSqGSIb3DQEBDAUAA4ICAQAdElvp8bW4B+Cv+1WSN87dg6TN +wGyIjJ14/QYURgyrZiYpUmZpj+/pJmprSWXu4KNyqHftmaidu7cdjL5nCAvAfnY5 +/6eDDbX4j8Gt9fb/6H9y0O0dn3mUPSEKG0crR+JRFAtPhn/2FNvst2P82yguWLv0 +pHjHVUVcq+HqDMtUIJsTPYjSh9Iy77Q6TOZKln9dyDOWJpCSkiUWQtMAKbCSlvzd +zTs/ahqpT+zLfGR1SR+T3snZHgQnbnemmz/XtlKl52NxccARwfcEEKaCRQyGq/pR +0PVZasyJS9JY4JfQs4YOdeOt4UMZ8BmW1+BQWGSkkb0QIRl8CszoKofucAlqdPcO +IT/ZaMVhI580LFGWiQIizWFskX6lqbCyHqJB3LDl8gJISB5vNTHOHpvpMOMs5PYt +cRl5Mrksx5MKMqG7y5R734nMlZxQIHjL5FOoOxTBp9KeWIL/Ib89T2QDaLw1SQ+w +ihqWBJ4ZdrIMWYpP3WqM+MXWk7WAem+xsFJdR+MDgOOuobVQTy5dGBlPks/6gpjm +rO9TjfQ36ppJ3b7LdKUPeRfnYmlR5RU4oyYJ//uLbClI443RZAgxaCXX/nyc12lr +eVLUMNF2abLX4/VF63m2/Z9ACgMRfqGshPssn1NN33OonrotQoj4S3N9ZrjvzKt8 +iHcaqd60QKpfiH2A3A== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICuDCCAj2gAwIBAgIQPaVGRuu86nh/ylZVCLB0MzAKBggqhkjOPQQDAzCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIGFwLW5vcnRoZWFzdC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyNTIyMDMxNloYDzIxMjEwNTI1MjMwMzE2WjCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIGFwLW5vcnRoZWFzdC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEexNURoB9KE93MEtEAlJG +obz4LS/pD2hc8Gczix1WhVvpJ8bN5zCDXaKdnDMCebetyRQsmQ2LYlfmCwpZwSDu +0zowB11Pt3I5Avu2EEcuKTlKIDMBeZ1WWuOd3Tf7MEAMo0IwQDAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBSaYbZPBvFLikSAjpa8mRJvyArMxzAOBgNVHQ8BAf8E +BAMCAYYwCgYIKoZIzj0EAwMDaQAwZgIxAOEJkuh3Zjb7Ih/zuNRd1RBqmIYcnyw0 +nwUZczKXry+9XebYj3VQxSRNadrarPWVqgIxAMg1dyGoDAYjY/L/9YElyMnvHltO +PwpJShmqHvCLc/mXMgjjYb/akK7yGthvW6j/uQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGCDCCA/CgAwIBAgIQChu3v5W1Doil3v6pgRIcVzANBgkqhkiG9w0BAQwFADCB +nDELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTUwMwYDVQQDDCxB +bWF6b24gUkRTIEJldGEgdXMtZWFzdC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4G +A1UEBwwHU2VhdHRsZTAgFw0yMTA1MTgyMTM0MTVaGA8yMTIxMDUxODIyMzQxNVow +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBCZXRhIHVzLWVhc3QtMSBSb290IENBIFJTQTQwOTYgRzExEDAO +BgNVBAcMB1NlYXR0bGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC1 +FUGQ5tf3OwpDR6hGBxhUcrkwKZhaXP+1St1lSOQvjG8wXT3RkKzRGMvb7Ee0kzqI +mzKKe4ASIhtV3UUWdlNmP0EA3XKnif6N79MismTeGkDj75Yzp5A6tSvqByCgxIjK +JqpJrch3Dszoyn8+XhwDxMZtkUa5nQVdJgPzJ6ltsQ8E4SWLyLtTu0S63jJDkqYY +S7cQblk7y7fel+Vn+LS5dGTdRRhMvSzEnb6mkVBaVzRyVX90FNUED06e8q+gU8Ob +htvQlf9/kRzHwRAdls2YBhH40ZeyhpUC7vdtPwlmIyvW5CZ/QiG0yglixnL6xahL +pbmTuTSA/Oqz4UGQZv2WzHe1lD2gRHhtFX2poQZeNQX8wO9IcUhrH5XurW/G9Xwl +Sat9CMPERQn4KC3HSkat4ir2xaEUrjfg6c4XsGyh2Pk/LZ0gLKum0dyWYpWP4JmM +RQNjrInXPbMhzQObozCyFT7jYegS/3cppdyy+K1K7434wzQGLU1gYXDKFnXwkX8R +bRKgx2pHNbH5lUddjnNt75+e8m83ygSq/ZNBUz2Ur6W2s0pl6aBjwaDES4VfWYlI +jokcmrGvJNDfQWygb1k00eF2bzNeNCHwgWsuo3HSxVgc/WGsbcGrTlDKfz+g3ich +bXUeUidPhRiv5UQIVCLIHpHuin3bj9lQO/0t6p+tAQIDAQABo0IwQDAPBgNVHRMB +Af8EBTADAQH/MB0GA1UdDgQWBBSFmMBgm5IsRv3hLrvDPIhcPweXYTAOBgNVHQ8B +Af8EBAMCAYYwDQYJKoZIhvcNAQEMBQADggIBAAa2EuozymOsQDJlEi7TqnyA2OhT +GXPfYqCyMJVkfrqNgcnsNpCAiNEiZbb+8sIPXnT8Ay8hrwJYEObJ5b7MHXpLuyft +z0Pu1oFLKnQxKjNxrIsCvaB4CRRdYjm1q7EqGhMGv76se9stOxkOqO9it31w/LoU +ENDk7GLsSqsV1OzYLhaH8t+MaNP6rZTSNuPrHwbV3CtBFl2TAZ7iKgKOhdFz1Hh9 +Pez0lG+oKi4mHZ7ajov6PD0W7njn5KqzCAkJR6OYmlNVPjir+c/vUtEs0j+owsMl +g7KE5g4ZpTRShyh5BjCFRK2tv0tkqafzNtxrKC5XNpEkqqVTCnLcKG+OplIEadtr +C7UWf4HyhCiR+xIyxFyR05p3uY/QQU/5uza7GlK0J+U1sBUytx7BZ+Fo8KQfPPqV +CqDCaYUksoJcnJE/KeoksyqNQys7sDGJhkd0NeUGDrFLKHSLhIwAMbEWnqGxvhli +E7sP2E5rI/I9Y9zTbLIiI8pfeZlFF8DBdoP/Hzg8pqsiE/yiXSFTKByDwKzGwNqz +F0VoFdIZcIbLdDbzlQitgGpJtvEL7HseB0WH7B2PMMD8KPJlYvPveO3/6OLzCsav ++CAkvk47NQViKMsUTKOA0JDCW+u981YRozxa3K081snhSiSe83zIPBz1ikldXxO9 +6YYLNPRrj3mi9T/f +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrjCCAjSgAwIBAgIRAMkvdFnVDb0mWWFiXqnKH68wCgYIKoZIzj0EAwMwgZYx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEvMC0GA1UEAwwmQW1h +em9uIFJEUyB1cy13ZXN0LTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjEwNTE5MTkxMzI0WhgPMjEyMTA1MTkyMDEzMjRaMIGWMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExLzAtBgNVBAMMJkFtYXpvbiBS +RFMgdXMtd2VzdC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQHDAdTZWF0dGxl +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEy86DB+9th/0A5VcWqMSWDxIUblWTt/R0 +ao6Z2l3vf2YDF2wt1A2NIOGpfQ5+WAOJO/IQmnV9LhYo+kacB8sOnXdQa6biZZkR +IyouUfikVQAKWEJnh1Cuo5YMM4E2sUt5o0IwQDAPBgNVHRMBAf8EBTADAQH/MB0G +A1UdDgQWBBQ8u3OnecANmG8OoT7KLWDuFzZwBTAOBgNVHQ8BAf8EBAMCAYYwCgYI +KoZIzj0EAwMDaAAwZQIwQ817qkb7mWJFnieRAN+m9W3E0FLVKaV3zC5aYJUk2fcZ +TaUx3oLp3jPLGvY5+wgeAjEA6wAicAki4ZiDfxvAIuYiIe1OS/7H5RA++R8BH6qG +iRzUBM/FItFpnkus7u/eTkvo +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrzCCAjWgAwIBAgIQS/+Ryfgb/IOVEa1pWoe8oTAKBggqhkjOPQQDAzCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIGFwLXNvdXRoLTIgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjIwNjA2MjE1NDQyWhgPMjEyMjA2MDYyMjU0NDJaMIGXMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpvbiBS +RFMgYXAtc291dGgtMiBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwHU2VhdHRs +ZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDsX6fhdUWBQpYTdseBD/P3s96Dtw2Iw +OrXKNToCnmX5nMkUGdRn9qKNiz1pw3EPzaPxShbYwQ7LYP09ENK/JN4QQjxMihxC +jLFxS85nhBQQQGRCWikDAe38mD8fSvREQKNCMEAwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUIh1xZiseQYFjPYKJmGbruAgRH+AwDgYDVR0PAQH/BAQDAgGGMAoG +CCqGSM49BAMDA2gAMGUCMFudS4zLy+UUGrtgNLtRMcu/DZ9BUzV4NdHxo0bkG44O +thnjl4+wTKI6VbyAbj2rkgIxAOHps8NMITU5DpyiMnKTxV8ubb/WGHrLl0BjB8Lw +ETVJk5DNuZvsIIcm7ykk6iL4Tw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGBDCCA+ygAwIBAgIQDcEmNIAVrDpUw5cH5ynutDANBgkqhkiG9w0BAQwFADCB +mjELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTMwMQYDVQQDDCpB +bWF6b24gUkRTIG1lLWNlbnRyYWwtMSBSb290IENBIFJTQTQwOTYgRzExEDAOBgNV +BAcMB1NlYXR0bGUwIBcNMjIwNTA3MDA0MDIzWhgPMjEyMjA1MDcwMTQwMjNaMIGa +MQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5j +LjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMzAxBgNVBAMMKkFt +YXpvbiBSRFMgbWUtY2VudHJhbC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UE +BwwHU2VhdHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKvADk8t +Fl9bFlU5sajLPPDSOUpPAkKs6iPlz+27o1GJC88THcOvf3x0nVAcu9WYe9Qaas+4 +j4a0vv51agqyODRD/SNi2HnqW7DbtLPAm6KBHe4twl28ItB/JD5g7u1oPAHFoXMS +cH1CZEAs5RtlZGzJhcBXLFsHNv/7+SCLyZ7+2XFh9OrtgU4wMzkHoRNndhfwV5bu +17bPTwuH+VxH37zXf1mQ/KjhuJos0C9dL0FpjYBAuyZTAWhZKs8dpSe4DI544z4w +gkwUB4bC2nA1TBzsywEAHyNuZ/xRjNpWvx0ToWAA2iFJqC3VO3iKcnBplMvaUuMt +jwzVSNBnKcoabXCZL2XDLt4YTZR8FSwz05IvsmwcPB7uNTBXq3T9sjejW8QQK3vT +tzyfLq4jKmQE7PoS6cqYm+hEPm2hDaC/WP9bp3FdEJxZlPH26fq1b7BWYWhQ9pBA +Nv9zTnzdR1xohTyOJBUFQ81ybEzabqXqVXUIANqIOaNcTB09/sLJ7+zuMhp3mwBu +LtjfJv8PLuT1r63bU3seROhKA98b5KfzjvbvPSg3vws78JQyoYGbqNyDfyjVjg3U +v//AdVuPie6PNtdrW3upZY4Qti5IjP9e3kimaJ+KAtTgMRG56W0WxD3SP7+YGGbG +KhntDOkKsN39hLpn9UOafTIqFu7kIaueEy/NAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFHAems86dTwdZbLe8AaPy3kfIUVoMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQwFAAOCAgEAOBHpp0ICx81kmeoBcZTrMdJs2gnhcd85 +FoSCjXx9H5XE5rmN/lQcxxOgj8hr3uPuLdLHu+i6THAyzjrl2NA1FWiqpfeECGmy +0jm7iZsYORgGQYp/VKnDrwnKNSqlZvOuRr0kfUexwFlr34Y4VmupvEOK/RdGsd3S ++3hiemcHse9ST/sJLHx962AWMkN86UHPscJEe4+eT3f2Wyzg6La8ARwdWZSNS+WH +ZfybrncMmuiXuUdHv9XspPsqhKgtHhcYeXOGUtrwQPLe3+VJZ0LVxhlTWr9951GZ +GfmWwTV/9VsyKVaCFIXeQ6L+gjcKyEzYF8wpMtQlSc7FFqwgC4bKxvMBSaRy88Nr +lV2+tJD/fr8zGUeBK44Emon0HKDBWGX+/Hq1ZIv0Da0S+j6LbA4fusWxtGfuGha+ +luhHgVInCpALIOamiBEdGhILkoTtx7JrYppt3/Raqg9gUNCOOYlCvGhqX7DXeEfL +DGabooiY2FNWot6h04JE9nqGj5QqT8D6t/TL1nzxhRPzbcSDIHUd/b5R+a0bAA+7 +YTU6JqzEVCWKEIEynYmqikgLMGB/OzWsgyEL6822QW6hJAQ78XpbNeCzrICF4+GC +7KShLnwuWoWpAb26268lvOEvCTFM47VC6jNQl97md+2SA9Ma81C9wflid2M83Wle +cuLMVcQZceE= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIQAhAteLRCvizAElaWORFU2zANBgkqhkiG9w0BAQsFADCB +mDELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChB +bWF6b24gUkRTIG1lLXNvdXRoLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyMDE3MDkxNloYDzIwNjEwNTIwMTgwOTE2WjCBmDEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChBbWF6 +b24gUkRTIG1lLXNvdXRoLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQHDAdT +ZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+qg7JAcOVKjh +N83SACnBFZPyB63EusfDr/0V9ZdL8lKcmZX9sv/CqoBo3N0EvBqHQqUUX6JvFb7F +XrMUZ740kr28gSRALfXTFgNODjXeDsCtEkKRTkac/UM8xXHn+hR7UFRPHS3e0GzI +iLiwQWDkr0Op74W8aM0CfaVKvh2bp4BI1jJbdDnQ9OKXpOxNHGUf0ZGb7TkNPkgI +b2CBAc8J5o3H9lfw4uiyvl6Fz5JoP+A+zPELAioYBXDrbE7wJeqQDJrETWqR9VEK +BXURCkVnHeaJy123MpAX2ozf4pqk0V0LOEOZRS29I+USF5DcWr7QIXR/w2I8ws1Q +7ys+qbE+kQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQFJ16n +1EcCMOIhoZs/F9sR+Jy++zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD +ggEBAOc5nXbT3XTDEZsxX2iD15YrQvmL5m13B3ImZWpx/pqmObsgx3/dg75rF2nQ +qS+Vl+f/HLh516pj2BPP/yWCq12TRYigGav8UH0qdT3CAClYy2o+zAzUJHm84oiB +ud+6pFVGkbqpsY+QMpJUbZWu52KViBpJMYsUEy+9cnPSFRVuRAHjYynSiLk2ZEjb +Wkdc4x0nOZR5tP0FgrX0Ve2KcjFwVQJVZLgOUqmFYQ/G0TIIGTNh9tcmR7yp+xJR +A2tbPV2Z6m9Yxx4E8lLEPNuoeouJ/GR4CkMEmF8cLwM310t174o3lKKUXJ4Vs2HO +Wj2uN6R9oI+jGLMSswTzCNV1vgc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICuDCCAj6gAwIBAgIRAOocLeZWjYkG/EbHmscuy8gwCgYIKoZIzj0EAwMwgZsx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE0MDIGA1UEAwwrQW1h +em9uIFJEUyBhcC1zb3V0aGVhc3QtMSBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UE +BwwHU2VhdHRsZTAgFw0yMTA1MjEyMTUwMDFaGA8yMTIxMDUyMTIyNTAwMVowgZsx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE0MDIGA1UEAwwrQW1h +em9uIFJEUyBhcC1zb3V0aGVhc3QtMSBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UE +BwwHU2VhdHRsZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABCEr3jq1KtRncnZfK5cq +btY0nW6ZG3FMbh7XwBIR6Ca0f8llGZ4vJEC1pXgiM/4Dh045B9ZIzNrR54rYOIfa +2NcYZ7mk06DjIQML64hbAxbQzOAuNzLPx268MrlL2uW2XaNCMEAwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUln75pChychwN4RfHl+tOinMrfVowDgYDVR0PAQH/ +BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMGiyPINRU1mwZ4Crw01vpuPvxZxb2IOr +yX3RNlOIu4We1H+5dQk5tIvH8KGYFbWEpAIxAO9NZ6/j9osMhLgZ0yj0WVjb+uZx +YlZR9fyFisY/jNfX7QhSk+nrc3SFLRUNtpXrng== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEBTCCAu2gAwIBAgIRAKiaRZatN8eiz9p0s0lu0rQwDQYJKoZIhvcNAQELBQAw +gZoxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEzMDEGA1UEAwwq +QW1hem9uIFJEUyBjYS1jZW50cmFsLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYD +VQQHDAdTZWF0dGxlMCAXDTIxMDUyMTIyMDIzNVoYDzIwNjEwNTIxMjMwMjM1WjCB +mjELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTMwMQYDVQQDDCpB +bWF6b24gUkRTIGNhLWNlbnRyYWwtMSBSb290IENBIFJTQTIwNDggRzExEDAOBgNV +BAcMB1NlYXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCygVMf +qB865IR9qYRBRFHn4eAqGJOCFx+UbraQZmjr/mnRqSkY+nhbM7Pn/DWOrRnxoh+w +q5F9ZxdZ5D5T1v6kljVwxyfFgHItyyyIL0YS7e2h7cRRscCM+75kMedAP7icb4YN +LfWBqfKHbHIOqvvQK8T6+Emu/QlG2B5LvuErrop9K0KinhITekpVIO4HCN61cuOe +CADBKF/5uUJHwS9pWw3uUbpGUwsLBuhJzCY/OpJlDqC8Y9aToi2Ivl5u3/Q/sKjr +6AZb9lx4q3J2z7tJDrm5MHYwV74elGSXoeoG8nODUqjgklIWAPrt6lQ3WJpO2kug +8RhCdSbWkcXHfX95AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FOIxhqTPkKVqKBZvMWtKewKWDvDBMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0B +AQsFAAOCAQEAqoItII89lOl4TKvg0I1EinxafZLXIheLcdGCxpjRxlZ9QMQUN3yb +y/8uFKBL0otbQgJEoGhxm4h0tp54g28M6TN1U0332dwkjYxUNwvzrMaV5Na55I2Z +1hq4GB3NMXW+PvdtsgVOZbEN+zOyOZ5MvJHEQVkT3YRnf6avsdntltcRzHJ16pJc +Y8rR7yWwPXh1lPaPkxddrCtwayyGxNbNmRybjR48uHRhwu7v2WuAMdChL8H8bp89 +TQLMrMHgSbZfee9hKhO4Zebelf1/cslRSrhkG0ESq6G5MUINj6lMg2g6F0F7Xz2v +ncD/vuRN5P+vT8th/oZ0Q2Gc68Pun0cn/g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID/zCCAuegAwIBAgIRAJYlnmkGRj4ju/2jBQsnXJYwDQYJKoZIhvcNAQELBQAw +gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn +QW1hem9uIFJEUyB1cy1lYXN0LTIgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyMTIzMDQ0NFoYDzIwNjEwNTIyMDAwNDQ0WjCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIHVzLWVhc3QtMiBSb290IENBIFJTQTIwNDggRzExEDAOBgNVBAcMB1Nl +YXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC74V3eigv+pCj5 +nqDBqplY0Jp16pTeNB06IKbzb4MOTvNde6QjsZxrE1xUmprT8LxQqN9tI3aDYEYk +b9v4F99WtQVgCv3Y34tYKX9NwWQgwS1vQwnIR8zOFBYqsAsHEkeJuSqAB12AYUSd +Zv2RVFjiFmYJho2X30IrSLQfS/IE3KV7fCyMMm154+/K1Z2IJlcissydEAwgsUHw +edrE6CxJVkkJ3EvIgG4ugK/suxd8eEMztaQYJwSdN8TdfT59LFuSPl7zmF3fIBdJ +//WexcQmGabaJ7Xnx+6o2HTfkP8Zzzzaq8fvjAcvA7gyFH5EP26G2ZqMG+0y4pTx +SPVTrQEXAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIWWuNEF +sUMOC82XlfJeqazzrkPDMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOC +AQEAgClmxcJaQTGpEZmjElL8G2Zc8lGc+ylGjiNlSIw8X25/bcLRptbDA90nuP+q +zXAMhEf0ccbdpwxG/P5a8JipmHgqQLHfpkvaXx+0CuP++3k+chAJ3Gk5XtY587jX ++MJfrPgjFt7vmMaKmynndf+NaIJAYczjhJj6xjPWmGrjM3MlTa9XesmelMwP3jep +bApIWAvCYVjGndbK9byyMq1nyj0TUzB8oJZQooaR3MMjHTmADuVBylWzkRMxbKPl +4Nlsk4Ef1JvIWBCzsMt+X17nuKfEatRfp3c9tbpGlAE/DSP0W2/Lnayxr4RpE9ds +ICF35uSis/7ZlsftODUe8wtpkQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrjCCAjOgAwIBAgIQS7vMpOTVq2Jw457NdZ2ffjAKBggqhkjOPQQDAzCBljEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMS8wLQYDVQQDDCZBbWF6 +b24gUkRTIGNhLXdlc3QtMSBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTAgFw0yMzA5MTkyMjExNDNaGA8yMTIzMDkxOTIzMTE0M1owgZYxCzAJBgNV +BAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYD +VQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEvMC0GA1UEAwwmQW1hem9uIFJE +UyBjYS13ZXN0LTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1NlYXR0bGUw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAARdgGSs/F2lpWKqS1ZpcmatFED1JurmNbXG +Sqhv1A/geHrKCS15MPwjtnfZiujYKY4fNkCCUseoGDwkC4281nwkokvnfWR1/cXy +LxfACoXNxsI4b+37CezSUBl48/5p1/OjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFFhLokGBuJGwKJhZcYSYKyZIitJtMA4GA1UdDwEB/wQEAwIBhjAKBggq +hkjOPQQDAwNpADBmAjEA8aQQlzJRHbqFsRY4O3u/cN0T8dzjcqnYn4NV1w+jvhzt +QPJLB+ggGyQhoFR6G2UrAjEA0be8OP5MWXD8d01KKbo5Dpy6TwukF5qoJmkFJKS3 +bKfEMvFWxXoV06HNZFWdI80u +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF/zCCA+egAwIBAgIRAPvvd+MCcp8E36lHziv0xhMwDQYJKoZIhvcNAQEMBQAw +gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn +QW1hem9uIFJEUyB1cy1lYXN0LTIgUm9vdCBDQSBSU0E0MDk2IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyMTIzMTEwNloYDzIxMjEwNTIyMDAxMTA2WjCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIHVzLWVhc3QtMiBSb290IENBIFJTQTQwOTYgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDbvwekKIKGcV/s +lDU96a71ZdN2pTYkev1X2e2/ICb765fw/i1jP9MwCzs8/xHBEQBJSxdfO4hPeNx3 +ENi0zbM+TrMKliS1kFVe1trTTEaHYjF8BMK9yTY0VgSpWiGxGwg4tshezIA5lpu8 +sF6XMRxosCEVCxD/44CFqGZTzZaREIvvFPDTXKJ6yOYnuEkhH3OcoOajHN2GEMMQ +ShuyRFDQvYkqOC/Q5icqFbKg7eGwfl4PmimdV7gOVsxSlw2s/0EeeIILXtHx22z3 +8QBhX25Lrq2rMuaGcD3IOMBeBo2d//YuEtd9J+LGXL9AeOXHAwpvInywJKAtXTMq +Wsy3LjhuANFrzMlzjR2YdjkGVzeQVx3dKUzJ2//Qf7IXPSPaEGmcgbxuatxjnvfT +H85oeKr3udKnXm0Kh7CLXeqJB5ITsvxI+Qq2iXtYCc+goHNR01QJwtGDSzuIMj3K +f+YMrqBXZgYBwU2J/kCNTH31nfw96WTbOfNGwLwmVRDgguzFa+QzmQsJW4FTDMwc +7cIjwdElQQVA+Gqa67uWmyDKAnoTkudmgAP+OTBkhnmc6NJuZDcy6f/iWUdl0X0u +/tsfgXXR6ZovnHonM13ANiN7VmEVqFlEMa0VVmc09m+2FYjjlk8F9sC7Rc4wt214 +7u5YvCiCsFZwx44baP5viyRZgkJVpQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBQgCZCsc34nVTRbWsniXBPjnUTQ2DAOBgNVHQ8BAf8EBAMCAYYw +DQYJKoZIhvcNAQEMBQADggIBAAQas3x1G6OpsIvQeMS9BbiHG3+kU9P/ba6Rrg+E +lUz8TmL04Bcd+I+R0IyMBww4NznT+K60cFdk+1iSmT8Q55bpqRekyhcdWda1Qu0r +JiTi7zz+3w2v66akofOnGevDpo/ilXGvCUJiLOBnHIF0izUqzvfczaMZGJT6xzKq +PcEVRyAN1IHHf5KnGzUlVFv9SGy47xJ9I1vTk24JU0LWkSLzMMoxiUudVmHSqJtN +u0h+n/x3Q6XguZi1/C1KOntH56ewRh8n5AF7c+9LJJSRM9wunb0Dzl7BEy21Xe9q +03xRYjf5wn8eDELB8FZPa1PrNKXIOLYM9egdctbKEcpSsse060+tkyBrl507+SJT +04lvJ4tcKjZFqxn+bUkDQvXYj0D3WK+iJ7a8kZJPRvz8BDHfIqancY8Tgw+69SUn +WqIb+HNZqFuRs16WFSzlMksqzXv6wcDSyI7aZOmCGGEcYW9NHk8EuOnOQ+1UMT9C +Qb1GJcipjRzry3M4KN/t5vN3hIetB+/PhmgTO4gKhBETTEyPC3HC1QbdVfRndB6e +U/NF2U/t8U2GvD26TTFLK4pScW7gyw4FQyXWs8g8FS8f+R2yWajhtS9++VDJQKom +fAUISoCH+PlPRJpu/nHd1Zrddeiiis53rBaLbXu2J1Q3VqjWOmtj0HjxJJxWnYmz +Pqj2 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGATCCA+mgAwIBAgIRAI/U4z6+GF8/znpHM8Dq8G0wDQYJKoZIhvcNAQEMBQAw +gZgxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwo +QW1hem9uIFJEUyBhcC1zb3V0aC0yIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UE +BwwHU2VhdHRsZTAgFw0yMjA2MDYyMTQ4MThaGA8yMTIyMDYwNjIyNDgxOFowgZgx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwoQW1h +em9uIFJEUyBhcC1zb3V0aC0yIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UEBwwH +U2VhdHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK5WqMvyq888 +3uuOtEj1FcP6iZhqO5kJurdJF59Otp2WCg+zv6I+QwaAspEWHQsKD405XfFsTGKV +SKTCwoMxwBniuChSmyhlagQGKSnRY9+znOWq0v7hgmJRwp6FqclTbubmr+K6lzPy +hs86mEp68O5TcOTYWUlPZDqfKwfNTbtCl5YDRr8Gxb5buHmkp6gUSgDkRsXiZ5VV +b3GBmXRqbnwo5ZRNAzQeM6ylXCn4jKs310lQGUrFbrJqlyxUdfxzqdlaIRn2X+HY +xRSYbHox3LVNPpJxYSBRvpQVFSy9xbX8d1v6OM8+xluB31cbLBtm08KqPFuqx+cO +I2H5F0CYqYzhyOSKJsiOEJT6/uH4ewryskZzncx9ae62SC+bB5n3aJLmOSTkKLFY +YS5IsmDT2m3iMgzsJNUKVoCx2zihAzgBanFFBsG+Xmoq0aKseZUI6vd2qpd5tUST +/wS1sNk0Ph7teWB2ACgbFE6etnJ6stwjHFZOj/iTYhlnR2zDRU8akunFdGb6CB4/ +hMxGJxaqXSJeGtHm7FpadlUTf+2ESbYcVW+ui/F8sdBJseQdKZf3VdZZMgM0bcaX +NE47cauDTy72WdU9YJX/YXKYMLDE0iFHTnGpfVGsuWGPYhlwZ3dFIO07mWnCRM6X +u5JXRB1oy5n5HRluMsmpSN/R92MeBxKFAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFNtH0F0xfijSLHEyIkRGD9gW6NazMA4GA1UdDwEB/wQEAwIB +hjANBgkqhkiG9w0BAQwFAAOCAgEACo+5jFeY3ygxoDDzL3xpfe5M0U1WxdKk+az4 +/OfjZvkoma7WfChi3IIMtwtKLYC2/seKWA4KjlB3rlTsCVNPnK6D+gAnybcfTKk/ +IRSPk92zagwQkSUWtAk80HpVfWJzpkSU16ejiajhedzOBRtg6BwsbSqLCDXb8hXr +eXWC1S9ZceGc+LcKRHewGWPu31JDhHE9bNcl9BFSAS0lYVZqxIRWxivZ+45j5uQv +wPrC8ggqsdU3K8quV6dblUQzzA8gKbXJpCzXZihkPrYpQHTH0szvXvgebh+CNUAG +rUxm8+yTS0NFI3U+RLbcLFVzSvjMOnEwCX0SPj5XZRYYXs5ajtQCoZhTUkkwpDV8 +RxXk8qGKiXwUxDO8GRvmvM82IOiXz5w2jy/h7b7soyIgdYiUydMq4Ja4ogB/xPZa +gf4y0o+bremO15HFf1MkaU2UxPK5FFVUds05pKvpSIaQWbF5lw4LHHj4ZtVup7zF +CLjPWs4Hs/oUkxLMqQDw0FBwlqa4uot8ItT8uq5BFpz196ZZ+4WXw5PVzfSxZibI +C/nwcj0AS6qharXOs8yPnPFLPSZ7BbmWzFDgo3tpglRqo3LbSPsiZR+sLeivqydr +0w4RK1btRda5Ws88uZMmW7+2aufposMKcbAdrApDEAVzHijbB/nolS5nsnFPHZoA +KDPtFEk= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICtzCCAj2gAwIBAgIQVZ5Y/KqjR4XLou8MCD5pOjAKBggqhkjOPQQDAzCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIGFwLXNvdXRoZWFzdC00IFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIyMDUyNTE2NTgzM1oYDzIxMjIwNTI1MTc1ODMzWjCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIGFwLXNvdXRoZWFzdC00IFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEbo473OmpD5vkckdJajXg +brhmNFyoSa0WCY1njuZC2zMFp3zP6rX4I1r3imrYnJd9pFH/aSiV/r6L5ACE5RPx +4qdg5SQ7JJUaZc3DWsTOiOed7BCZSzM+KTYK/2QzDMApo0IwQDAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTmogc06+1knsej1ltKUOdWFvwgsjAOBgNVHQ8BAf8E +BAMCAYYwCgYIKoZIzj0EAwMDaAAwZQIxAIs7TlLMbGTWNXpGiKf9DxaM07d/iDHe +F/Vv/wyWSTGdobxBL6iArQNVXz0Gr4dvPAIwd0rsoa6R0x5mtvhdRPtM37FYrbHJ +pbV+OMusQqcSLseunLBoCHenvJW0QOCQ8EDY +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGBTCCA+2gAwIBAgIRAO9dVdiLTEGO8kjUFExJmgowDQYJKoZIhvcNAQEMBQAw +gZoxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEzMDEGA1UEAwwq +QW1hem9uIFJEUyBpbC1jZW50cmFsLTEgUm9vdCBDQSBSU0E0MDk2IEcxMRAwDgYD +VQQHDAdTZWF0dGxlMCAXDTIyMTIwMjIwMjYwOFoYDzIxMjIxMjAyMjEyNjA4WjCB +mjELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTMwMQYDVQQDDCpB +bWF6b24gUkRTIGlsLWNlbnRyYWwtMSBSb290IENBIFJTQTQwOTYgRzExEDAOBgNV +BAcMB1NlYXR0bGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDkVHmJ +bUc8CNDGBcgPmXHSHj5dS1PDnnpk3doCu6pahyYXW8tqAOmOqsDuNz48exY7YVy4 +u9I9OPBeTYB9ZUKwxq+1ZNLsr1cwVz5DdOyDREVFOjlU4rvw0eTgzhP5yw/d+Ai/ ++WmPebZG0irwPKN2f60W/KJ45UNtR+30MT8ugfnPuSHWjjV+dqCOCp/mj8nOCckn +k8GoREwjuTFJMKInpQUC0BaVVX6LiIdgtoLY4wdx00EqNBuROoRTAvrked0jvm7J +UI39CSYxhNZJ9F6LdESZXjI4u2apfNQeSoy6WptxFHr+kh2yss1B2KT6lbwGjwWm +l9HODk9kbBNSy2NeewAms36q+p8wSLPavL28IRfK0UaBAiN1hr2a/2RDGCwOJmw6 +5erRC5IIX5kCStyXPEGhVPp18EvMuBd37eLIxjZBBO8AIDf4Ue8QmxSeZH0cT204 +3/Bd6XR6+Up9iMTxkHr1URcL1AR8Zd62lg/lbEfxePNMK9mQGxKP8eTMG5AjtW9G +TatEoRclgE0wZQalXHmKpBNshyYdGqQZhzL1MxCxWzfHNgZkTKIsdzxrjnP7RiBR +jdRH0YhXn6Y906QfLwMCaufwfQ5J8+nj/tu7nG138kSxsu6VUkhnQJhUcUsxuHD/ +NnBx0KGVEldtZiZf7ccgtRVp1lA0OrVtq3ZLMQIDAQABo0IwQDAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBQ2WC3p8rWeE2N0S4Om01KsNLpk/jAOBgNVHQ8BAf8E +BAMCAYYwDQYJKoZIhvcNAQEMBQADggIBAFFEVDt45Obr6Ax9E4RMgsKjj4QjMFB9 +wHev1jL7hezl/ULrHuWxjIusaIZEIcKfn+v2aWtqOq13P3ht7jV5KsV29CmFuCdQ +q3PWiAXVs+hnMskTOmGMDnptqd6/UuSIha8mlOKKAvnmRQJvfX9hIfb/b/mVyKWD +uvTTmcy3cOTJY5ZIWGyzuvmcqA0YNcb7rkJt/iaLq4RX3/ofq4y4w36hefbcvj++ +pXHOmXk3dAej3y6SMBOUcGMyCJcCluRPNYKDTLn+fitcPxPC3JG7fI5bxQ0D6Hpa +qbyGBQu96sfahQyMc+//H8EYlo4b0vPeS5RFFXJS/VBf0AyNT4vVc7H17Q6KjeNp +wEARqsIa7UalHx9MnxrQ/LSTTxiC8qmDkIFuQtw8iQMN0SoL5S0eCZNRD31awgaY +y1PvY8JMN549ugIUjOXnown/OxharLW1evWUraU5rArq3JfeFpPXl4K/u10T5SCL +iJRoxFilGPMFE3hvnmbi5rEy8wRUn7TpLb4I4s/CB/lT2qZTPqvQHwxKCnMm9BKF +NHb4rLL5dCvUi5NJ6fQ/exOoGdOVSfT7jqFeq2TtNunERSz9vpriweliB6iIe1Al +Thj8aEs1GqA764rLVGA+vUe18NhjJm9EemrdIzjSQFy/NdbN/DMaHqEzJogWloAI +izQWYnCS19TJ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICvTCCAkOgAwIBAgIQCIY7E/bFvFN2lK9Kckb0dTAKBggqhkjOPQQDAzCBnjEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTcwNQYDVQQDDC5BbWF6 +b24gUkRTIFByZXZpZXcgdXMtZWFzdC0yIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYD +VQQHDAdTZWF0dGxlMCAXDTIxMDUxODIxMDUxMFoYDzIxMjEwNTE4MjIwNTEwWjCB +njELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTcwNQYDVQQDDC5B +bWF6b24gUkRTIFByZXZpZXcgdXMtZWFzdC0yIFJvb3QgQ0EgRUNDMzg0IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEMI0hzf1JCEOI +Eue4+DmcNnSs2i2UaJxHMrNGGfU7b42a7vwP53F7045ffHPBGP4jb9q02/bStZzd +VHqfcgqkSRI7beBKjD2mfz82hF/wJSITTgCLs+NRpS6zKMFOFHUNo0IwQDAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBS8uF/6hk5mPLH4qaWv9NVZaMmyTjAOBgNV +HQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMDaAAwZQIxAO7Pu9wzLyM0X7Q08uLIL+vL +qaxe3UFuzFTWjM16MLJHbzLf1i9IDFKz+Q4hXCSiJwIwClMBsqT49BPUxVsJnjGr +EbyEk6aOOVfY1p2yQL649zh3M4h8okLnwf+bYIb1YpeU +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIQY+JhwFEQTe36qyRlUlF8ozANBgkqhkiG9w0BAQsFADCB +mDELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChB +bWF6b24gUkRTIGFmLXNvdXRoLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUxOTE5MjQxNloYDzIwNjEwNTE5MjAyNDE2WjCBmDEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChBbWF6 +b24gUkRTIGFmLXNvdXRoLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQHDAdT +ZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnIye77j6ev40 +8wRPyN2OdKFSUfI9jB20Or2RLO+RDoL43+USXdrze0Wv4HMRLqaen9BcmCfaKMp0 +E4SFo47bXK/O17r6G8eyq1sqnHE+v288mWtYH9lAlSamNFRF6YwA7zncmE/iKL8J +0vePHMHP/B6svw8LULZCk+nZk3tgxQn2+r0B4FOz+RmpkoVddfqqUPMbKUxhM2wf +fO7F6bJaUXDNMBPhCn/3ayKCjYr49ErmnpYV2ZVs1i34S+LFq39J7kyv6zAgbHv9 ++/MtRMoRB1CjpqW0jIOZkHBdYcd1o9p1zFn591Do1wPkmMsWdjIYj+6e7UXcHvOB +2+ScIRAcnwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQGtq2W +YSyMMxpdQ3IZvcGE+nyZqTAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD +ggEBAEgoP3ixJsKSD5FN8dQ01RNHERl/IFbA7TRXfwC+L1yFocKnQh4Mp/msPRSV ++OeHIvemPW/wtZDJzLTOFJ6eTolGekHK1GRTQ6ZqsWiU2fmiOP8ks4oSpI+tQ9Lw +VrfZqTiEcS5wEIqyfUAZZfKDo7W1xp+dQWzfczSBuZJZwI5iaha7+ILM0r8Ckden +TVTapc5pLSoO15v0ziRuQ2bT3V3nwu/U0MRK44z+VWOJdSiKxdnOYDs8hFNnKhfe +klbTZF7kW7WbiNYB43OaAQBJ6BALZsIskEaqfeZT8FD71uN928TcEQyBDXdZpRN+ +iGQZDGhht0r0URGMDSs9waJtTfA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF/jCCA+agAwIBAgIQXY/dmS+72lZPranO2JM9jjANBgkqhkiG9w0BAQwFADCB +lzELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdB +bWF6b24gUkRTIGFwLWVhc3QtMSBSb290IENBIFJTQTQwOTYgRzExEDAOBgNVBAcM +B1NlYXR0bGUwIBcNMjEwNTI1MjEzNDUxWhgPMjEyMTA1MjUyMjM0NTFaMIGXMQsw +CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET +MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpv +biBSRFMgYXAtZWFzdC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMyW9kBJjD/hx8e8 +b5E1sF42bp8TXsz1htSYE3Tl3T1Aq379DfEhB+xa/ASDZxt7/vwa81BkNo4M6HYq +okYIXeE7cu5SnSgjWXqcERhgPevtAwgmhdE3yREe8oz2DyOi2qKKZqah+1gpPaIQ +fK0uAqoeQlyHosye3KZZKkDHBatjBsQ5kf8lhuf7wVulEZVRHY2bP2X7N98PfbpL +QdH7mWXzDtJJ0LiwFwds47BrkgK1pkHx2p1mTo+HMkfX0P6Fq1atkVC2RHHtbB/X +iYyH7paaHBzviFrhr679zNqwXIOKlbf74w3mS11P76rFn9rS1BAH2Qm6eY5S/Fxe +HEKXm4kjPN63Zy0p3yE5EjPt54yPkvumOnT+RqDGJ2HCI9k8Ehcbve0ogfdRKNqQ +VHWYTy8V33ndQRHZlx/CuU1yN61TH4WSoMly1+q1ihTX9sApmlQ14B2pJi/9DnKW +cwECrPy1jAowC2UJ45RtC8UC05CbP9yrIy/7Noj8gQDiDOepm+6w1g6aNlWoiuQS +kyI6nzz1983GcnOHya73ga7otXo0Qfg9jPghlYiMomrgshlSLDHZG0Ib/3hb8cnR +1OcN9FpzNmVK2Ll1SmTMLrIhuCkyNYX9O/bOknbcf706XeESxGduSkHEjIw/k1+2 +Atteoq5dT6cwjnJ9hyhiueVlVkiDAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8w +HQYDVR0OBBYEFLUI+DD7RJs+0nRnjcwIVWzzYSsFMA4GA1UdDwEB/wQEAwIBhjAN +BgkqhkiG9w0BAQwFAAOCAgEAb1mcCHv4qMQetLGTBH9IxsB2YUUhr5dda0D2BcHr +UtDbfd0VQs4tux6h/6iKwHPx0Ew8fuuYj99WknG0ffgJfNc5/fMspxR/pc1jpdyU +5zMQ+B9wi0lOZPO9uH7/pr+d2odcNEy8zAwqdv/ihsTwLmGP54is9fVbsgzNW1cm +HKAVL2t/Ope+3QnRiRilKCN1lzhav4HHdLlN401TcWRWKbEuxF/FgxSO2Hmx86pj +e726lweCTMmnq/cTsPOVY0WMjs0or3eHDVlyLgVeV5ldyN+ptg3Oit60T05SRa58 +AJPTaVKIcGQ/gKkKZConpu7GDofT67P/ox0YNY57LRbhsx9r5UY4ROgz7WMQ1yoS +Y+19xizm+mBm2PyjMUbfwZUyCxsdKMwVdOq5/UmTmdms+TR8+m1uBHPOTQ2vKR0s +Pd/THSzPuu+d3dbzRyDSLQbHFFneG760CUlD/ZmzFlQjJ89/HmAmz8IyENq+Sjhx +Jgzy+FjVZb8aRUoYLlnffpUpej1n87Ynlr1GrvC4GsRpNpOHlwuf6WD4W0qUTsC/ +C9JO+fBzUj/aWlJzNcLEW6pte1SB+EdkR2sZvWH+F88TxemeDrV0jKJw5R89CDf8 +ZQNfkxJYjhns+YeV0moYjqQdc7tq4i04uggEQEtVzEhRLU5PE83nlh/K2NZZm8Kj +dIA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID/zCCAuegAwIBAgIRAPVSMfFitmM5PhmbaOFoGfUwDQYJKoZIhvcNAQELBQAw +gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn +QW1hem9uIFJEUyB1cy1lYXN0LTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyNTIyMzQ1N1oYDzIwNjEwNTI1MjMzNDU3WjCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIHVzLWVhc3QtMSBSb290IENBIFJTQTIwNDggRzExEDAOBgNVBAcMB1Nl +YXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDu9H7TBeGoDzMr +dxN6H8COntJX4IR6dbyhnj5qMD4xl/IWvp50lt0VpmMd+z2PNZzx8RazeGC5IniV +5nrLg0AKWRQ2A/lGGXbUrGXCSe09brMQCxWBSIYe1WZZ1iU1IJ/6Bp4D2YEHpXrW +bPkOq5x3YPcsoitgm1Xh8ygz6vb7PsvJvPbvRMnkDg5IqEThapPjmKb8ZJWyEFEE +QRrkCIRueB1EqQtJw0fvP4PKDlCJAKBEs/y049FoOqYpT3pRy0WKqPhWve+hScMd +6obq8kxTFy1IHACjHc51nrGII5Bt76/MpTWhnJIJrCnq1/Uc3Qs8IVeb+sLaFC8K +DI69Sw6bAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFE7PCopt +lyOgtXX0Y1lObBUxuKaCMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOC +AQEAFj+bX8gLmMNefr5jRJfHjrL3iuZCjf7YEZgn89pS4z8408mjj9z6Q5D1H7yS +jNETVV8QaJip1qyhh5gRzRaArgGAYvi2/r0zPsy+Tgf7v1KGL5Lh8NT8iCEGGXwF +g3Ir+Nl3e+9XUp0eyyzBIjHtjLBm6yy8rGk9p6OtFDQnKF5OxwbAgip42CD75r/q +p421maEDDvvRFR4D+99JZxgAYDBGqRRceUoe16qDzbMvlz0A9paCZFclxeftAxv6 +QlR5rItMz/XdzpBJUpYhdzM0gCzAzdQuVO5tjJxmXhkSMcDP+8Q+Uv6FA9k2VpUV +E/O5jgpqUJJ2Hc/5rs9VkAPXeA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrzCCAjWgAwIBAgIQW0yuFCle3uj4vWiGU0SaGzAKBggqhkjOPQQDAzCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIGFmLXNvdXRoLTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjEwNTE5MTkzNTE2WhgPMjEyMTA1MTkyMDM1MTZaMIGXMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpvbiBS +RFMgYWYtc291dGgtMSBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwHU2VhdHRs +ZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDPiKNZSaXs3Un/J/v+LTsFDANHpi7en +oL2qh0u0DoqNzEBTbBjvO23bLN3k599zh6CY3HKW0r2k1yaIdbWqt4upMCRCcUFi +I4iedAmubgzh56wJdoMZztjXZRwDthTkJKNCMEAwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUWbYkcrvVSnAWPR5PJhIzppcAnZIwDgYDVR0PAQH/BAQDAgGGMAoG +CCqGSM49BAMDA2gAMGUCMCESGqpat93CjrSEjE7z+Hbvz0psZTHwqaxuiH64GKUm +mYynIiwpKHyBrzjKBmeDoQIxANGrjIo6/b8Jl6sdIZQI18V0pAyLfLiZjlHVOnhM +MOTVgr82ZuPoEHTX78MxeMnYlw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgIRAIbsx8XOl0sgTNiCN4O+18QwDQYJKoZIhvcNAQELBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1ub3J0aGVhc3QtMSBSb290IENBIFJTQTIwNDggRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjEwNTI1MjE1NDU4WhgPMjA2MTA1MjUyMjU0NTha +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgYXAtbm9ydGhlYXN0LTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +tROxwXWCgn5R9gI/2Ivjzaxc0g95ysBjoJsnhPdJEHQb7w3y2kWrVWU3Y9fOitgb +CEsnEC3PrhRnzNVW0fPsK6kbvOeCmjvY30rdbxbc8h+bjXfGmIOgAkmoULEr6Hc7 +G1Q/+tvv4lEwIs7bEaf+abSZxRJbZ0MBxhbHn7UHHDiMZYvzK+SV1MGCxx7JVhrm +xWu3GC1zZCsGDhB9YqY9eR6PmjbqA5wy8vqbC57dZZa1QVtWIQn3JaRXn+faIzHx +nLMN5CEWihsdmHBXhnRboXprE/OS4MFv1UrQF/XM/h5RBeCywpHePpC+Oe1T3LNC +iP8KzRFrjC1MX/WXJnmOVQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBS33XbXAUMs1znyZo4B0+B3D68WFTAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI +hvcNAQELBQADggEBADuadd2EmlpueY2VlrIIPC30QkoA1EOSoCmZgN6124apkoY1 +HiV4r+QNPljN4WP8gmcARnNkS7ZeR4fvWi8xPh5AxQCpiaBMw4gcbTMCuKDV68Pw +P2dZCTMspvR3CDfM35oXCufdtFnxyU6PAyINUqF/wyTHguO3owRFPz64+sk3r2pT +WHmJjG9E7V+KOh0s6REgD17Gqn6C5ijLchSrPUHB0wOIkeLJZndHxN/76h7+zhMt +fFeNxPWHY2MfpcaLjz4UREzZPSB2U9k+y3pW1omCIcl6MQU9itGx/LpQE+H3ZeX2 +M2bdYd5L+ow+bdbGtsVKOuN+R9Dm17YpswF+vyQ= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGATCCA+mgAwIBAgIRAKlQ+3JX9yHXyjP/Ja6kZhkwDQYJKoZIhvcNAQEMBQAw +gZgxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwo +QW1hem9uIFJEUyBhcC1zb3V0aC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UE +BwwHU2VhdHRsZTAgFw0yMTA1MTkxNzQ1MjBaGA8yMTIxMDUxOTE4NDUyMFowgZgx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwoQW1h +em9uIFJEUyBhcC1zb3V0aC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UEBwwH +U2VhdHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKtahBrpUjQ6 +H2mni05BAKU6Z5USPZeSKmBBJN3YgD17rJ93ikJxSgzJ+CupGy5rvYQ0xznJyiV0 +91QeQN4P+G2MjGQR0RGeUuZcfcZitJro7iAg3UBvw8WIGkcDUg+MGVpRv/B7ry88 +7E4OxKb8CPNoa+a9j6ABjOaaxaI22Bb7j3OJ+JyMICs6CU2bgkJaj3VUV9FCNUOc +h9PxD4jzT9yyGYm/sK9BAT1WOTPG8XQUkpcFqy/IerZDfiQkf1koiSd4s5VhBkUn +aQHOdri/stldT7a+HJFVyz2AXDGPDj+UBMOuLq0K6GAT6ThpkXCb2RIf4mdTy7ox +N5BaJ+ih+Ro3ZwPkok60egnt/RN98jgbm+WstgjJWuLqSNInnMUgkuqjyBWwePqX +Kib+wdpyx/LOzhKPEFpeMIvHQ3A0sjlulIjnh+j+itezD+dp0UNxMERlW4Bn/IlS +sYQVNfYutWkRPRLErXOZXtlxxkI98JWQtLjvGzQr+jywxTiw644FSLWdhKa6DtfU +2JWBHqQPJicMElfZpmfaHZjtXuCZNdZQXWg7onZYohe281ZrdFPOqC4rUq7gYamL +T+ZB+2P+YCPOLJ60bj/XSvcB7mesAdg8P0DNddPhHUFWx2dFqOs1HxIVB4FZVA9U +Ppbv4a484yxjTgG7zFZNqXHKTqze6rBBAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFCEAqjighncv/UnWzBjqu1Ka2Yb4MA4GA1UdDwEB/wQEAwIB +hjANBgkqhkiG9w0BAQwFAAOCAgEAYyvumblckIXlohzi3QiShkZhqFzZultbFIu9 +GhA5CDar1IFMhJ9vJpO9nUK/camKs1VQRs8ZsBbXa0GFUM2p8y2cgUfLwFULAiC/ +sWETyW5lcX/xc4Pyf6dONhqFJt/ovVBxNZtcmMEWv/1D6Tf0nLeEb0P2i/pnSRR4 +Oq99LVFjossXtyvtaq06OSiUUZ1zLPvV6AQINg8dWeBOWRcQYhYcEcC2wQ06KShZ +0ahuu7ar5Gym3vuLK6nH+eQrkUievVomN/LpASrYhK32joQ5ypIJej3sICIgJUEP +UoeswJ+Z16f3ECoL1OSnq4A0riiLj1ZGmVHNhM6m/gotKaHNMxsK9zsbqmuU6IT/ +P6cR0S+vdigQG8ZNFf5vEyVNXhl8KcaJn6lMD/gMB2rY0qpaeTg4gPfU5wcg8S4Y +C9V//tw3hv0f2n+8kGNmqZrylOQDQWSSo8j8M2SRSXiwOHDoTASd1fyBEIqBAwzn +LvXVg8wQd1WlmM3b0Vrsbzltyh6y4SuKSkmgufYYvC07NknQO5vqvZcNoYbLNea3 +76NkFaMHUekSbwVejZgG5HGwbaYBgNdJEdpbWlA3X4yGRVxknQSUyt4dZRnw/HrX +k8x6/wvtw7wht0/DOqz1li7baSsMazqxx+jDdSr1h9xML416Q4loFCLgqQhil8Jq +Em4Hy3A= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEBDCCAuygAwIBAgIQFn6AJ+uxaPDpNVx7174CpjANBgkqhkiG9w0BAQsFADCB +mjELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTMwMQYDVQQDDCpB +bWF6b24gUkRTIGlsLWNlbnRyYWwtMSBSb290IENBIFJTQTIwNDggRzExEDAOBgNV +BAcMB1NlYXR0bGUwIBcNMjIxMjAyMjAxNDA4WhgPMjA2MjEyMDIyMTE0MDhaMIGa +MQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5j +LjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMzAxBgNVBAMMKkFt +YXpvbiBSRFMgaWwtY2VudHJhbC0xIFJvb3QgQ0EgUlNBMjA0OCBHMTEQMA4GA1UE +BwwHU2VhdHRsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL2xGTSJ +fXorki/dkkTqdLyv4U1neeFYEyUCPN/HJ7ZloNwhj8RBrHYhZ4qtvUAvN+rs8fUm +L0wmaL69ye61S+CSfDzNwBDGwOzUm/cc1NEJOHCm8XA0unBNBvpJTjsFk2LQ+rz8 +oU0lVV4mjnfGektrTDeADonO1adJvUTYmF6v1wMnykSkp8AnW9EG/6nwcAJuAJ7d +BfaLThm6lfxPdsBNG81DLKi2me2TLQ4yl+vgRKJi2fJWwA77NaDqQuD5upRIcQwt +5noJt2kFFmeiro98ZMMRaDTHAHhJfWkwkw5f2QNIww7T4r85IwbQCgJVRo4m4ZTC +W/1eiEccU2407mECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +DNhVvGHzKXv0Yh6asK0apP9jJlUwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +CwUAA4IBAQCoEVTUY/rF9Zrlpb1Y1hptEguw0i2pCLakcmv3YNj6thsubbGeGx8Z +RjUA/gPKirpoae2HU1y64WEu7akwr6pdTRtXXjbe9NReT6OW/0xAwceSXCOiStqS +cMsWWTGg6BA3uHqad5clqITjDZr1baQ8X8en4SXRBxXyhJXbOkB60HOQeFR9CNeh +pJdrWLeNYXwU0Z59juqdVMGwvDAYdugWUhW2rhafVUXszfRA5c8Izc+E31kq90aY +LmxFXUHUfG0eQOmxmg+Z/nG7yLUdHIFA3id8MRh22hye3KvRdQ7ZVGFni0hG2vQQ +Q01AvD/rhzyjg0czzJKLK9U/RttwdMaV +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGBTCCA+2gAwIBAgIRAJfKe4Zh4aWNt3bv6ZjQwogwDQYJKoZIhvcNAQEMBQAw +gZoxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEzMDEGA1UEAwwq +QW1hem9uIFJEUyBjYS1jZW50cmFsLTEgUm9vdCBDQSBSU0E0MDk2IEcxMRAwDgYD +VQQHDAdTZWF0dGxlMCAXDTIxMDUyMTIyMDg1M1oYDzIxMjEwNTIxMjMwODUzWjCB +mjELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTMwMQYDVQQDDCpB +bWF6b24gUkRTIGNhLWNlbnRyYWwtMSBSb290IENBIFJTQTQwOTYgRzExEDAOBgNV +BAcMB1NlYXR0bGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCpgUH6 +Crzd8cOw9prAh2rkQqAOx2vtuI7xX4tmBG4I/um28eBjyVmgwQ1fpq0Zg2nCKS54 +Nn0pCmT7f3h6Bvopxn0J45AzXEtajFqXf92NQ3iPth95GVfAJSD7gk2LWMhpmID9 +JGQyoGuDPg+hYyr292X6d0madzEktVVGO4mKTF989qEg+tY8+oN0U2fRTrqa2tZp +iYsmg350ynNopvntsJAfpCO/srwpsqHHLNFZ9jvhTU8uW90wgaKO9i31j/mHggCE ++CAOaJCM3g+L8DPl/2QKsb6UkBgaaIwKyRgKSj1IlgrK+OdCBCOgM9jjId4Tqo2j +ZIrrPBGl6fbn1+etZX+2/tf6tegz+yV0HHQRAcKCpaH8AXF44bny9andslBoNjGx +H6R/3ib4FhPrnBMElzZ5i4+eM/cuPC2huZMBXb/jKgRC/QN1Wm3/nah5FWq+yn+N +tiAF10Ga0BYzVhHDEwZzN7gn38bcY5yi/CjDUNpY0OzEe2+dpaBKPlXTaFfn9Nba +CBmXPRF0lLGGtPeTAgjcju+NEcVa82Ht1pqxyu2sDtbu3J5bxp4RKtj+ShwN8nut +Tkf5Ea9rSmHEY13fzgibZlQhXaiFSKA2ASUwgJP19Putm0XKlBCNSGCoECemewxL ++7Y8FszS4Uu4eaIwvXVqUEE2yf+4ex0hqQ1acQIDAQABo0IwQDAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBSeUnXIRxNbYsZLtKomIz4Y1nOZEzAOBgNVHQ8BAf8E +BAMCAYYwDQYJKoZIhvcNAQEMBQADggIBAIpRvxVS0dzoosBh/qw65ghPUGSbP2D4 +dm6oYCv5g/zJr4fR7NzEbHOXX5aOQnHbQL4M/7veuOCLNPOW1uXwywMg6gY+dbKe +YtPVA1as8G9sUyadeXyGh2uXGsziMFXyaESwiAXZyiYyKChS3+g26/7jwECFo5vC +XGhWpIO7Hp35Yglp8AnwnEAo/PnuXgyt2nvyTSrxlEYa0jus6GZEZd77pa82U1JH +qFhIgmKPWWdvELA3+ra1nKnvpWM/xX0pnMznMej5B3RT3Y+k61+kWghJE81Ix78T ++tG4jSotgbaL53BhtQWBD1yzbbilqsGE1/DXPXzHVf9yD73fwh2tGWSaVInKYinr +a4tcrB3KDN/PFq0/w5/21lpZjVFyu/eiPj6DmWDuHW73XnRwZpHo/2OFkei5R7cT +rn/YdDD6c1dYtSw5YNnS6hdCQ3sOiB/xbPRN9VWJa6se79uZ9NLz6RMOr73DNnb2 +bhIR9Gf7XAA5lYKqQk+A+stoKbIT0F65RnkxrXi/6vSiXfCh/bV6B41cf7MY/6YW +ehserSdjhQamv35rTFdM+foJwUKz1QN9n9KZhPxeRmwqPitAV79PloksOnX25ElN +SlyxdndIoA1wia1HRd26EFm2pqfZ2vtD2EjU3wD42CXX4H8fKVDna30nNFSYF0yn +jGKc3k6UNxpg +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF/jCCA+agAwIBAgIQaRHaEqqacXN20e8zZJtmDDANBgkqhkiG9w0BAQwFADCB +lzELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdB +bWF6b24gUkRTIHVzLWVhc3QtMSBSb290IENBIFJTQTQwOTYgRzExEDAOBgNVBAcM +B1NlYXR0bGUwIBcNMjEwNTI1MjIzODM1WhgPMjEyMTA1MjUyMzM4MzVaMIGXMQsw +CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET +MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpv +biBSRFMgdXMtZWFzdC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAInfBCaHuvj6Rb5c +L5Wmn1jv2PHtEGMHm+7Z8dYosdwouG8VG2A+BCYCZfij9lIGszrTXkY4O7vnXgru +JUNdxh0Q3M83p4X+bg+gODUs3jf+Z3Oeq7nTOk/2UYvQLcxP4FEXILxDInbQFcIx +yen1ESHggGrjEodgn6nbKQNRfIhjhW+TKYaewfsVWH7EF2pfj+cjbJ6njjgZ0/M9 +VZifJFBgat6XUTOf3jwHwkCBh7T6rDpgy19A61laImJCQhdTnHKvzTpxcxiLRh69 +ZObypR7W04OAUmFS88V7IotlPmCL8xf7kwxG+gQfvx31+A9IDMsiTqJ1Cc4fYEKg +bL+Vo+2Ii4W2esCTGVYmHm73drznfeKwL+kmIC/Bq+DrZ+veTqKFYwSkpHRyJCEe +U4Zym6POqQ/4LBSKwDUhWLJIlq99bjKX+hNTJykB+Lbcx0ScOP4IAZQoxmDxGWxN +S+lQj+Cx2pwU3S/7+OxlRndZAX/FKgk7xSMkg88HykUZaZ/ozIiqJqSnGpgXCtED +oQ4OJw5ozAr+/wudOawaMwUWQl5asD8fuy/hl5S1nv9XxIc842QJOtJFxhyeMIXt +LVECVw/dPekhMjS3Zo3wwRgYbnKG7YXXT5WMxJEnHu8+cYpMiRClzq2BEP6/MtI2 +AZQQUFu2yFjRGL2OZA6IYjxnXYiRAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8w +HQYDVR0OBBYEFADCcQCPX2HmkqQcmuHfiQ2jjqnrMA4GA1UdDwEB/wQEAwIBhjAN +BgkqhkiG9w0BAQwFAAOCAgEASXkGQ2eUmudIKPeOIF7RBryCoPmMOsqP0+1qxF8l +pGkwmrgNDGpmd9s0ArfIVBTc1jmpgB3oiRW9c6n2OmwBKL4UPuQ8O3KwSP0iD2sZ +KMXoMEyphCEzW1I2GRvYDugL3Z9MWrnHkoaoH2l8YyTYvszTvdgxBPpM2x4pSkp+ +76d4/eRpJ5mVuQ93nC+YG0wXCxSq63hX4kyZgPxgCdAA+qgFfKIGyNqUIqWgeyTP +n5OgKaboYk2141Rf2hGMD3/hsGm0rrJh7g3C0ZirPws3eeJfulvAOIy2IZzqHUSY +jkFzraz6LEH3IlArT3jUPvWKqvh2lJWnnp56aqxBR7qHH5voD49UpJWY1K0BjGnS +OHcurpp0Yt/BIs4VZeWdCZwI7JaSeDcPMaMDBvND3Ia5Fga0thgYQTG6dE+N5fgF +z+hRaujXO2nb0LmddVyvE8prYlWRMuYFv+Co8hcMdJ0lEZlfVNu0jbm9/GmwAZ+l +9umeYO9yz/uC7edC8XJBglMAKUmVK9wNtOckUWAcCfnPWYLbYa/PqtXBYcxrso5j +iaS/A7iEW51uteHBGrViCy1afGG+hiUWwFlesli+Rq4dNstX3h6h2baWABaAxEVJ +y1RnTQSz6mROT1VmZSgSVO37rgIyY0Hf0872ogcTS+FfvXgBxCxsNWEbiQ/XXva4 +0Ws= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICtDCCAjqgAwIBAgIRAMyaTlVLN0ndGp4ffwKAfoMwCgYIKoZIzj0EAwMwgZkx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEyMDAGA1UEAwwpQW1h +em9uIFJEUyBtZS1jZW50cmFsLTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcM +B1NlYXR0bGUwIBcNMjIwNTA3MDA0NDM3WhgPMjEyMjA1MDcwMTQ0MzdaMIGZMQsw +CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET +MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMjAwBgNVBAMMKUFtYXpv +biBSRFMgbWUtY2VudHJhbC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQHDAdT +ZWF0dGxlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE19nCV1nsI6CohSor13+B25cr +zg+IHdi9Y3L7ziQnHWI6yjBazvnKD+oC71aRRlR8b5YXsYGUQxWzPLHN7EGPcSGv +bzA9SLG1KQYCJaQ0m9Eg/iGrwKWOgylbhVw0bCxoo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS4KsknsJXM9+QPEkBdZxUPaLr11zAOBgNVHQ8BAf8EBAMC +AYYwCgYIKoZIzj0EAwMDaAAwZQIxAJaRgrYIEfXQMZQQDxMTYS0azpyWSseQooXo +L3nYq4OHGBgYyQ9gVjvRYWU85PXbfgIwdi82DtANQFkCu+j+BU0JBY/uRKPEeYzo +JG92igKIcXPqCoxIJ7lJbbzmuf73gQu5 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGATCCA+mgAwIBAgIRAJwCobx0Os8F7ihbJngxrR8wDQYJKoZIhvcNAQEMBQAw +gZgxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwo +QW1hem9uIFJEUyBtZS1zb3V0aC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UE +BwwHU2VhdHRsZTAgFw0yMTA1MjAxNzE1MzNaGA8yMTIxMDUyMDE4MTUzM1owgZgx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwoQW1h +em9uIFJEUyBtZS1zb3V0aC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UEBwwH +U2VhdHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANukKwlm+ZaI +Y5MkWGbEVLApEyLmlrHLEg8PfiiEa9ts7jssQcin3bzEPdTqGr5jo91ONoZ3ccWq +xJgg1W3bLu5CAO2CqIOXTXHRyCO/u0Ch1FGgWB8xETPSi3UHt/Vn1ltdO6DYdbDU +mYgwzYrvLBdRCwxsb9o+BuYQHVFzUYonqk/y9ujz3gotzFq7r55UwDTA1ita3vb4 +eDKjIb4b1M4Wr81M23WHonpje+9qkkrAkdQcHrkgvSCV046xsq/6NctzwCUUNsgF +7Q1a8ut5qJEYpz5ta8vI1rqFqAMBqCbFjRYlmAoTTpFPOmzAVxV+YoqTrW5A16su +/2SXlMYfJ/n/ad/QfBNPPAAQMpyOr2RCL/YiL/PFZPs7NxYjnZHNWxMLSPgFyI+/ +t2klnn5jR76KJK2qimmaXedB90EtFsMRUU1e4NxH9gDuyrihKPJ3aVnZ35mSipvR +/1KB8t8gtFXp/VQaz2sg8+uxPMKB81O37fL4zz6Mg5K8+aq3ejBiyHucpFGnsnVB +3kQWeD36ONkybngmgWoyPceuSWm1hQ0Z7VRAQX+KlxxSaHmSaIk1XxZu9h9riQHx +fMuev6KXjRn/CjCoUTn+7eFrt0dT5GryQEIZP+nA0oq0LKxogigHNZlwAT4flrqb +JUfZJrqgoce5HjZSXl10APbtPjJi0fW9AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFEfV+LztI29OVDRm0tqClP3NrmEWMA4GA1UdDwEB/wQEAwIB +hjANBgkqhkiG9w0BAQwFAAOCAgEAvSNe+0wuk53KhWlRlRf2x/97H2Q76X3anzF0 +5fOSVm022ldALzXMzqOfdnoKIhAu2oVKiHHKs7mMas+T6TL+Mkphx0CYEVxFE3PG +061q3CqJU+wMm9W9xsB79oB2XG47r1fIEywZZ3GaRsatAbjcNOT8uBaATPQAfJFN +zjFe4XyN+rA4cFrYNvfHTeu5ftrYmvks7JlRaJgEGWsz+qXux7uvaEEVPqEumd2H +uYeaRNOZ2V23R009X5lbgBFx9tq5VDTnKhQiTQ2SeT0rc1W3Dz5ik6SbQQNP3nSR +0Ywy7r/sZ3fcDyfFiqnrVY4Ympfvb4YW2PZ6OsQJbzH6xjdnTG2HtzEU30ngxdp1 +WUEF4zt6rjJCp7QBUqXgdlHvJqYu6949qtWjEPiFN9uSsRV2i1YDjJqN52dLjAPn +AipJKo8x1PHTwUzuITqnB9BdP+5TlTl8biJfkEf/+08eWDTLlDHr2VrZLOLompTh +bS5OrhDmqA2Q+O+EWrTIhMflwwlCpR9QYM/Xwvlbad9H0FUHbJsCVNaru3wGOgWo +tt3dNSK9Lqnv/Ej9K9v6CRr36in4ylJKivhJ5B9E7ABHg7EpBJ1xi7O5eNDkNoJG ++pFyphJq3AkBR2U4ni2tUaTAtSW2tks7IaiDV+UMtqZyGabT5ISQfWLLtLHSWn2F +Tspdjbg= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgIRAJZFh4s9aZGzKaTMLrSb4acwDQYJKoZIhvcNAQELBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBCZXRhIHVzLWVhc3QtMSBSb290IENBIFJTQTIwNDggRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjEwNTE4MjEyODQxWhgPMjA2MTA1MTgyMjI4NDFa +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgQmV0YSB1cy1lYXN0LTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +17i2yoU6diep+WrqxIn2CrDEO2NdJVwWTSckx4WMZlLpkQDoymSmkNHjq9ADIApD +A31Cx+843apL7wub8QkFZD0Tk7/ThdHWJOzcAM3ov98QBPQfOC1W5zYIIRP2F+vQ +TRETHQnLcW3rLv0NMk5oQvIKpJoC9ett6aeVrzu+4cU4DZVWYlJUoC/ljWzCluau +8blfW0Vwin6OB7s0HCG5/wijQWJBU5SrP/KAIPeQi1GqG5efbqAXDr/ple0Ipwyo +Xjjl73LenGUgqpANlC9EAT4i7FkJcllLPeK3NcOHjuUG0AccLv1lGsHAxZLgjk/x +z9ZcnVV9UFWZiyJTKxeKPwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBRWyMuZUo4gxCR3Luf9/bd2AqZ7CjAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI +hvcNAQELBQADggEBAIqN2DlIKlvDFPO0QUZQVFbsi/tLdYM98/vvzBpttlTGVMyD +gJuQeHVz+MnhGIwoCGOlGU3OOUoIlLAut0+WG74qYczn43oA2gbMd7HoD7oL/IGg +njorBwJVcuuLv2G//SqM3nxGcLRtkRnQ+lvqPxMz9+0fKFUn6QcIDuF0QSfthLs2 +WSiGEPKO9c9RSXdRQ4pXA7c3hXng8P4A2ZmdciPne5Nu4I4qLDGZYRrRLRkNTrOi +TyS6r2HNGUfgF7eOSeKt3NWL+mNChcYj71/Vycf5edeczpUgfnWy9WbPrK1svKyl +aAs2xg+X6O8qB+Mnj2dNBzm+lZIS3sIlm+nO9sg= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrjCCAjSgAwIBAgIRAPAlEk8VJPmEzVRRaWvTh2AwCgYIKoZIzj0EAwMwgZYx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEvMC0GA1UEAwwmQW1h +em9uIFJEUyB1cy1lYXN0LTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjEwNTI1MjI0MTU1WhgPMjEyMTA1MjUyMzQxNTVaMIGWMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExLzAtBgNVBAMMJkFtYXpvbiBS +RFMgdXMtZWFzdC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQHDAdTZWF0dGxl +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEx5xjrup8II4HOJw15NTnS3H5yMrQGlbj +EDA5MMGnE9DmHp5dACIxmPXPMe/99nO7wNdl7G71OYPCgEvWm0FhdvVUeTb3LVnV +BnaXt32Ek7/oxGk1T+Df03C+W0vmuJ+wo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0G +A1UdDgQWBBTGXmqBWN/1tkSea4pNw0oHrjk2UDAOBgNVHQ8BAf8EBAMCAYYwCgYI +KoZIzj0EAwMDaAAwZQIxAIqqZWCSrIkZ7zsv/FygtAusW6yvlL935YAWYPVXU30m +jkMFLM+/RJ9GMvnO8jHfCgIwB+whlkcItzE9CRQ6CsMo/d5cEHDUu/QW6jSIh9BR +OGh9pTYPVkUbBiKPA7lVVhre +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF/zCCA+egAwIBAgIRAJGY9kZITwfSRaAS/bSBOw8wDQYJKoZIhvcNAQEMBQAw +gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn +QW1hem9uIFJEUyBzYS1lYXN0LTEgUm9vdCBDQSBSU0E0MDk2IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUxOTE4MTEyMFoYDzIxMjEwNTE5MTkxMTIwWjCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIHNhLWVhc3QtMSBSb290IENBIFJTQTQwOTYgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDe2vlDp6Eo4WQi +Wi32YJOgdXHhxTFrLjB9SRy22DYoMaWfginJIwJcSR8yse8ZDQuoNhERB9LRggAE +eng23mhrfvtL1yQkMlZfBu4vG1nOb22XiPFzk7X2wqz/WigdYNBCqa1kK3jrLqPx +YUy7jk2oZle4GLVRTNGuMfcid6S2hs3UCdXfkJuM2z2wc3WUlvHoVNk37v2/jzR/ +hSCHZv5YHAtzL/kLb/e64QkqxKll5QmKhyI6d7vt6Lr1C0zb+DmwxUoJhseAS0hI +dRk5DklMb4Aqpj6KN0ss0HAYqYERGRIQM7KKA4+hxDMUkJmt8KqWKZkAlCZgflzl +m8NZ31o2cvBzf6g+VFHx+6iVrSkohVQydkCxx7NJ743iPKsh8BytSM4qU7xx4OnD +H2yNXcypu+D5bZnVZr4Pywq0w0WqbTM2bpYthG9IC4JeVUvZ2mDc01lqOlbMeyfT +og5BRPLDXdZK8lapo7se2teh64cIfXtCmM2lDSwm1wnH2iSK+AWZVIM3iE45WSGc +vZ+drHfVgjJJ5u1YrMCWNL5C2utFbyF9Obw9ZAwm61MSbPQL9JwznhNlCh7F2ANW +ZHWQPNcOAJqzE4uVcJB1ZeVl28ORYY1668lx+s9yYeMXk3QQdj4xmdnvoBFggqRB +ZR6Z0D7ZohADXe024RzEo1TukrQgKQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBT7Vs4Y5uG/9aXnYGNMEs6ycPUT3jAOBgNVHQ8BAf8EBAMCAYYw +DQYJKoZIhvcNAQEMBQADggIBACN4Htp2PvGcQA0/sAS+qUVWWJoAXSsu8Pgc6Gar +7tKVlNJ/4W/a6pUV2Xo/Tz3msg4yiE8sMESp2k+USosD5n9Alai5s5qpWDQjrqrh +76AGyF2nzve4kIN19GArYhm4Mz/EKEG1QHYvBDGgXi3kNvL/a2Zbybp+3LevG+q7 +xtx4Sz9yIyMzuT/6Y7ijtiMZ9XbuxGf5wab8UtwT3Xq1UradJy0KCkzRJAz/Wy/X +HbTkEvKSaYKExH6sLo0jqdIjV/d2Io31gt4e0Ly1ER2wPyFa+pc/swu7HCzrN+iz +A2ZM4+KX9nBvFyfkHLix4rALg+WTYJa/dIsObXkdZ3z8qPf5A9PXlULiaa1mcP4+ +rokw74IyLEYooQ8iSOjxumXhnkTS69MAdGzXYE5gnHokABtGD+BB5qLhtLt4fqAp +8AyHpQWMyV42M9SJLzQ+iOz7kAgJOBOaVtJI3FV/iAg/eqWVm3yLuUTWDxSHrKuL +N19+pSjF6TNvUSFXwEa2LJkfDqIOCE32iOuy85QY//3NsgrSQF6UkSPa95eJrSGI +3hTRYYh3Up2GhBGl1KUy7/o0k3KRZTk4s38fylY8bZ3TakUOH5iIGoHyFVVcp361 +Pyy25SzFSmNalWoQd9wZVc/Cps2ldxhcttM+WLkFNzprd0VJa8qTz8vYtHP0ouDN +nWS0 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICtDCCAjmgAwIBAgIQKKqVZvk6NsLET+uYv5myCzAKBggqhkjOPQQDAzCBmTEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTIwMAYDVQQDDClBbWF6 +b24gUkRTIGlsLWNlbnRyYWwtMSBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwH +U2VhdHRsZTAgFw0yMjEyMDIyMDMyMjBaGA8yMTIyMTIwMjIxMzIyMFowgZkxCzAJ +BgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMw +EQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEyMDAGA1UEAwwpQW1hem9u +IFJEUyBpbC1jZW50cmFsLTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASYwfvj8BmvLAP6UkNQ4X4dXBB/ +webBO7swW+8HnFN2DAu+Cn/lpcDpu+dys1JmkVX435lrCH3oZjol0kCDIM1lF4Cv ++78yoY1Jr/YMat22E4iz4AZd9q0NToS7+ZA0r2yjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFO/8Py16qPr7J2GWpvxlTMB+op7XMA4GA1UdDwEB/wQEAwIB +hjAKBggqhkjOPQQDAwNpADBmAjEAwk+rg788+u8JL6sdix7l57WTo8E/M+o3TO5x +uRuPdShrBFm4ArGR2PPs4zCQuKgqAjEAi0TA3PVqAxKpoz+Ps8/054p9WTgDfBFZ +i/lm2yTaPs0xjY6FNWoy7fsVw5oEKxOn +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGCTCCA/GgAwIBAgIRAOY7gfcBZgR2tqfBzMbFQCUwDQYJKoZIhvcNAQEMBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1zb3V0aGVhc3QtNCBSb290IENBIFJTQTQwOTYgRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjIwNTI1MTY1NDU5WhgPMjEyMjA1MjUxNzU0NTla +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgYXAtc291dGhlYXN0LTQgUm9vdCBDQSBSU0E0MDk2IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA +lfxER43FuLRdL08bddF0YhbCP+XXKj1A/TFMXmd2My8XDei8rPXFYyyjMig9+xZw +uAsIxLwz8uiA26CKA8bCZKg5VG2kTeOJAfvBJaLv1CZefs3Z4Uf1Sjvm6MF2yqEj +GoORfyfL9HiZFTDuF/hcjWoKYCfMuG6M/wO8IbdICrX3n+BiYQJu/pFO660Mg3h/ +8YBBWYDbHoCiH/vkqqJugQ5BM3OI5nsElW51P1icEEqti4AZ7JmtSv9t7fIFBVyR +oaEyOgpp0sm193F/cDJQdssvjoOnaubsSYm1ep3awZAUyGN/X8MBrPY95d0hLhfH +Ehc5Icyg+hsosBljlAyksmt4hFQ9iBnWIz/ZTfGMck+6p3HVL9RDgvluez+rWv59 +8q7omUGsiPApy5PDdwI/Wt/KtC34/2sjslIJfvgifdAtkRPkhff1WEwER00ADrN9 +eGGInaCpJfb1Rq8cV2n00jxg7DcEd65VR3dmIRb0bL+jWK62ni/WdEyomAOMfmGj +aWf78S/4rasHllWJ+QwnaUYY3u6N8Cgio0/ep4i34FxMXqMV3V0/qXdfhyabi/LM +wCxNo1Dwt+s6OtPJbwO92JL+829QAxydfmaMTeHBsgMPkG7RwAekeuatKGHNsc2Z +x2Q4C2wVvOGAhcHwxfM8JfZs3nDSZJndtVVnFlUY0UECAwEAAaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUpnG7mWazy6k97/tb5iduRB3RXgQwDgYDVR0P +AQH/BAQDAgGGMA0GCSqGSIb3DQEBDAUAA4ICAQCDLqq1Wwa9Tkuv7vxBnIeVvvFF +ecTn+P+wJxl9Qa2ortzqTHZsBDyJO62d04AgBwiDXkJ9a+bthgG0H1J7Xee8xqv1 +xyX2yKj24ygHjspLotKP4eDMdDi5TYq+gdkbPmm9Q69B1+W6e049JVGXvWG8/7kU +igxeuCYwtCCdUPRLf6D8y+1XMGgVv3/DSOHWvTg3MJ1wJ3n3+eve3rjGdRYWZeJu +k21HLSZYzVrCtUsh2YAeLnUbSxVuT2Xr4JehYe9zW5HEQ8Je/OUfnCy9vzoN/ITw +osAH+EBJQey7RxEDqMwCaRefH0yeHFcnOll0OXg/urnQmwbEYzQ1uutJaBPsjU0J +Qf06sMxI7GiB5nPE+CnI2sM6A9AW9kvwexGXpNJiLxF8dvPQthpOKGcYu6BFvRmt +6ctfXd9b7JJoVqMWuf5cCY6ihpk1e9JTlAqu4Eb/7JNyGiGCR40iSLvV28un9wiE +plrdYxwcNYq851BEu3r3AyYWw/UW1AKJ5tM+/Gtok+AphMC9ywT66o/Kfu44mOWm +L3nSLSWEcgfUVgrikpnyGbUnGtgCmHiMlUtNVexcE7OtCIZoVAlCGKNu7tyuJf10 +Qlk8oIIzfSIlcbHpOYoN79FkLoDNc2er4Gd+7w1oPQmdAB0jBJnA6t0OUBPKdDdE +Ufff2jrbfbzECn1ELg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGCDCCA/CgAwIBAgIQIuO1A8LOnmc7zZ/vMm3TrDANBgkqhkiG9w0BAQwFADCB +nDELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTUwMwYDVQQDDCxB +bWF6b24gUkRTIGFwLXNvdXRoZWFzdC0yIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4G +A1UEBwwHU2VhdHRsZTAgFw0yMTA1MjQyMDQ2MThaGA8yMTIxMDUyNDIxNDYxOFow +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1zb3V0aGVhc3QtMiBSb290IENBIFJTQTQwOTYgRzExEDAO +BgNVBAcMB1NlYXR0bGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDq +qRHKbG8ZK6/GkGm2cenznEF06yHwI1gD5sdsHjTgekDZ2Dl9RwtDmUH2zFuIQwGj +SeC7E2iKwrJRA5wYzL9/Vk8NOILEKQOP8OIKUHbc7q8rEtjs401KcU6pFBBEdO9G +CTiRhogq+8mhC13AM/UriZJbKhwgM2UaDOzAneGMhQAGjH8z83NsNcPxpYVE7tqM +sch5yLtIJLkJRusrmQQTeHUev16YNqyUa+LuFclFL0FzFCimkcxUhXlbfEKXbssS +yPzjiv8wokGyo7+gA0SueceMO2UjfGfute3HlXZDcNvBbkSY+ver41jPydyRD6Qq +oEkh0tyIbPoa3oU74kwipJtz6KBEA3u3iq61OUR0ENhR2NeP7CSKrC24SnQJZ/92 +qxusrbyV/0w+U4m62ug/o4hWNK1lUcc2AqiBOvCSJ7qpdteTFxcEIzDwYfERDx6a +d9+3IPvzMb0ZCxBIIUFMxLTF7yAxI9s6KZBBXSZ6tDcCCYIgEysEPRWMRAcG+ye/ +fZVn9Vnzsj4/2wchC2eQrYpb1QvG4eMXA4M5tFHKi+/8cOPiUzJRgwS222J8YuDj +yEBval874OzXk8H8Mj0JXJ/jH66WuxcBbh5K7Rp5oJn7yju9yqX6qubY8gVeMZ1i +u4oXCopefDqa35JplQNUXbWwSebi0qJ4EK0V8F9Q+QIDAQABo0IwQDAPBgNVHRMB +Af8EBTADAQH/MB0GA1UdDgQWBBT4ysqCxaPe7y+g1KUIAenqu8PAgzAOBgNVHQ8B +Af8EBAMCAYYwDQYJKoZIhvcNAQEMBQADggIBALU8WN35KAjPZEX65tobtCDQFkIO +uJjv0alD7qLB0i9eY80C+kD87HKqdMDJv50a5fZdqOta8BrHutgFtDm+xo5F/1M3 +u5/Vva5lV4xy5DqPajcF4Mw52czYBmeiLRTnyPJsU93EQIC2Bp4Egvb6LI4cMOgm +4pY2hL8DojOC5PXt4B1/7c1DNcJX3CMzHDm4SMwiv2MAxSuC/cbHXcWMk+qXdrVx ++ayLUSh8acaAOy3KLs1MVExJ6j9iFIGsDVsO4vr4ZNsYQiyHjp+L8ops6YVBO5AT +k/pI+axHIVsO5qiD4cFWvkGqmZ0gsVtgGUchZaacboyFsVmo6QPrl28l6LwxkIEv +GGJYvIBW8sfqtGRspjfX5TlNy5IgW/VOwGBdHHsvg/xpRo31PR3HOFw7uPBi7cAr +FiZRLJut7af98EB2UvovZnOh7uIEGPeecQWeOTQfJeWet2FqTzFYd0NUMgqPuJx1 +vLKferP+ajAZLJvVnW1J7Vccx/pm0rMiUJEf0LRb/6XFxx7T2RGjJTi0EzXODTYI +gnLfBBjnolQqw+emf4pJ4pAtly0Gq1KoxTG2QN+wTd4lsCMjnelklFDjejwnl7Uy +vtxzRBAu/hi/AqDkDFf94m6j+edIrjbi9/JDFtQ9EDlyeqPgw0qwi2fwtJyMD45V +fejbXelUSJSzDIdY +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGCTCCA/GgAwIBAgIRAN7Y9G9i4I+ZaslPobE7VL4wDQYJKoZIhvcNAQEMBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1ub3J0aGVhc3QtMiBSb290IENBIFJTQTQwOTYgRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjEwNTIwMTYzMzIzWhgPMjEyMTA1MjAxNzMzMjNa +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgYXAtbm9ydGhlYXN0LTIgUm9vdCBDQSBSU0E0MDk2IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA +4BEPCiIfiK66Q/qa8k+eqf1Q3qsa6Xuu/fPkpuStXVBShhtXd3eqrM0iT4Xxs420 +Va0vSB3oZ7l86P9zYfa60n6PzRxdYFckYX330aI7L/oFIdaodB/C9szvROI0oLG+ +6RwmIF2zcprH0cTby8MiM7G3v9ykpq27g4WhDC1if2j8giOQL3oHpUaByekZNIHF +dIllsI3RkXmR3xmmxoOxJM1B9MZi7e1CvuVtTGOnSGpNCQiqofehTGwxCN2wFSK8 +xysaWlw48G0VzZs7cbxoXMH9QbMpb4tpk0d+T8JfAPu6uWO9UwCLWWydf0CkmA/+ +D50/xd1t33X9P4FEaPSg5lYbHXzSLWn7oLbrN2UqMLaQrkoEBg/VGvzmfN0mbflw ++T87bJ/VEOVNlG+gepyCTf89qIQVWOjuYMox4sK0PjzZGsYEuYiq1+OUT3vk/e5K +ag1fCcq2Isy4/iwB2xcXrsQ6ljwdk1fc+EmOnjGKrhuOHJY3S+RFv4ToQBsVyYhC +XGaC3EkqIX0xaCpDimxYhFjWhpDXAjG/zJ+hRLDAMCMhl/LPGRk/D1kzSbPmdjpl +lEMK5695PeBvEBTQdBQdOiYgOU3vWU6tzwwHfiM2/wgvess/q0FDAHfJhppbgbb9 +3vgsIUcsvoC5o29JvMsUxsDRvsAfEmMSDGkJoA/X6GECAwEAAaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUgEWm1mZCbGD6ytbwk2UU1aLaOUUwDgYDVR0P +AQH/BAQDAgGGMA0GCSqGSIb3DQEBDAUAA4ICAQBb4+ABTGBGwxK1U/q4g8JDqTQM +1Wh8Oz8yAk4XtPJMAmCctxbd81cRnSnePWw/hxViLVtkZ/GsemvXfqAQyOn1coN7 +QeYSw+ZOlu0j2jEJVynmgsR7nIRqE7QkCyZAU+d2FTJUfmee+IiBiGyFGgxz9n7A +JhBZ/eahBbiuoOik/APW2JWLh0xp0W0GznfJ8lAlaQTyDa8iDXmVtbJg9P9qzkvl +FgPXQttzEOyooF8Pb2LCZO4kUz+1sbU7tHdr2YE+SXxt6D3SBv+Yf0FlvyWLiqVk +GDEOlPPTDSjAWgKnqST8UJ0RDcZK/v1ixs7ayqQJU0GUQm1I7LGTErWXHMnCuHKe +UKYuiSZwmTcJ06NgdhcCnGZgPq13ryMDqxPeltQc3n5eO7f1cL9ERYLDLOzm6A9P +oQ3MfcVOsbHgGHZWaPSeNrQRN9xefqBXH0ZPasgcH9WJdsLlEjVUXoultaHOKx3b +UCCb+d3EfqF6pRT488ippOL6bk7zNubwhRa/+y4wjZtwe3kAX78ACJVcjPobH9jZ +ErySads5zdQeaoee5wRKdp3TOfvuCe4bwLRdhOLCHWzEcXzY3g/6+ppLvNom8o+h +Bh5X26G6KSfr9tqhQ3O9IcbARjnuPbvtJnoPY0gz3EHHGPhy0RNW8i2gl3nUp0ah +PtjwbKW0hYAhIttT0Q== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICtzCCAj2gAwIBAgIQQRBQTs6Y3H1DDbpHGta3lzAKBggqhkjOPQQDAzCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIGFwLXNvdXRoZWFzdC0zIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDYxMTAwMTI0M1oYDzIxMjEwNjExMDExMjQzWjCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIGFwLXNvdXRoZWFzdC0zIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEs0942Xj4m/gKA+WA6F5h +AHYuek9eGpzTRoLJddM4rEV1T3eSueytMVKOSlS3Ub9IhyQrH2D8EHsLYk9ktnGR +pATk0kCYTqFbB7onNo070lmMJmGT/Q7NgwC8cySChFxbo0IwQDAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBQ20iKBKiNkcbIZRu0y1uoF1yJTEzAOBgNVHQ8BAf8E +BAMCAYYwCgYIKoZIzj0EAwMDaAAwZQIwYv0wTSrpQTaPaarfLN8Xcqrqu3hzl07n +FrESIoRw6Cx77ZscFi2/MV6AFyjCV/TlAjEAhpwJ3tpzPXpThRML8DMJYZ3YgMh3 +CMuLqhPpla3cL0PhybrD27hJWl29C4el6aMO +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrDCCAjOgAwIBAgIQGcztRyV40pyMKbNeSN+vXTAKBggqhkjOPQQDAzCBljEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMS8wLQYDVQQDDCZBbWF6 +b24gUkRTIHVzLWVhc3QtMiBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTAgFw0yMTA1MjEyMzE1NTZaGA8yMTIxMDUyMjAwMTU1NlowgZYxCzAJBgNV +BAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYD +VQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEvMC0GA1UEAwwmQW1hem9uIFJE +UyB1cy1lYXN0LTIgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1NlYXR0bGUw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQfDcv+GGRESD9wT+I5YIPRsD3L+/jsiIis +Tr7t9RSbFl+gYpO7ZbDXvNbV5UGOC5lMJo/SnqFRTC6vL06NF7qOHfig3XO8QnQz +6T5uhhrhnX2RSY3/10d2kTyHq3ZZg3+jQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFLDyD3PRyNXpvKHPYYxjHXWOgfPnMA4GA1UdDwEB/wQEAwIBhjAKBggq +hkjOPQQDAwNnADBkAjB20HQp6YL7CqYD82KaLGzgw305aUKw2aMrdkBR29J183jY +6Ocj9+Wcif9xnRMS+7oCMAvrt03rbh4SU9BohpRUcQ2Pjkh7RoY0jDR4Xq4qzjNr +5UFr3BXpFvACxXF51BksGQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrjCCAjWgAwIBAgIQeKbS5zvtqDvRtwr5H48cAjAKBggqhkjOPQQDAzCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIG1lLXNvdXRoLTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjEwNTIwMTcxOTU1WhgPMjEyMTA1MjAxODE5NTVaMIGXMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpvbiBS +RFMgbWUtc291dGgtMSBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwHU2VhdHRs +ZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABEKjgUaAPmUlRMEQdBC7BScAGosJ1zRV +LDd38qTBjzgmwBfQJ5ZfGIvyEK5unB09MB4e/3qqK5I/L6Qn5Px/n5g4dq0c7MQZ +u7G9GBYm90U3WRJBf7lQrPStXaRnS4A/O6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUNKcAbGEIn03/vkwd8g6jNyiRdD4wDgYDVR0PAQH/BAQDAgGGMAoG +CCqGSM49BAMDA2cAMGQCMHIeTrjenCSYuGC6txuBt/0ZwnM/ciO9kHGWVCoK8QLs +jGghb5/YSFGZbmQ6qpGlSAIwVOQgdFfTpEfe5i+Vs9frLJ4QKAfc27cTNYzRIM0I +E+AJgK4C4+DiyyMzOpiCfmvq +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGCDCCA/CgAwIBAgIQSFkEUzu9FYgC5dW+5lnTgjANBgkqhkiG9w0BAQwFADCB +nDELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTUwMwYDVQQDDCxB +bWF6b24gUkRTIGFwLXNvdXRoZWFzdC0zIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4G +A1UEBwwHU2VhdHRsZTAgFw0yMTA2MTEwMDA4MzZaGA8yMTIxMDYxMTAxMDgzNlow +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1zb3V0aGVhc3QtMyBSb290IENBIFJTQTQwOTYgRzExEDAO +BgNVBAcMB1NlYXR0bGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDx +my5Qmd8zdwaI/KOKV9Xar9oNbhJP5ED0JCiigkuvCkg5qM36klszE8JhsUj40xpp +vQw9wkYW4y+C8twBpzKGBvakqMnoaVUV7lOCKx0RofrnNwkZCboTBB4X/GCZ3fIl +YTybS7Ehi1UuiaZspIT5A2jidoA8HiBPk+mTg1UUkoWS9h+MEAPa8L4DY6fGf4pO +J1Gk2cdePuNzzIrpm2yPto+I8MRROwZ3ha7ooyymOXKtz2c7jEHHJ314boCXAv9G +cdo27WiebewZkHHH7Zx9iTIVuuk2abyVSzvLVeGv7Nuy4lmSqa5clWYqWsGXxvZ2 +0fZC5Gd+BDUMW1eSpW7QDTk3top6x/coNoWuLSfXiC5ZrJkIKimSp9iguULgpK7G +abMMN4PR+O+vhcB8E879hcwmS2yd3IwcPTl3QXxufqeSV58/h2ibkqb/W4Bvggf6 +5JMHQPlPHOqMCVFIHP1IffIo+Of7clb30g9FD2j3F4qgV3OLwEDNg/zuO1DiAvH1 +L+OnmGHkfbtYz+AVApkAZrxMWwoYrwpauyBusvSzwRE24vLTd2i80ZDH422QBLXG +rN7Zas8rwIiBKacJLYtBYETw8mfsNt8gb72aIQX6cZOsphqp6hUtKaiMTVgGazl7 +tBXqbB+sIv3S9X6bM4cZJKkMJOXbnyCCLZFYv8TurwIDAQABo0IwQDAPBgNVHRMB +Af8EBTADAQH/MB0GA1UdDgQWBBTOVtaS1b/lz6yJDvNk65vEastbQTAOBgNVHQ8B +Af8EBAMCAYYwDQYJKoZIhvcNAQEMBQADggIBABEONg+TmMZM/PrYGNAfB4S41zp1 +3CVjslZswh/pC4kgXSf8cPJiUOzMwUevuFQj7tCqxQtJEygJM2IFg4ViInIah2kh +xlRakEGGw2dEVlxZAmmLWxlL1s1lN1565t5kgVwM0GVfwYM2xEvUaby6KDVJIkD3 +aM6sFDBshvVA70qOggM6kU6mwTbivOROzfoIQDnVaT+LQjHqY/T+ok6IN0YXXCWl +Favai8RDjzLDFwXSRvgIK+1c49vlFFY4W9Efp7Z9tPSZU1TvWUcKdAtV8P2fPHAS +vAZ+g9JuNfeawhEibjXkwg6Z/yFUueQCQOs9TRXYogzp5CMMkfdNJF8byKYqHscs +UosIcETnHwqwban99u35sWcoDZPr6aBIrz7LGKTJrL8Nis8qHqnqQBXu/fsQEN8u +zJ2LBi8sievnzd0qI0kaWmg8GzZmYH1JCt1GXSqOFkI8FMy2bahP7TUQR1LBUKQ3 +hrOSqldkhN+cSAOnvbQcFzLr+iEYEk34+NhcMIFVE+51KJ1n6+zISOinr6mI3ckX +6p2tmiCD4Shk2Xx/VTY/KGvQWKFcQApWezBSvDNlGe0yV71LtLf3dr1pr4ofo7cE +rYucCJ40bfxEU/fmzYdBF32xP7AOD9U0FbOR3Mcthc6Z6w20WFC+zru8FGY08gPf +WT1QcNdw7ntUJP/w +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrzCCAjWgAwIBAgIQARky6+5PNFRkFVOp3Ob1CTAKBggqhkjOPQQDAzCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIGV1LXNvdXRoLTIgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjIwNTIzMTg0MTI4WhgPMjEyMjA1MjMxOTQxMjdaMIGXMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpvbiBS +RFMgZXUtc291dGgtMiBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwHU2VhdHRs +ZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABNVGL5oF7cfIBxKyWd2PVK/S5yQfaJY3 +QFHWvEdt6951n9JhiiPrHzfVHsxZp1CBjILRMzjgRbYWmc8qRoLkgGE7htGdwudJ +Fa/WuKzO574Prv4iZXUnVGTboC7JdvKbh6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUgDeIIEKynwUbNXApdIPnmRWieZwwDgYDVR0PAQH/BAQDAgGGMAoG +CCqGSM49BAMDA2gAMGUCMEOOJfucrST+FxuqJkMZyCM3gWGZaB+/w6+XUAJC6hFM +uSTY0F44/bERkA4XhH+YGAIxAIpJQBakCA1/mXjsTnQ+0El9ty+LODp8ibkn031c +8DKDS7pR9UK7ZYdR6zFg3ZCjQw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrjCCAjOgAwIBAgIQJvkWUcYLbnxtuwnyjMmntDAKBggqhkjOPQQDAzCBljEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMS8wLQYDVQQDDCZBbWF6 +b24gUkRTIGV1LXdlc3QtMyBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTAgFw0yMTA1MjUyMjI2MTJaGA8yMTIxMDUyNTIzMjYxMlowgZYxCzAJBgNV +BAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYD +VQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEvMC0GA1UEAwwmQW1hem9uIFJE +UyBldS13ZXN0LTMgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1NlYXR0bGUw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAARENn8uHCyjn1dFax4OeXxvbV861qsXFD9G +DshumTmFzWWHN/69WN/AOsxy9XN5S7Cgad4gQgeYYYgZ5taw+tFo/jQvCLY//uR5 +uihcLuLJ78opvRPvD9kbWZ6oXfBtFkWjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFKiK3LpoF+gDnqPldGSwChBPCYciMA4GA1UdDwEB/wQEAwIBhjAKBggq +hkjOPQQDAwNpADBmAjEA+7qfvRlnvF1Aosyp9HzxxCbN7VKu+QXXPhLEBWa5oeWW +UOcifunf/IVLC4/FGCsLAjEAte1AYp+iJyOHDB8UYkhBE/1sxnFaTiEPbvQBU0wZ +SuwWVLhu2wWDuSW+K7tTuL8p +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID/zCCAuegAwIBAgIRAKeDpqX5WFCGNo94M4v69sUwDQYJKoZIhvcNAQELBQAw +gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn +QW1hem9uIFJEUyBldS13ZXN0LTMgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyNTIyMTgzM1oYDzIwNjEwNTI1MjMxODMzWjCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIGV1LXdlc3QtMyBSb290IENBIFJTQTIwNDggRzExEDAOBgNVBAcMB1Nl +YXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCcKOTEMTfzvs4H +WtJR8gI7GXN6xesulWtZPv21oT+fLGwJ+9Bv8ADCGDDrDxfeH/HxJmzG9hgVAzVn +4g97Bn7q07tGZM5pVi96/aNp11velZT7spOJKfJDZTlGns6DPdHmx48whpdO+dOb +6+eR0VwCIv+Vl1fWXgoACXYCoKjhxJs+R+fwY//0JJ1YG8yjZ+ghLCJmvlkOJmE1 +TCPUyIENaEONd6T+FHGLVYRRxC2cPO65Jc4yQjsXvvQypoGgx7FwD5voNJnFMdyY +754JGPOOe/SZdepN7Tz7UEq8kn7NQSbhmCsgA/Hkjkchz96qN/YJ+H/okiQUTNB0 +eG9ogiVFAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFjayw9Y +MjbxfF14XAhMM2VPl0PfMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOC +AQEAAtmx6d9+9CWlMoU0JCirtp4dSS41bBfb9Oor6GQ8WIr2LdfZLL6uES/ubJPE +1Sh5Vu/Zon5/MbqLMVrfniv3UpQIof37jKXsjZJFE1JVD/qQfRzG8AlBkYgHNEiS +VtD4lFxERmaCkY1tjKB4Dbd5hfhdrDy29618ZjbSP7NwAfnwb96jobCmMKgxVGiH +UqsLSiEBZ33b2hI7PJ6iTJnYBWGuiDnsWzKRmheA4nxwbmcQSfjbrNwa93w3caL2 +v/4u54Kcasvcu3yFsUwJygt8z43jsGAemNZsS7GWESxVVlW93MJRn6M+MMakkl9L +tWaXdHZ+KUV7LhfYLb0ajvb40w== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEBDCCAuygAwIBAgIQJ5oxPEjefCsaESSwrxk68DANBgkqhkiG9w0BAQsFADCB +mjELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTMwMQYDVQQDDCpB +bWF6b24gUkRTIGV1LWNlbnRyYWwtMiBSb290IENBIFJTQTIwNDggRzExEDAOBgNV +BAcMB1NlYXR0bGUwIBcNMjIwNjA2MjExNzA1WhgPMjA2MjA2MDYyMjE3MDVaMIGa +MQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5j +LjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMzAxBgNVBAMMKkFt +YXpvbiBSRFMgZXUtY2VudHJhbC0yIFJvb3QgQ0EgUlNBMjA0OCBHMTEQMA4GA1UE +BwwHU2VhdHRsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALTQt5eX +g+VP3BjO9VBkWJhE0GfLrU/QIk32I6WvrnejayTrlup9H1z4QWlXF7GNJrqScRMY +KhJHlcP05aPsx1lYco6pdFOf42ybXyWHHJdShj4A5glU81GTT+VrXGzHSarLmtua +eozkQgPpDsSlPt0RefyTyel7r3Cq+5K/4vyjCTcIqbfgaGwTU36ffjM1LaPCuE4O +nINMeD6YuImt2hU/mFl20FZ+IZQUIFZZU7pxGLqTRz/PWcH8tDDxnkYg7tNuXOeN +JbTpXrw7St50/E9ZQ0llGS+MxJD8jGRAa/oL4G/cwnV8P2OEPVVkgN9xDDQeieo0 +3xkzolkDkmeKOnUCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +bwu8635iQGQMRanekesORM8Hkm4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +CwUAA4IBAQAgN6LE9mUgjsj6xGCX1afYE69fnmCjjb0rC6eEe1mb/QZNcyw4XBIW +6+zTXo4mjZ4ffoxb//R0/+vdTE7IvaLgfAZgFsLKJCtYDDstXZj8ujQnGR9Pig3R +W+LpNacvOOSJSawNQq0Xrlcu55AU4buyD5VjcICnfF1dqBMnGTnh27m/scd/ZMx/ +kapHZ/fMoK2mAgSX/NvUKF3UkhT85vSSM2BTtET33DzCPDQTZQYxFBa4rFRmFi4c +BLlmIReiCGyh3eJhuUUuYAbK6wLaRyPsyEcIOLMQmZe1+gAFm1+1/q5Ke9ugBmjf +PbTWjsi/lfZ5CdVAhc5lmZj/l5aKqwaS +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrjCCAjSgAwIBAgIRAKKPTYKln9L4NTx9dpZGUjowCgYIKoZIzj0EAwMwgZYx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEvMC0GA1UEAwwmQW1h +em9uIFJEUyBldS13ZXN0LTIgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjEwNTIxMjI1NTIxWhgPMjEyMTA1MjEyMzU1MjFaMIGWMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExLzAtBgNVBAMMJkFtYXpvbiBS +RFMgZXUtd2VzdC0yIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQHDAdTZWF0dGxl +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE/owTReDvaRqdmbtTzXbyRmEpKCETNj6O +hZMKH0F8oU9Tmn8RU7kQQj6xUKEyjLPrFBN7c+26TvrVO1KmJAvbc8bVliiJZMbc +C0yV5PtJTalvlMZA1NnciZuhxaxrzlK1o0IwQDAPBgNVHRMBAf8EBTADAQH/MB0G +A1UdDgQWBBT4i5HaoHtrs7Mi8auLhMbKM1XevDAOBgNVHQ8BAf8EBAMCAYYwCgYI +KoZIzj0EAwMDaAAwZQIxAK9A+8/lFdX4XJKgfP+ZLy5ySXC2E0Spoy12Gv2GdUEZ +p1G7c1KbWVlyb1d6subzkQIwKyH0Naf/3usWfftkmq8SzagicKz5cGcEUaULq4tO +GzA/AMpr63IDBAqkZbMDTCmH +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrzCCAjWgAwIBAgIQTgIvwTDuNWQo0Oe1sOPQEzAKBggqhkjOPQQDAzCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIGV1LW5vcnRoLTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjEwNTI0MjEwNjM4WhgPMjEyMTA1MjQyMjA2MzhaMIGXMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpvbiBS +RFMgZXUtbm9ydGgtMSBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwHU2VhdHRs +ZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJuzXLU8q6WwSKXBvx8BbdIi3mPhb7Xo +rNJBfuMW1XRj5BcKH1ZoGaDGw+BIIwyBJg8qNmCK8kqIb4cH8/Hbo3Y+xBJyoXq/ +cuk8aPrxiNoRsKWwiDHCsVxaK9L7GhHHAqNCMEAwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUYgcsdU4fm5xtuqLNppkfTHM2QMYwDgYDVR0PAQH/BAQDAgGGMAoG +CCqGSM49BAMDA2gAMGUCMQDz/Rm89+QJOWJecYAmYcBWCcETASyoK1kbr4vw7Hsg +7Ew3LpLeq4IRmTyuiTMl0gMCMAa0QSjfAnxBKGhAnYxcNJSntUyyMpaXzur43ec0 +3D8npJghwC4DuICtKEkQiI5cSg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGATCCA+mgAwIBAgIRAORIGqQXLTcbbYT2upIsSnQwDQYJKoZIhvcNAQEMBQAw +gZgxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwo +QW1hem9uIFJEUyBldS1zb3V0aC0yIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UE +BwwHU2VhdHRsZTAgFw0yMjA1MjMxODM0MjJaGA8yMTIyMDUyMzE5MzQyMlowgZgx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwoQW1h +em9uIFJEUyBldS1zb3V0aC0yIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UEBwwH +U2VhdHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPKukwsW2s/h +1k+Hf65pOP0knVBnOnMQyT1mopp2XHGdXznj9xS49S30jYoUnWccyXgD983A1bzu +w4fuJRHg4MFdz/NWTgXvy+zy0Roe83OPIJjUmXnnzwUHQcBa9vl6XUO65iQ3pbSi +fQfNDFXD8cvuXbkezeADoy+iFAlzhXTzV9MD44GTuo9Z3qAXNGHQCrgRSCL7uRYt +t1nfwboCbsVRnElopn2cTigyVXE62HzBUmAw1GTbAZeFAqCn5giBWYAfHwTUldRL +6eEa6atfsS2oPNus4ZENa1iQxXq7ft+pMdNt0qKXTCZiiCZjmLkY0V9kWwHTRRF8 +r+75oSL//3di43QnuSCgjwMRIeWNtMud5jf3eQzSBci+9njb6DrrSUbx7blP0srg +94/C/fYOp/0/EHH34w99Th14VVuGWgDgKahT9/COychLOubXUT6vD1As47S9KxTv +yYleVKwJnF9cVjepODN72fNlEf74BwzgSIhUmhksmZSeJBabrjSUj3pdyo/iRZN/ +CiYz9YPQ29eXHPQjBZVIUqWbOVfdwsx0/Xu5T1e7yyXByQ3/oDulahtcoKPAFQ3J +ee6NJK655MdS7pM9hJnU2Rzu3qZ/GkM6YK7xTlMXVouPUZov/VbiaCKbqYDs8Dg+ +UKdeNXAT6+BMleGQzly1X7vjhgeA8ugVAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFJdaPwpCf78UolFTEn6GO85/QwUIMA4GA1UdDwEB/wQEAwIB +hjANBgkqhkiG9w0BAQwFAAOCAgEAWkxHIT3mers5YnZRSVjmpxCLivGj1jMB9VYC +iKqTAeIvD0940L0YaZgivQll5pue8UUcQ6M2uCdVVAsNJdmQ5XHIYiGOknYPtxzO +aO+bnZp7VIZw/vJ49hvH6RreA2bbxYMZO/ossYdcWsWbOKHFrRmAw0AhtK/my51g +obV7eQg+WmlE5Iqc75ycUsoZdc3NimkjBi7LQoNP1HMvlLHlF71UZhQDdq+/WdV7 +0zmg+epkki1LjgMmuPyb+xWuYkFKT1/faX+Xs62hIm5BY+aI4if4RuQ+J//0pOSs +UajrjTo+jLGB8A96jAe8HaFQenbwMjlaHRDAF0wvbkYrMr5a6EbneAB37V05QD0Y +Rh4L4RrSs9DX2hbSmS6iLDuPEjanHKzglF5ePEvnItbRvGGkynqDVlwF+Bqfnw8l +0i8Hr1f1/LP1c075UjkvsHlUnGgPbLqA0rDdcxF8Fdlv1BunUjX0pVlz10Ha5M6P +AdyWUOneOfaA5G7jjv7i9qg3r99JNs1/Lmyg/tV++gnWTAsSPFSSEte81kmPhlK3 +2UtAO47nOdTtk+q4VIRAwY1MaOR7wTFZPfer1mWs4RhKNu/odp8urEY87iIzbMWT +QYO/4I6BGj9rEWNGncvR5XTowwIthMCj2KWKM3Z/JxvjVFylSf+s+FFfO1bNIm6h +u3UBpZI= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICtDCCAjmgAwIBAgIQenQbcP/Zbj9JxvZ+jXbRnTAKBggqhkjOPQQDAzCBmTEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTIwMAYDVQQDDClBbWF6 +b24gUkRTIGV1LWNlbnRyYWwtMSBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwH +U2VhdHRsZTAgFw0yMTA1MjEyMjMzMjRaGA8yMTIxMDUyMTIzMzMyNFowgZkxCzAJ +BgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMw +EQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEyMDAGA1UEAwwpQW1hem9u +IFJEUyBldS1jZW50cmFsLTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATlBHiEM9LoEb1Hdnd5j2VpCDOU +5nGuFoBD8ROUCkFLFh5mHrHfPXwBc63heW9WrP3qnDEm+UZEUvW7ROvtWCTPZdLz +Z4XaqgAlSqeE2VfUyZOZzBSgUUJk7OlznXfkCMOjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFDT/ThjQZl42Nv/4Z/7JYaPNMly2MA4GA1UdDwEB/wQEAwIB +hjAKBggqhkjOPQQDAwNpADBmAjEAnZWmSgpEbmq+oiCa13l5aGmxSlfp9h12Orvw +Dq/W5cENJz891QD0ufOsic5oGq1JAjEAp5kSJj0MxJBTHQze1Aa9gG4sjHBxXn98 +4MP1VGsQuhfndNHQb4V0Au7OWnOeiobq +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID/zCCAuegAwIBAgIRAMgnyikWz46xY6yRgiYwZ3swDQYJKoZIhvcNAQELBQAw +gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn +QW1hem9uIFJEUyBldS13ZXN0LTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyMDE2NDkxMloYDzIwNjEwNTIwMTc0OTEyWjCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIGV1LXdlc3QtMSBSb290IENBIFJTQTIwNDggRzExEDAOBgNVBAcMB1Nl +YXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCi8JYOc9cYSgZH +gYPxLk6Xcc7HqzamvsnjYU98Dcb98y6iDqS46Ra2Ne02MITtU5MDL+qjxb8WGDZV +RUA9ZS69tkTO3gldW8QdiSh3J6hVNJQW81F0M7ZWgV0gB3n76WCmfT4IWos0AXHM +5v7M/M4tqVmCPViQnZb2kdVlM3/Xc9GInfSMCgNfwHPTXl+PXX+xCdNBePaP/A5C +5S0oK3HiXaKGQAy3K7VnaQaYdiv32XUatlM4K2WS4AMKt+2cw3hTCjlmqKRHvYFQ +veWCXAuc+U5PQDJ9SuxB1buFJZhT4VP3JagOuZbh5NWpIbOTxlAJOb5pGEDuJTKi +1gQQQVEFAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNXm+N87 +OFxK9Af/bjSxDCiulGUzMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOC +AQEAkqIbkgZ45spvrgRQ6n9VKzDLvNg+WciLtmVrqyohwwJbj4pYvWwnKQCkVc7c +hUOSBmlSBa5REAPbH5o8bdt00FPRrD6BdXLXhaECKgjsHe1WW08nsequRKD8xVmc +8bEX6sw/utBeBV3mB+3Zv7ejYAbDFM4vnRsWtO+XqgReOgrl+cwdA6SNQT9oW3e5 +rSQ+VaXgJtl9NhkiIysq9BeYigxqS/A13pHQp0COMwS8nz+kBPHhJTsajHCDc8F4 +HfLi6cgs9G0gaRhT8FCH66OdGSqn196sE7Y3bPFFFs/3U+vxvmQgoZC6jegQXAg5 +Prxd+VNXtNI/azitTysQPumH7A== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEBTCCAu2gAwIBAgIRAO8bekN7rUReuNPG8pSTKtEwDQYJKoZIhvcNAQELBQAw +gZoxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEzMDEGA1UEAwwq +QW1hem9uIFJEUyBldS1jZW50cmFsLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYD +VQQHDAdTZWF0dGxlMCAXDTIxMDUyMTIyMjM0N1oYDzIwNjEwNTIxMjMyMzQ3WjCB +mjELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTMwMQYDVQQDDCpB +bWF6b24gUkRTIGV1LWNlbnRyYWwtMSBSb290IENBIFJTQTIwNDggRzExEDAOBgNV +BAcMB1NlYXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCTTYds +Tray+Q9VA5j5jTh5TunHKFQzn68ZbOzdqaoi/Rq4ohfC0xdLrxCpfqn2TGDHN6Zi +2qGK1tWJZEd1H0trhzd9d1CtGK+3cjabUmz/TjSW/qBar7e9MA67/iJ74Gc+Ww43 +A0xPNIWcL4aLrHaLm7sHgAO2UCKsrBUpxErOAACERScVYwPAfu79xeFcX7DmcX+e +lIqY16pQAvK2RIzrekSYfLFxwFq2hnlgKHaVgZ3keKP+nmXcXmRSHQYUUr72oYNZ +HcNYl2+gxCc9ccPEHM7xncVEKmb5cWEWvVoaysgQ+osi5f5aQdzgC2X2g2daKbyA +XL/z5FM9GHpS5BJjAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FBDAiJ7Py9/A9etNa/ebOnx5l5MGMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0B +AQsFAAOCAQEALMh/+81fFPdJV/RrJUeoUvFCGMp8iaANu97NpeJyKitNOv7RoeVP +WjivS0KcCqZaDBs+p6IZ0sLI5ZH098LDzzytcfZg0PsGqUAb8a0MiU/LfgDCI9Ee +jsOiwaFB8k0tfUJK32NPcIoQYApTMT2e26lPzYORSkfuntme2PTHUnuC7ikiQrZk +P+SZjWgRuMcp09JfRXyAYWIuix4Gy0eZ4rpRuaTK6mjAb1/LYoNK/iZ/gTeIqrNt +l70OWRsWW8jEmSyNTIubGK/gGGyfuZGSyqoRX6OKHESkP6SSulbIZHyJ5VZkgtXo +2XvyRyJ7w5pFyoofrL3Wv0UF8yt/GDszmg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF/zCCA+egAwIBAgIRAMDk/F+rrhdn42SfE+ghPC8wDQYJKoZIhvcNAQEMBQAw +gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn +QW1hem9uIFJEUyBldS13ZXN0LTIgUm9vdCBDQSBSU0E0MDk2IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyMTIyNTEyMloYDzIxMjEwNTIxMjM1MTIyWjCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIGV1LXdlc3QtMiBSb290IENBIFJTQTQwOTYgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2twMALVg9vRVu +VNqsr6N8thmp3Dy8jEGTsm3GCQ+C5P2YcGlD/T/5icfWW84uF7Sx3ezcGlvsqFMf +Ukj9sQyqtz7qfFFugyy7pa/eH9f48kWFHLbQYm9GEgbYBIrWMp1cy3vyxuMCwQN4 +DCncqU+yNpy0CprQJEha3PzY+3yJOjDQtc3zr99lyECCFJTDUucxHzyQvX89eL74 +uh8la0lKH3v9wPpnEoftbrwmm5jHNFdzj7uXUHUJ41N7af7z7QUfghIRhlBDiKtx +5lYZemPCXajTc3ryDKUZC/b+B6ViXZmAeMdmQoPE0jwyEp/uaUcdp+FlUQwCfsBk +ayPFEApTWgPiku2isjdeTVmEgL8bJTDUZ6FYFR7ZHcYAsDzcwHgIu3GGEMVRS3Uf +ILmioiyly9vcK4Sa01ondARmsi/I0s7pWpKflaekyv5boJKD/xqwz9lGejmJHelf +8Od2TyqJScMpB7Q8c2ROxBwqwB72jMCEvYigB+Wnbb8RipliqNflIGx938FRCzKL +UQUBmNAznR/yRRL0wHf9UAE/8v9a09uZABeiznzOFAl/frHpgdAbC00LkFlnwwgX +g8YfEFlkp4fLx5B7LtoO6uVNFVimLxtwirpyKoj3G4M/kvSTux8bTw0heBCmWmKR +57MS6k7ODzbv+Kpeht2hqVZCNFMxoQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBRuMnDhJjoj7DcKALj+HbxEqj3r6jAOBgNVHQ8BAf8EBAMCAYYw +DQYJKoZIhvcNAQEMBQADggIBALSnXfx72C3ldhBP5kY4Mo2DDaGQ8FGpTOOiD95d +0rf7I9LrsBGVqu/Nir+kqqP80PB70+Jy9fHFFigXwcPBX3MpKGxK8Cel7kVf8t1B +4YD6A6bqlzP+OUL0uGWfZpdpDxwMDI2Flt4NEldHgXWPjvN1VblEKs0+kPnKowyg +jhRMgBbD/y+8yg0fIcjXUDTAw/+INcp21gWaMukKQr/8HswqC1yoqW9in2ijQkpK +2RB9vcQ0/gXR0oJUbZQx0jn0OH8Agt7yfMAnJAdnHO4M3gjvlJLzIC5/4aGrRXZl +JoZKfJ2fZRnrFMi0nhAYDeInoS+Rwx+QzaBk6fX5VPyCj8foZ0nmqvuYoydzD8W5 +mMlycgxFqS+DUmO+liWllQC4/MnVBlHGB1Cu3wTj5kgOvNs/k+FW3GXGzD3+rpv0 +QTLuwSbMr+MbEThxrSZRSXTCQzKfehyC+WZejgLb+8ylLJUA10e62o7H9PvCrwj+ +ZDVmN7qj6amzvndCP98sZfX7CFZPLfcBd4wVIjHsFjSNEwWHOiFyLPPG7cdolGKA +lOFvonvo4A1uRc13/zFeP0Xi5n5OZ2go8aOOeGYdI2vB2sgH9R2IASH/jHmr0gvY +0dfBCcfXNgrS0toq0LX/y+5KkKOxh52vEYsJLdhqrveuZhQnsFEm/mFwjRXkyO7c +2jpC +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGADCCA+igAwIBAgIQYe0HgSuFFP9ivYM2vONTrTANBgkqhkiG9w0BAQwFADCB +mDELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChB +bWF6b24gUkRTIGV1LXNvdXRoLTEgUm9vdCBDQSBSU0E0MDk2IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUxOTE4MzMyMVoYDzIxMjEwNTE5MTkzMzIxWjCBmDEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChBbWF6 +b24gUkRTIGV1LXNvdXRoLTEgUm9vdCBDQSBSU0E0MDk2IEcxMRAwDgYDVQQHDAdT +ZWF0dGxlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuO7QPKfPMTo2 +POQWvzDLwi5f++X98hGjORI1zkN9kotCYH5pAzSBwBPoMNaIfedgmsIxGHj2fq5G +4oXagNhNuGP79Zl6uKW5H7S74W7aWM8C0s8zuxMOI4GZy5h2IfQk3m/3AzZEX5w8 +UtNPkzo2feDVOkerHT+j+vjXgAxZ4wHnuMDcRT+K4r9EXlAH6X9b/RO0JlfEwmNz +xlqqGxocq9qRC66N6W0HF2fNEAKP84n8H80xcZBOBthQORRi8HSmKcPdmrvwCuPz +M+L+j18q6RAVaA0ABbD0jMWcTf0UvjUfBStn5mvu/wGlLjmmRkZsppUTRukfwqXK +yltUsTq0tOIgCIpne5zA4v+MebbR5JBnsvd4gdh5BI01QH470yB7BkUefZ9bobOm +OseAAVXcYFJKe4DAA6uLDrqOfFSxV+CzVvEp3IhLRaik4G5MwI/h2c/jEYDqkg2J +HMflxc2gcSMdk7E5ByLz5f6QrFfSDFk02ZJTs4ssbbUEYohht9znPMQEaWVqATWE +3n0VspqZyoBNkH/agE5GiGZ/k/QyeqzMNj+c9kr43Upu8DpLrz8v2uAp5xNj3YVg +ihaeD6GW8+PQoEjZ3mrCmH7uGLmHxh7Am59LfEyNrDn+8Rq95WvkmbyHSVxZnBmo +h/6O3Jk+0/QhIXZ2hryMflPcYWeRGH0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQU2eFK7+R3x/me8roIBNxBrplkM6EwDgYDVR0PAQH/BAQDAgGG +MA0GCSqGSIb3DQEBDAUAA4ICAQB5gWFe5s7ObQFj1fTO9L6gYgtFhnwdmxU0q8Ke +HWCrdFmyXdC39qdAFOwM5/7fa9zKmiMrZvy9HNvCXEp4Z7z9mHhBmuqPZQx0qPgU +uLdP8wGRuWryzp3g2oqkX9t31Z0JnkbIdp7kfRT6ME4I4VQsaY5Y3mh+hIHOUvcy +p+98i3UuEIcwJnVAV9wTTzrWusZl9iaQ1nSYbmkX9bBssJ2GmtW+T+VS/1hJ/Q4f +AlE3dOQkLFoPPb3YRWBHr2n1LPIqMVwDNAuWavRA2dSfaLl+kzbn/dua7HTQU5D4 +b2Fu2vLhGirwRJe+V7zdef+tI7sngXqjgObyOeG5O2BY3s+um6D4fS0Th3QchMO7 +0+GwcIgSgcjIjlrt6/xJwJLE8cRkUUieYKq1C4McpZWTF30WnzOPUzRzLHkcNzNA +0A7sKMK6QoYWo5Rmo8zewUxUqzc9oQSrYADP7PEwGncLtFe+dlRFx+PA1a+lcIgo +1ZGfXigYtQ3VKkcknyYlJ+hN4eCMBHtD81xDy9iP2MLE41JhLnoB2rVEtewO5diF +7o95Mwl84VMkLhhHPeGKSKzEbBtYYBifHNct+Bst8dru8UumTltgfX6urH3DN+/8 +JF+5h3U8oR2LL5y76cyeb+GWDXXy9zoQe2QvTyTy88LwZq1JzujYi2k8QiLLhFIf +FEv9Bg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICsDCCAjagAwIBAgIRAMgApnfGYPpK/fD0dbN2U4YwCgYIKoZIzj0EAwMwgZcx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwnQW1h +em9uIFJEUyBldS1zb3V0aC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQHDAdT +ZWF0dGxlMCAXDTIxMDUxOTE4MzgxMVoYDzIxMjEwNTE5MTkzODExWjCBlzELMAkG +A1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4xEzAR +BgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6b24g +UkRTIGV1LXNvdXRoLTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1NlYXR0 +bGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQfEWl6d4qSuIoECdZPp+39LaKsfsX7 +THs3/RrtT0+h/jl3bjZ7Qc68k16x+HGcHbaayHfqD0LPdzH/kKtNSfQKqemdxDQh +Z4pwkixJu8T1VpXZ5zzCvBXCl75UqgEFS92jQjBAMA8GA1UdEwEB/wQFMAMBAf8w +HQYDVR0OBBYEFFPrSNtWS5JU+Tvi6ABV231XbjbEMA4GA1UdDwEB/wQEAwIBhjAK +BggqhkjOPQQDAwNoADBlAjEA+a7hF1IrNkBd2N/l7IQYAQw8chnRZDzh4wiGsZsC +6A83maaKFWUKIb3qZYXFSi02AjAbp3wxH3myAmF8WekDHhKcC2zDvyOiKLkg9Y6v +ZVmyMR043dscQbcsVoacOYv198c= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICtDCCAjqgAwIBAgIRAPhVkIsQ51JFhD2kjFK5uAkwCgYIKoZIzj0EAwMwgZkx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEyMDAGA1UEAwwpQW1h +em9uIFJEUyBldS1jZW50cmFsLTIgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcM +B1NlYXR0bGUwIBcNMjIwNjA2MjEyOTE3WhgPMjEyMjA2MDYyMjI5MTdaMIGZMQsw +CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET +MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMjAwBgNVBAMMKUFtYXpv +biBSRFMgZXUtY2VudHJhbC0yIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQHDAdT +ZWF0dGxlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEA5xnIEBtG5b2nmbj49UEwQza +yX0844fXjccYzZ8xCDUe9dS2XOUi0aZlGblgSe/3lwjg8fMcKXLObGGQfgIx1+5h +AIBjORis/dlyN5q/yH4U5sjS8tcR0GDGVHrsRUZCo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBRK+lSGutXf4DkTjR3WNfv4+KeNFTAOBgNVHQ8BAf8EBAMC +AYYwCgYIKoZIzj0EAwMDaAAwZQIxAJ4NxQ1Gerqr70ZrnUqc62Vl8NNqTzInamCG +Kce3FTsMWbS9qkgrjZkO9QqOcGIw/gIwSLrwUT+PKr9+H9eHyGvpq9/3AIYSnFkb +Cf3dyWPiLKoAtLFwjzB/CkJlsAS1c8dS +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF/jCCA+agAwIBAgIQGZH12Q7x41qIh9vDu9ikTjANBgkqhkiG9w0BAQwFADCB +lzELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdB +bWF6b24gUkRTIGV1LXdlc3QtMyBSb290IENBIFJTQTQwOTYgRzExEDAOBgNVBAcM +B1NlYXR0bGUwIBcNMjEwNTI1MjIyMjMzWhgPMjEyMTA1MjUyMzIyMzNaMIGXMQsw +CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET +MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpv +biBSRFMgZXUtd2VzdC0zIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMqE47sHXWzdpuqj +JHb+6jM9tDbQLDFnYjDWpq4VpLPZhb7xPNh9gnYYTPKG4avG421EblAHqzy9D2pN +1z90yKbIfUb/Sy2MhQbmZomsObhONEra06fJ0Dydyjswf1iYRp2kwpx5AgkVoNo7 +3dlws73zFjD7ImKvUx2C7B75bhnw2pJWkFnGcswl8fZt9B5Yt95sFOKEz2MSJE91 +kZlHtya19OUxZ/cSGci4MlOySzqzbGwUqGxEIDlY8I39VMwXaYQ8uXUN4G780VcL +u46FeyRGxZGz2n3hMc805WAA1V5uir87vuirTvoSVREET97HVRGVVNJJ/FM6GXr1 +VKtptybbo81nefYJg9KBysxAa2Ao2x2ry/2ZxwhS6VZ6v1+90bpZA1BIYFEDXXn/ +dW07HSCFnYSlgPtSc+Muh15mdr94LspYeDqNIierK9i4tB6ep7llJAnq0BU91fM2 +JPeqyoTtc3m06QhLf68ccSxO4l8Hmq9kLSHO7UXgtdjfRVaffngopTNk8qK7bIb7 +LrgkqhiQw/PRCZjUdyXL153/fUcsj9nFNe25gM4vcFYwH6c5trd2tUl31NTi1MfG +Mgp3d2dqxQBIYANkEjtBDMy3SqQLIo9EymqmVP8xx2A/gCBgaxvMAsI6FSWRoC7+ +hqJ8XH4mFnXSHKtYMe6WPY+/XZgtAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8w +HQYDVR0OBBYEFIkXqTnllT/VJnI2NqipA4XV8rh1MA4GA1UdDwEB/wQEAwIBhjAN +BgkqhkiG9w0BAQwFAAOCAgEAKjSle8eenGeHgT8pltWCw/HzWyQruVKhfYIBfKJd +MhV4EnH5BK7LxBIvpXGsFUrb0ThzSw0fn0zoA9jBs3i/Sj6KyeZ9qUF6b8ycDXd+ +wHonmJiQ7nk7UuMefaYAfs06vosgl1rI7eBHC0itexIQmKh0aX+821l4GEgEoSMf +loMFTLXv2w36fPHHCsZ67ODldgcZbKNnpCTX0YrCwEYO3Pz/L398btiRcWGrewrK +jdxAAyietra8DRno1Zl87685tfqc6HsL9v8rVw58clAo9XAQvT+fmSOFw/PogRZ7 +OMHUat3gu/uQ1M5S64nkLLFsKu7jzudBuoNmcJysPlzIbqJ7vYc82OUGe9ucF3wi +3tbKQ983hdJiTExVRBLX/fYjPsGbG3JtPTv89eg2tjWHlPhCDMMxyRKl6isu2RTq +6VT489Z2zQrC33MYF8ZqO1NKjtyMAMIZwxVu4cGLkVsqFmEV2ScDHa5RadDyD3Ok +m+mqybhvEVm5tPgY6p0ILPMN3yvJsMSPSvuBXhO/X5ppNnpw9gnxpwbjQKNhkFaG +M5pkADZ14uRguOLM4VthSwUSEAr5VQYCFZhEwK+UOyJAGiB/nJz6IxL5XBNUXmRM +Hl8Xvz4riq48LMQbjcVQj0XvH941yPh+P8xOi00SGaQRaWp55Vyr4YKGbV0mEDz1 +r1o= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF/zCCA+egAwIBAgIRAKwYju1QWxUZpn6D1gOtwgQwDQYJKoZIhvcNAQEMBQAw +gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn +QW1hem9uIFJEUyBldS13ZXN0LTEgUm9vdCBDQSBSU0E0MDk2IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyMDE2NTM1NFoYDzIxMjEwNTIwMTc1MzU0WjCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIGV1LXdlc3QtMSBSb290IENBIFJTQTQwOTYgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCKdBP1U4lqWWkc +Cb25/BKRTsvNVnISiKocva8GAzJyKfcGRa85gmgu41U+Hz6+39K+XkRfM0YS4BvQ +F1XxWT0bNyypuvwCvmYShSTjN1TY0ltncDddahTajE/4MdSOZb/c98u0yt03cH+G +hVwRyT50h0v/UEol50VfwcVAEZEgcQQYhf1IFUFlIvKpmDOqLuFakOnc7c9akK+i +ivST+JO1tgowbnNkn2iLlSSgUWgb1gjaOsNfysagv1RXdlyPw3EyfwkFifAQvF2P +Q0ayYZfYS640cccv7efM1MSVyFHR9PrrDsF/zr2S2sGPbeHr7R/HwLl+S5J/l9N9 +y0rk6IHAWV4dEkOvgpnuJKURwA48iu1Hhi9e4moNS6eqoK2KmY3VFpuiyWcA73nH +GSmyaH+YuMrF7Fnuu7GEHZL/o6+F5cL3mj2SJJhL7sz0ryf5Cs5R4yN9BIEj/f49 +wh84pM6nexoI0Q4wiSFCxWiBpjSmOK6h7z6+2utaB5p20XDZHhxAlmlx4vMuWtjh +XckgRFxc+ZpVMU3cAHUpVEoO49e/+qKEpPzp8Xg4cToKw2+AfTk3cmyyXQfGwXMQ +ZUHNZ3w9ILMWihGCM2aGUsLcGDRennvNmnmin/SENsOQ8Ku0/a3teEzwV9cmmdYz +5iYs1YtgPvKFobY6+T2RXXh+A5kprwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBSyUrsQVnKmA8z6/2Ech0rCvqpNmTAOBgNVHQ8BAf8EBAMCAYYw +DQYJKoZIhvcNAQEMBQADggIBAFlj3IFmgiFz5lvTzFTRizhVofhTJsGr14Yfkuc7 +UrXPuXOwJomd4uot2d/VIeGJpfnuS84qGdmQyGewGTJ9inatHsGZgHl9NHNWRwKZ +lTKTbBiq7aqgtUSFa06v202wpzU+1kadxJJePrbABxiXVfOmIW/a1a4hPNcT3syH +FIEg1+CGsp71UNjBuwg3JTKWna0sLSKcxLOSOvX1fzxK5djzVpEsvQMB4PSAzXca +vENgg2ErTwgTA+4s6rRtiBF9pAusN1QVuBahYP3ftrY6f3ycS4K65GnqscyfvKt5 +YgjtEKO3ZeeX8NpubMbzC+0Z6tVKfPFk/9TXuJtwvVeqow0YMrLLyRiYvK7EzJ97 +rrkxoKnHYQSZ+rH2tZ5SE392/rfk1PJL0cdHnkpDkUDO+8cKsFjjYKAQSNC52sKX +74AVh6wMwxYwVZZJf2/2XxkjMWWhKNejsZhUkTISSmiLs+qPe3L67IM7GyKm9/m6 +R3r8x6NGjhTsKH64iYJg7AeKeax4b2e4hBb6GXFftyOs7unpEOIVkJJgM6gh3mwn +R7v4gwFbLKADKt1vHuerSZMiTuNTGhSfCeDM53XI/mjZl2HeuCKP1mCDLlaO+gZR +Q/G+E0sBKgEX4xTkAc3kgkuQGfExdGtnN2U2ehF80lBHB8+2y2E+xWWXih/ZyIcW +wOx+ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGBDCCA+ygAwIBAgIQM4C8g5iFRucSWdC8EdqHeDANBgkqhkiG9w0BAQwFADCB +mjELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTMwMQYDVQQDDCpB +bWF6b24gUkRTIGV1LWNlbnRyYWwtMSBSb290IENBIFJTQTQwOTYgRzExEDAOBgNV +BAcMB1NlYXR0bGUwIBcNMjEwNTIxMjIyODI2WhgPMjEyMTA1MjEyMzI4MjZaMIGa +MQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5j +LjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMzAxBgNVBAMMKkFt +YXpvbiBSRFMgZXUtY2VudHJhbC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UE +BwwHU2VhdHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANeTsD/u +6saPiY4Sg0GlJlMXMBltnrcGAEkwq34OKQ0bCXqcoNJ2rcAMmuFC5x9Ho1Y3YzB7 +NO2GpIh6bZaO76GzSv4cnimcv9n/sQSYXsGbPD+bAtnN/RvNW1avt4C0q0/ghgF1 +VFS8JihIrgPYIArAmDtGNEdl5PUrdi9y6QGggbRfidMDdxlRdZBe1C18ZdgERSEv +UgSTPRlVczONG5qcQkUGCH83MMqL5MKQiby/Br5ZyPq6rxQMwRnQ7tROuElzyYzL +7d6kke+PNzG1mYy4cbYdjebwANCtZ2qYRSUHAQsOgybRcSoarv2xqcjO9cEsDiRU +l97ToadGYa4VVERuTaNZxQwrld4mvzpyKuirqZltOqg0eoy8VUsaRPL3dc5aChR0 +dSrBgRYmSAClcR2/2ZCWpXemikwgt031Dsc0A/+TmVurrsqszwbr0e5xqMow9LzO +MI/JtLd0VFtoOkL/7GG2tN8a+7gnLFxpv+AQ0DH5n4k/BY/IyS+H1erqSJhOTQ11 +vDOFTM5YplB9hWV9fp5PRs54ILlHTlZLpWGs3I2BrJwzRtg/rOlvsosqcge9ryai +AKm2j+JBg5wJ19R8oxRy8cfrNTftZePpISaLTyV2B16w/GsSjqixjTQe9LRN2DHk +cC+HPqYyzW2a3pUVyTGHhW6a7YsPBs9yzt6hAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFIqA8QkOs2cSirOpCuKuOh9VDfJfMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQwFAAOCAgEAOUI90mEIsa+vNJku0iUwdBMnHiO4gm7E +5JloP7JG0xUr7d0hypDorMM3zVDAL+aZRHsq8n934Cywj7qEp1304UF6538ByGdz +tkfacJsUSYfdlNJE9KbA4T+U+7SNhj9jvePpVjdQbhgzxITE9f8CxY/eM40yluJJ +PhbaWvOiRagzo74wttlcDerzLT6Y/JrVpWhnB7IY8HvzK+BwAdaCsBUPC3HF+kth +CIqLq7J3YArTToejWZAp5OOI6DLPM1MEudyoejL02w0jq0CChmZ5i55ElEMnapRX +7GQTARHmjgAOqa95FjbHEZzRPqZ72AtZAWKFcYFNk+grXSeWiDgPFOsq6mDg8DDB +0kfbYwKLFFCC9YFmYzR2YrWw2NxAScccUc2chOWAoSNHiqBbHR8ofrlJSWrtmKqd +YRCXzn8wqXnTS3NNHNccqJ6dN+iMr9NGnytw8zwwSchiev53Fpc1mGrJ7BKTWH0t +ZrA6m32wzpMymtKozlOPYoE5mtZEzrzHEXfa44Rns7XIHxVQSXVWyBHLtIsZOrvW +U5F41rQaFEpEeUQ7sQvqUoISfTUVRNDn6GK6YaccEhCji14APLFIvhRQUDyYMIiM +4vll0F/xgVRHTgDVQ8b8sxdhSYlqB4Wc2Ym41YRz+X2yPqk3typEZBpc4P5Tt1/N +89cEIGdbjsA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIQYjbPSg4+RNRD3zNxO1fuKDANBgkqhkiG9w0BAQsFADCB +mDELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChB +bWF6b24gUkRTIGV1LW5vcnRoLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyNDIwNTkyMVoYDzIwNjEwNTI0MjE1OTIxWjCBmDEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChBbWF6 +b24gUkRTIGV1LW5vcnRoLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQHDAdT +ZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA179eQHxcV0YL +XMkqEmhSBazHhnRVd8yICbMq82PitE3BZcnv1Z5Zs/oOgNmMkOKae4tCXO/41JCX +wAgbs/eWWi+nnCfpQ/FqbLPg0h3dqzAgeszQyNl9IzTzX4Nd7JFRBVJXPIIKzlRf ++GmFsAhi3rYgDgO27pz3ciahVSN+CuACIRYnA0K0s9lhYdddmrW/SYeWyoB7jPa2 +LmWpAs7bDOgS4LlP2H3eFepBPgNufRytSQUVA8f58lsE5w25vNiUSnrdlvDrIU5n +Qwzc7NIZCx4qJpRbSKWrUtbyJriWfAkGU7i0IoainHLn0eHp9bWkwb9D+C/tMk1X +ERZw2PDGkwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSFmR7s +dAblusFN+xhf1ae0KUqhWTAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD +ggEBAHsXOpjPMyH9lDhPM61zYdja1ebcMVgfUvsDvt+w0xKMKPhBzYDMs/cFOi1N +Q8LV79VNNfI2NuvFmGygcvTIR+4h0pqqZ+wjWl3Kk5jVxCrbHg3RBX02QLumKd/i +kwGcEtTUvTssn3SM8bgM0/1BDXgImZPC567ciLvWDo0s/Fe9dJJC3E0G7d/4s09n +OMdextcxFuWBZrBm/KK3QF0ByA8MG3//VXaGO9OIeeOJCpWn1G1PjT1UklYhkg61 +EbsTiZVA2DLd1BGzfU4o4M5mo68l0msse/ndR1nEY6IywwpgIFue7+rEleDh6b9d +PYkG1rHVw2I0XDG4o17aOn5E94I= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIQC6W4HFghUkkgyQw14a6JljANBgkqhkiG9w0BAQsFADCB +mDELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChB +bWF6b24gUkRTIGV1LXNvdXRoLTIgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIyMDUyMzE4MTYzMloYDzIwNjIwNTIzMTkxNjMyWjCBmDEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChBbWF6 +b24gUkRTIGV1LXNvdXRoLTIgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQHDAdT +ZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiM/t4FV2R9Nx +UQG203UY83jInTa/6TMq0SPyg617FqYZxvz2kkx09x3dmxepUg9ttGMlPgjsRZM5 +LCFEi1FWk+hxHzt7vAdhHES5tdjwds3aIkgNEillmRDVrUsbrDwufLaa+MMDO2E1 +wQ/JYFXw16WBCCi2g1EtyQ2Xp+tZDX5IWOTnvhZpW8vVDptZ2AcJ5rMhfOYO3OsK +5EF0GGA5ldzuezP+BkrBYGJ4wVKGxeaq9+5AT8iVZrypjwRkD7Y5CurywK3+aBwm +s9Q5Nd8t45JCOUzYp92rFKsCriD86n/JnEvgDfdP6Hvtm0/DkwXK40Wz2q0Zrd0k +mjP054NRPwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRR7yqd +SfKcX2Q8GzhcVucReIpewTAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD +ggEBAEszBRDwXcZyNm07VcFwI1Im94oKwKccuKYeJEsizTBsVon8VpEiMwDs+yGu +3p8kBhvkLwWybkD/vv6McH7T5b9jDX2DoOudqYnnaYeypsPH/00Vh3LvKagqzQza +orWLx+0tLo8xW4BtU+Wrn3JId8LvAhxyYXTn9bm+EwPcStp8xGLwu53OPD1RXYuy +uu+3ps/2piP7GVfou7H6PRaqbFHNfiGg6Y+WA0HGHiJzn8uLmrRJ5YRdIOOG9/xi +qTmAZloUNM7VNuurcMM2hWF494tQpsQ6ysg2qPjbBqzlGoOt3GfBTOZmqmwmqtam +K7juWM/mdMQAJ3SMlE5wI8nVdx4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrjCCAjSgAwIBAgIRAL9SdzVPcpq7GOpvdGoM80IwCgYIKoZIzj0EAwMwgZYx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEvMC0GA1UEAwwmQW1h +em9uIFJEUyBldS13ZXN0LTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjEwNTIwMTY1ODA3WhgPMjEyMTA1MjAxNzU4MDdaMIGWMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExLzAtBgNVBAMMJkFtYXpvbiBS +RFMgZXUtd2VzdC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQHDAdTZWF0dGxl +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEJWDgXebvwjR+Ce+hxKOLbnsfN5W5dOlP +Zn8kwWnD+SLkU81Eac/BDJsXGrMk6jFD1vg16PEkoSevsuYWlC8xR6FmT6F6pmeh +fsMGOyJpfK4fyoEPhKeQoT23lFIc5Orjo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0G +A1UdDgQWBBSVNAN1CHAz0eZ77qz2adeqjm31TzAOBgNVHQ8BAf8EBAMCAYYwCgYI +KoZIzj0EAwMDaAAwZQIxAMlQeHbcjor49jqmcJ9gRLWdEWpXG8thIf6zfYQ/OEAg +d7GDh4fR/OUk0VfjsBUN/gIwZB0bGdXvK38s6AAE/9IT051cz/wMe9GIrX1MnL1T +1F5OqnXJdiwfZRRTHsRQ/L00 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGBDCCA+ygAwIBAgIQalr16vDfX4Rsr+gfQ4iVFDANBgkqhkiG9w0BAQwFADCB +mjELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTMwMQYDVQQDDCpB +bWF6b24gUkRTIGV1LWNlbnRyYWwtMiBSb290IENBIFJTQTQwOTYgRzExEDAOBgNV +BAcMB1NlYXR0bGUwIBcNMjIwNjA2MjEyNTIzWhgPMjEyMjA2MDYyMjI1MjNaMIGa +MQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5j +LjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMzAxBgNVBAMMKkFt +YXpvbiBSRFMgZXUtY2VudHJhbC0yIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UE +BwwHU2VhdHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANbHbFg7 +2VhZor1YNtez0VlNFaobS3PwOMcEn45BE3y7HONnElIIWXGQa0811M8V2FnyqnE8 +Z5aO1EuvijvWf/3D8DPZkdmAkIfh5hlZYY6Aatr65kEOckwIAm7ZZzrwFogYuaFC +z/q0CW+8gxNK+98H/zeFx+IxiVoPPPX6UlrLvn+R6XYNERyHMLNgoZbbS5gGHk43 +KhENVv3AWCCcCc85O4rVd+DGb2vMVt6IzXdTQt6Kih28+RGph+WDwYmf+3txTYr8 +xMcCBt1+whyCPlMbC+Yn/ivtCO4LRf0MPZDRQrqTTrFf0h/V0BGEUmMGwuKgmzf5 +Kl9ILdWv6S956ioZin2WgAxhcn7+z//sN++zkqLreSf90Vgv+A7xPRqIpTdJ/nWG +JaAOUofBfsDsk4X4SUFE7xJa1FZAiu2lqB/E+y7jnWOvFRalzxVJ2Y+D/ZfUfrnK +4pfKtyD1C6ni1celrZrAwLrJ3PoXPSg4aJKh8+CHex477SRsGj8KP19FG8r0P5AG +8lS1V+enFCNvT5KqEBpDZ/Y5SQAhAYFUX+zH4/n4ql0l/emS+x23kSRrF+yMkB9q +lhC/fMk6Pi3tICBjrDQ8XAxv56hfud9w6+/ljYB2uQ1iUYtlE3JdIiuE+3ws26O8 +i7PLMD9zQmo+sVi12pLHfBHQ6RRHtdVRXbXRAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFBFot08ipEL9ZUXCG4lagmF53C0/MA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQwFAAOCAgEAi2mcZi6cpaeqJ10xzMY0F3L2eOKYnlEQ +h6QyhmNKCUF05q5u+cok5KtznzqMwy7TFOZtbVHl8uUX+xvgq/MQCxqFAnuStBXm +gr2dg1h509ZwvTdk7TDxGdftvPCfnPNJBFbMSq4CZtNcOFBg9Rj8c3Yj+Qvwd56V +zWs65BUkDNJrXmxdvhJZjUkMa9vi/oFN+M84xXeZTaC5YDYNZZeW9706QqDbAVES +5ulvKLavB8waLI/lhRBK5/k0YykCMl0A8Togt8D1QsQ0eWWbIM8/HYJMPVFhJ8Wj +vT1p/YVeDA3Bo1iKDOttgC5vILf5Rw1ZEeDxjf/r8A7VS13D3OLjBmc31zxRTs3n +XvHKP9MieQHn9GE44tEYPjK3/yC6BDFzCBlvccYHmqGb+jvDEXEBXKzimdC9mcDl +f4BBQWGJBH5jkbU9p6iti19L/zHhz7qU6UJWbxY40w92L9jS9Utljh4A0LCTjlnR +NQUgjnGC6K+jkw8hj0LTC5Ip87oqoT9w7Av5EJ3VJ4hcnmNMXJJ1DkWYdnytcGpO +DMVITQzzDZRwhbitCVPHagTN2wdi9TEuYE33J0VmFeTc6FSI50wP2aOAZ0Q1/8Aj +bxeM5jS25eaHc2CQAuhrc/7GLnxOcPwdWQb2XWT8eHudhMnoRikVv/KSK3mf6om4 +1YfpdH2jp30= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIQTDc+UgTRtYO7ZGTQ8UWKDDANBgkqhkiG9w0BAQsFADCB +lzELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdB +bWF6b24gUkRTIGV1LXdlc3QtMiBSb290IENBIFJTQTIwNDggRzExEDAOBgNVBAcM +B1NlYXR0bGUwIBcNMjEwNTIxMjI0NjI0WhgPMjA2MTA1MjEyMzQ2MjRaMIGXMQsw +CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET +MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpv +biBSRFMgZXUtd2VzdC0yIFJvb3QgQ0EgUlNBMjA0OCBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM1oGtthQ1YiVIC2 +i4u4swMAGxAjc/BZp0yq0eP5ZQFaxnxs7zFAPabEWsrjeDzrRhdVO0h7zskrertP +gblGhfD20JfjvCHdP1RUhy/nzG+T+hn6Takan/GIgs8grlBMRHMgBYHW7tklhjaH +3F7LujhceAHhhgp6IOrpb6YTaTTaJbF3GTmkqxSJ3l1LtEoWz8Al/nL/Ftzxrtez +Vs6ebpvd7sw37sxmXBWX2OlvUrPCTmladw9OrllGXtCFw4YyLe3zozBlZ3cHzQ0q +lINhpRcajTMfZrsiGCkQtoJT+AqVJPS2sHjqsEH8yiySW9Jbq4zyMbM1yqQ2vnnx +MJgoYMcCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUaQG88UnV +JPTI+Pcti1P+q3H7pGYwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4IB +AQBAkgr75V0sEJimC6QRiTVWEuj2Khy7unjSfudbM6zumhXEU2/sUaVLiYy6cA/x +3v0laDle6T07x9g64j5YastE/4jbzrGgIINFlY0JnaYmR3KZEjgi1s1fkRRf3llL +PJm9u4Q1mbwAMQK/ZjLuuRcL3uRIHJek18nRqT5h43GB26qXyvJqeYYpYfIjL9+/ +YiZAbSRRZG+Li23cmPWrbA1CJY121SB+WybCbysbOXzhD3Sl2KSZRwSw4p2HrFtV +1Prk0dOBtZxCG9luf87ultuDZpfS0w6oNBAMXocgswk24ylcADkkFxBWW+7BETn1 +EpK+t1Lm37mU4sxtuha00XAi +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIQcY44/8NUvBwr6LlHfRy7KjANBgkqhkiG9w0BAQsFADCB +mDELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChB +bWF6b24gUkRTIGV1LXNvdXRoLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUxOTE4MjcxOFoYDzIwNjEwNTE5MTkyNzE4WjCBmDEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChBbWF6 +b24gUkRTIGV1LXNvdXRoLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQHDAdT +ZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0UaBeC+Usalu +EtXnV7+PnH+gi7/71tI/jkKVGKuhD2JDVvqLVoqbMHRh3+wGMvqKCjbHPcC2XMWv +566fpAj4UZ9CLB5fVzss+QVNTl+FH2XhEzigopp+872ajsNzcZxrMkifxGb4i0U+ +t0Zi+UrbL5tsfP2JonKR1crOrbS6/DlzHBjIiJazGOQcMsJjNuTOItLbMohLpraA +/nApa3kOvI7Ufool1/34MG0+wL3UUA4YkZ6oBJVxjZvvs6tI7Lzz/SnhK2widGdc +snbLqBpHNIZQSorVoiwcFaRBGYX/uzYkiw44Yfa4cK2V/B5zgu1Fbr0gbI2am4eh +yVYyg4jPawIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBS9gM1m +IIjyh9O5H/7Vj0R/akI7UzAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD +ggEBAF0Sm9HC2AUyedBVnwgkVXMibnYChOzz7T+0Y+fOLXYAEXex2s8oqGeZdGYX +JHkjBn7JXu7LM+TpTbPbFFDoc1sgMguD/ls+8XsqAl1CssW+amryIL+jfcfbgQ+P +ICwEUD9hGdjBgJ5WcuS+qqxHsEIlFNci3HxcxfBa9VsWs5TjI7Vsl4meL5lf7ZyL +wDV7dHRuU+cImqG1MIvPRIlvPnT7EghrCYi2VCPhP2pM/UvShuwVnkz4MJ29ebIk +WR9kpblFxFdE92D5UUvMCjC2kmtgzNiErvTcwIvOO9YCbBHzRB1fFiWrXUHhJWq9 +IkaxR5icb/IpAV0A1lYZEWMVsfQ= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGATCCA+mgAwIBAgIRAMa0TPL+QgbWfUPpYXQkf8wwDQYJKoZIhvcNAQEMBQAw +gZgxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwo +QW1hem9uIFJEUyBldS1ub3J0aC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UE +BwwHU2VhdHRsZTAgFw0yMTA1MjQyMTAzMjBaGA8yMTIxMDUyNDIyMDMyMFowgZgx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwoQW1h +em9uIFJEUyBldS1ub3J0aC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UEBwwH +U2VhdHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANhS9LJVJyWp +6Rudy9t47y6kzvgnFYDrvJVtgEK0vFn5ifdlHE7xqMz4LZqWBFTnS+3oidwVRqo7 +tqsuuElsouStO8m315/YUzKZEPmkw8h5ufWt/lg3NTCoUZNkB4p4skr7TspyMUwE +VdlKQuWTCOLtofwmWT+BnFF3To6xTh3XPlT3ssancw27Gob8kJegD7E0TSMVsecP +B8je65+3b8CGwcD3QB3kCTGLy87tXuS2+07pncHvjMRMBdDQQQqhXWsRSeUNg0IP +xdHTWcuwMldYPWK5zus9M4dCNBDlmZjKdcZZVUOKeBBAm7Uo7CbJCk8r/Fvfr6mw +nXXDtuWhqn/WhJiI/y0QU27M+Hy5CQMxBwFsfAjJkByBpdXmyYxUgTmMpLf43p7H +oWfH1xN0cT0OQEVmAQjMakauow4AQLNkilV+X6uAAu3STQVFRSrpvMen9Xx3EPC3 +G9flHueTa71bU65Xe8ZmEmFhGeFYHY0GrNPAFhq9RThPRY0IPyCZe0Th8uGejkek +jQjm0FHPOqs5jc8CD8eJs4jSEFt9lasFLVDcAhx0FkacLKQjGHvKAnnbRwhN/dF3 +xt4oL8Z4JGPCLau056gKnYaEyviN7PgO+IFIVOVIdKEBu2ASGE8/+QJB5bcHefNj +04hEkDW0UYJbSfPpVbGAR0gFI/QpycKnAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFFMXvvjoaGGUcul8GA3FT05DLbZcMA4GA1UdDwEB/wQEAwIB +hjANBgkqhkiG9w0BAQwFAAOCAgEAQLwFhd2JKn4K/6salLyIA4mP58qbA/9BTB/r +D9l0bEwDlVPSdY7R3gZCe6v7SWLfA9RjE5tdWDrQMi5IU6W2OVrVsZS/yGJfwnwe +a/9iUAYprA5QYKDg37h12XhVsDKlYCekHdC+qa5WwB1SL3YUprDLPWeaIQdg+Uh2 ++LxvpZGoxoEbca0fc7flwq9ke/3sXt/3V4wJDyY6AL2YNdjFzC+FtYjHHx8rYxHs +aesP7yunuN17KcfOZBBnSFRrx96k+Xm95VReTEEpwiBqAECqEpMbd+R0mFAayMb1 +cE77GaK5yeC2f67NLYGpkpIoPbO9p9rzoXLE5GpSizMjimnz6QCbXPFAFBDfSzim +u6azp40kEUO6kWd7rBhqRwLc43D3TtNWQYxMve5mTRG4Od+eMKwYZmQz89BQCeqm +aZiJP9y9uwJw4p/A5V3lYHTDQqzmbOyhGUk6OdpdE8HXs/1ep1xTT20QDYOx3Ekt +r4mmNYfH/8v9nHNRlYJOqFhmoh1i85IUl5IHhg6OT5ZTTwsGTSxvgQQXrmmHVrgZ +rZIqyBKllCgVeB9sMEsntn4bGLig7CS/N1y2mYdW/745yCLZv2gj0NXhPqgEIdVV +f9DhFD4ohE1C63XP0kOQee+LYg/MY5vH8swpCSWxQgX5icv5jVDz8YTdCKgUc5u8 +rM2p0kk= +-----END CERTIFICATE----- diff --git a/examples/lambda-rds-iam-auth/src/main.rs b/examples/lambda-rds-iam-auth/src/main.rs new file mode 100644 index 000000000..fbf8394f3 --- /dev/null +++ b/examples/lambda-rds-iam-auth/src/main.rs @@ -0,0 +1,98 @@ +use aws_config::BehaviorVersion; +use aws_credential_types::provider::ProvideCredentials; +use aws_sigv4::{ + http_request::{sign, SignableBody, SignableRequest, SigningSettings}, + sign::v4, +}; +use lambda_runtime::{run, service_fn, Error, LambdaEvent}; +use serde_json::{json, Value}; +use sqlx::postgres::PgConnectOptions; +use std::{ + env, + time::{Duration, SystemTime}, +}; + +const RDS_CERTS: &[u8] = include_bytes!("global-bundle.pem"); + +async fn generate_rds_iam_token(db_hostname: &str, port: u16, db_username: &str) -> Result { + let config = aws_config::load_defaults(BehaviorVersion::latest()).await; + + let credentials = config + .credentials_provider() + .expect("no credentials provider found") + .provide_credentials() + .await + .expect("unable to load credentials"); + let identity = credentials.into(); + let region = config.region().unwrap().to_string(); + + let mut signing_settings = SigningSettings::default(); + signing_settings.expires_in = Some(Duration::from_secs(900)); + signing_settings.signature_location = aws_sigv4::http_request::SignatureLocation::QueryParams; + + let signing_params = v4::SigningParams::builder() + .identity(&identity) + .region(®ion) + .name("rds-db") + .time(SystemTime::now()) + .settings(signing_settings) + .build()?; + + let url = format!("https://{db_hostname}:{port}/?Action=connect&DBUser={db_username}"); + + let signable_request = + SignableRequest::new("GET", &url, std::iter::empty(), SignableBody::Bytes(&[])).expect("signable request"); + + let (signing_instructions, _signature) = sign(signable_request, &signing_params.into())?.into_parts(); + + let mut url = url::Url::parse(&url).unwrap(); + for (name, value) in signing_instructions.params() { + url.query_pairs_mut().append_pair(name, value); + } + + let response = url.to_string().split_off("https://".len()); + + Ok(response) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + run(service_fn(handler)).await +} + +async fn handler(_event: LambdaEvent) -> Result { + let db_host = env::var("DB_HOSTNAME").expect("DB_HOSTNAME must be set"); + let db_port = env::var("DB_PORT") + .expect("DB_PORT must be set") + .parse::() + .expect("PORT must be a valid number"); + let db_name = env::var("DB_NAME").expect("DB_NAME must be set"); + let db_user_name = env::var("DB_USERNAME").expect("DB_USERNAME must be set"); + + let token = generate_rds_iam_token(&db_host, db_port, &db_user_name).await?; + + let opts = PgConnectOptions::new() + .host(&db_host) + .port(db_port) + .username(&db_user_name) + .password(&token) + .database(&db_name) + .ssl_root_cert_from_pem(RDS_CERTS.to_vec()) + .ssl_mode(sqlx::postgres::PgSslMode::Require); + + let pool = sqlx::postgres::PgPoolOptions::new().connect_with(opts).await?; + + let result: i32 = sqlx::query_scalar("SELECT $1 + $2") + .bind(3) + .bind(2) + .fetch_one(&pool) + .await?; + + println!("Result: {result:?}"); + + Ok(json!({ + "statusCode": 200, + "content-type": "text/plain", + "body": format!("The selected sum is: {result}") + })) +} diff --git a/examples/opentelemetry-tracing/Cargo.toml b/examples/opentelemetry-tracing/Cargo.toml new file mode 100644 index 000000000..c9b2a9cb2 --- /dev/null +++ b/examples/opentelemetry-tracing/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "opentelemetry-tracing" +version = "0.1.0" +edition = "2021" + +[dependencies] +lambda_runtime = { path = "../../lambda-runtime", features = ["opentelemetry"] } +opentelemetry = "0.30" +opentelemetry_sdk = { version = "0.30", features = ["rt-tokio"] } +opentelemetry-stdout = { version = "0.30", features = ["trace"] } +serde_json = "1.0" +tokio = "1" +tower = "0.5" +tracing = "0.1" +tracing-opentelemetry = "0.31" +tracing-subscriber = "0.3" diff --git a/examples/opentelemetry-tracing/src/main.rs b/examples/opentelemetry-tracing/src/main.rs new file mode 100644 index 000000000..a75aa0168 --- /dev/null +++ b/examples/opentelemetry-tracing/src/main.rs @@ -0,0 +1,46 @@ +use lambda_runtime::{ + layers::{OpenTelemetryFaasTrigger, OpenTelemetryLayer as OtelLayer}, + tracing::Span, + LambdaEvent, Runtime, +}; +use opentelemetry::trace::TracerProvider; +use opentelemetry_sdk::trace; +use tower::{service_fn, BoxError}; +use tracing_subscriber::prelude::*; + +async fn echo(event: LambdaEvent) -> Result { + let span = Span::current(); + span.record("otel.kind", "SERVER"); + Ok(event.payload) +} + +#[tokio::main] +async fn main() -> Result<(), BoxError> { + // Set up OpenTelemetry tracer provider that writes spans to stdout for debugging purposes + let exporter = opentelemetry_stdout::SpanExporter::default(); + let tracer_provider = trace::SdkTracerProvider::builder() + .with_batch_exporter(exporter) + .build(); + + // Set up link between OpenTelemetry and tracing crate + tracing_subscriber::registry() + .with(tracing_opentelemetry::OpenTelemetryLayer::new( + tracer_provider.tracer("my-app"), + )) + .init(); + + // Initialize the Lambda runtime and add OpenTelemetry tracing + let runtime = Runtime::new(service_fn(echo)).layer( + // Create a tracing span for each Lambda invocation + OtelLayer::new(|| { + // Make sure that the trace is exported before the Lambda runtime is frozen + if let Err(err) = tracer_provider.force_flush() { + eprintln!("Error flushing traces: {err:#?}"); + } + }) + // Set the "faas.trigger" attribute of the span to "pubsub" + .with_trigger(OpenTelemetryFaasTrigger::PubSub), + ); + runtime.run().await?; + Ok(()) +} diff --git a/lambda-events/CHANGELOG.md b/lambda-events/CHANGELOG.md new file mode 100644 index 000000000..61fefd7d8 --- /dev/null +++ b/lambda-events/CHANGELOG.md @@ -0,0 +1,56 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.2.1](https://github.com/aws/aws-lambda-rust-runtime/compare/aws_lambda_events-v1.2.0...aws_lambda_events-v1.2.1) - 2026-09-29 + +### Other + +- Add #[serde(default)] to `to` field of SimpleEmailCommonHeaders ([#1171](https://github.com/aws/aws-lambda-rust-runtime/pull/1171)) + +## [1.2.0](https://github.com/aws/aws-lambda-rust-runtime/compare/aws_lambda_events-v1.1.3...aws_lambda_events-v1.2.0) - 2026-05-08 + +### Added + +- Add VPC Lattice as an upstream source for lambda-http ([#1136](https://github.com/aws/aws-lambda-rust-runtime/pull/1136)) + +## [1.1.3](https://github.com/aws/aws-lambda-rust-runtime/compare/aws_lambda_events-v1.1.2...aws_lambda_events-v1.1.3) - 2026-04-16 + +### Other + +- remove non exhaustive from time structs ([#1138](https://github.com/aws/aws-lambda-rust-runtime/pull/1138)) + +## [1.1.2](https://github.com/aws/aws-lambda-rust-runtime/compare/aws_lambda_events-v1.1.1...aws_lambda_events-v1.1.2) - 2026-03-19 + +### Fixed + +- fix handling of null groupConfiguration ([#1130](https://github.com/aws/aws-lambda-rust-runtime/pull/1130)) + +## [1.1.1](https://github.com/aws/aws-lambda-rust-runtime/compare/aws_lambda_events-v1.0.3...aws_lambda_events-v1.1.1) - 2026-03-11 + +### Added + +- *(lambda-events)* support X509 custom authorizer in IoT events ([#1114](https://github.com/aws/aws-lambda-rust-runtime/pull/1114)) +- *(lambda-events)* add Control Tower lifecycle events module ([#1107](https://github.com/aws/aws-lambda-rust-runtime/pull/1107)) +- *(lambda-events)* add VPC Lattice event structures ([#1036](https://github.com/aws/aws-lambda-rust-runtime/pull/1036)) +- Add builder pattern support for event response types ([#1090](https://github.com/aws/aws-lambda-rust-runtime/pull/1090)) + +### Fixed + +- *(sns)* support SubscriptionConfirmation and UnsubscribeConfirmation message types ([#1102](https://github.com/aws/aws-lambda-rust-runtime/pull/1102)) +- *(lambda-events)* `claims_to_add_or_override` type in `CognitoEventUserPoolsPreTokenGenV2` ([#1100](https://github.com/aws/aws-lambda-rust-runtime/pull/1100)) + +### Other + +- *(lambda-events)* deprecate authorizer-specific fields in `ApiGatewayV2httpRequest` ([#1089](https://github.com/aws/aws-lambda-rust-runtime/pull/1089)) + +## [1.0.3](https://github.com/aws/aws-lambda-rust-runtime/compare/aws_lambda_events-v1.0.2...aws_lambda_events-v1.0.3) - 2026-01-06 + +### Changed + +- MSRV updated from 1.82.0 to 1.84.0, enabled MSRV-aware resolver ([#1078](https://github.com/aws/aws-lambda-rust-runtime/pull/1078)) diff --git a/lambda-events/Cargo.toml b/lambda-events/Cargo.toml new file mode 100644 index 000000000..f20987022 --- /dev/null +++ b/lambda-events/Cargo.toml @@ -0,0 +1,140 @@ +[package] +name = "aws_lambda_events" +version = "1.2.1" +rust-version = "1.84.0" +description = "AWS Lambda event definitions" +authors = [ + "Christian Legnitto ", + "Sam Rijs ", + "David Calavera ", +] +license = "MIT" +homepage = "https://github.com/aws/aws-lambda-rust-runtime" +repository = "https://github.com/aws/aws-lambda-rust-runtime" +readme = "README.md" +keywords = ["lambda", "aws", "amazon", "events", "S3"] +categories = ["api-bindings", "encoding", "web-programming"] +edition = "2021" + +[dependencies] +base64 = { workspace = true } +bytes = { workspace = true, features = ["serde"], optional = true } +chrono = { workspace = true, optional = true } +bon = { version = "3", optional = true } +flate2 = { version = "1.0.24", optional = true } +http = { workspace = true, optional = true } +http-body = { workspace = true, optional = true } +http-serde = { workspace = true, optional = true } +query_map = { version = "^0.7", features = [ + "serde", + "url-query", +], optional = true } +serde = { version = "^1", features = ["derive"] } +serde_with = { version = "^3", features = ["json"], optional = true } +serde_json = "^1" +serde_dynamo = { version = "^4.1", optional = true } + +[features] +default = [ + "activemq", + "alb", + "apigw", + "appsync", + "autoscaling", + "bedrock_agent_runtime", + "chime_bot", + "clientvpn", + "cloudformation", + "cloudwatch_alarms", + "cloudwatch_events", + "cloudwatch_logs", + "code_commit", + "codebuild", + "codedeploy", + "codepipeline_cloudwatch", + "codepipeline_job", + "cognito", + "config", + "connect", + "dynamodb", + "ecr_scan", + "firehose", + "iam", + "iot", + "iot_1_click", + "iot_button", + "iot_deprecated", + "kafka", + "kinesis", + "kinesis_analytics", + "lambda_function_urls", + "lex", + "rabbitmq", + "s3", + "s3_batch_job", + "ses", + "secretsmanager", + "sns", + "sqs", + "streams", + "documentdb", + "eventbridge", + "controltower", + "vpc_lattice" +] + +activemq = [] +alb = ["bytes", "http", "http-body", "http-serde", "query_map"] +apigw = ["bytes", "http", "http-body", "http-serde", "iam", "query_map"] +appsync = [] +autoscaling = ["chrono"] +bedrock_agent_runtime = [] +chime_bot = ["chrono"] +clientvpn = [] +cloudformation = [] +cloudwatch_alarms = ["chrono"] +cloudwatch_events = ["chrono"] +cloudwatch_logs = ["flate2"] +code_commit = ["chrono"] +codebuild = ["chrono"] +codedeploy = ["chrono"] +codepipeline = [] +codepipeline_cloudwatch = ["chrono"] +codepipeline_job = [] +cognito = [] +config = [] +connect = [] +controltower = [] +dynamodb = ["chrono", "serde_dynamo", "streams"] +ecr_scan = [] +firehose = ["chrono"] +iam = [] +iot = ["bytes", "http", "http-body", "http-serde", "iam"] +iot_1_click = [] +iot_button = [] +iot_deprecated = ["iot"] +kafka = ["chrono"] +kinesis = ["chrono"] +kinesis_analytics = ["kinesis"] +lambda_function_urls = ["bytes", "http", "http-body", "http-serde"] +lex = [] +rabbitmq = [] +s3 = ["bytes", "chrono", "http", "http-body", "http-serde"] +s3_batch_job = ["s3"] +secretsmanager = [] +ses = ["chrono"] +sns = ["chrono", "serde_with"] +sqs = ["serde_with"] +streams = [] +documentdb = [] +eventbridge = ["chrono", "serde_with"] +vpc_lattice = ["bytes", "http", "http-body", "http-serde", "iam", "query_map"] + +catch-all-fields = [] +builders = ["bon"] + +[package.metadata.docs.rs] +all-features = true + +[dev-dependencies] +lambda_runtime = { version = "1", path = "../lambda-runtime" } diff --git a/lambda-events/LICENSE b/lambda-events/LICENSE new file mode 100644 index 000000000..2329b5731 --- /dev/null +++ b/lambda-events/LICENSE @@ -0,0 +1,23 @@ +MIT License + +Copyright (c) 2018 Sam Rijs and Christian Legnitto +Copyright 2023 Amazon.com, Inc. or its affiliates + + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lambda-events/README.md b/lambda-events/README.md new file mode 100644 index 000000000..c1166cb49 --- /dev/null +++ b/lambda-events/README.md @@ -0,0 +1,84 @@ +# AWS Lambda Events + +[![crates.io][crate-image]][crate-link] +[![Documentation][docs-image]][docs-link] + +This crate provides strongly-typed [AWS Lambda event structs](https://docs.aws.amazon.com/lambda/latest/dg/invoking-lambda-function.html) in Rust. + +## Installation + +Add the dependency with Cargo: `cargo add aws_lambda_events`. + +## Usage + +The crate itself has no AWS Lambda handler logic and instead exists to serialize +and deserialize AWS Lambda events into strongly-typed Rust structs. + +The types +defined in this crate are usually used with handlers / runtimes provided by the [official Rust runtime](https://github.com/aws/aws-lambda-rust-runtime). + +For a list of supported AWS Lambda events and services, see [the crate reference documentation](https://docs.rs/aws_lambda_events). + +## Conditional compilation of features + +This crate divides all Lambda Events into features named after the service that the events are generated from. By default all events are enabled when you include this crate as a dependency to your project. If you only want to import specific events from this crate, you can disable the default features, and enable only the events that you need. This will make your project to compile a little bit faster, since rustc doesn't need to compile events that you're not going to use. Here's an example on how to do that: + +``` +cargo add aws_lambda_events --no-default-features --features apigw,alb +``` + +### Builder pattern support + +The crate provides an optional `builders` feature that adds builder pattern support for event types. This enables type-safe, immutable construction of event responses with a clean, ergonomic API. + +Enable the builders feature: + +``` +cargo add aws_lambda_events --features builders +``` + +Example using builders with API Gateway custom authorizers: + +```rust +use aws_lambda_events::event::apigw::{ + ApiGatewayV2CustomAuthorizerSimpleResponse, + ApiGatewayV2CustomAuthorizerV2Request, +}; +use lambda_runtime::{Error, LambdaEvent}; + +// Context type without Default implementation +struct MyContext { + user_id: String, + permissions: Vec, +} + +async fn handler( + event: LambdaEvent, +) -> Result, Error> { + let context = MyContext { + user_id: "user-123".to_string(), + permissions: vec!["read".to_string()], + }; + + let response = ApiGatewayV2CustomAuthorizerSimpleResponse::builder() + .is_authorized(true) + .context(context) + .build(); + + Ok(response) +} +``` + +See the [examples directory](https://github.com/aws/aws-lambda-rust-runtime/tree/main/lambda-events/examples) for more builder pattern examples. + +## History + +The AWS Lambda Events crate was created by [Christian Legnitto](https://github.com/LegNeato). Without all his work and dedication, this project could have not been possible. + +In 2023, the AWS Lambda Event crate was moved into this repository to continue its support for all AWS customers that use Rust on AWS Lambda. + +[//]: # 'badges' +[crate-image]: https://img.shields.io/crates/v/aws_lambda_events.svg +[crate-link]: https://crates.io/crates/aws_lambda_events +[docs-image]: https://docs.rs/aws_lambda_events/badge.svg +[docs-link]: https://docs.rs/aws_lambda_events \ No newline at end of file diff --git a/lambda-events/lambda-events-examples/Cargo.toml b/lambda-events/lambda-events-examples/Cargo.toml new file mode 100644 index 000000000..216faa082 --- /dev/null +++ b/lambda-events/lambda-events-examples/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "lambda-events-examples" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +aws_lambda_events = { path = "..", features = ["builders"] } +lambda_runtime = { path = "../../lambda-runtime" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +chrono = { version = "0.4", default-features = false, features = ["clock"] } +serde_dynamo = "4" + +[[example]] +name = "comprehensive-builders" +path = "examples/comprehensive-builders.rs" + +[[example]] +name = "lambda-runtime-authorizer-builder" +path = "examples/lambda-runtime-authorizer-builder.rs" + +[[example]] +name = "lambda-runtime-sqs-batch-builder" +path = "examples/lambda-runtime-sqs-batch-builder.rs" diff --git a/lambda-events/lambda-events-examples/examples/comprehensive-builders.rs b/lambda-events/lambda-events-examples/examples/comprehensive-builders.rs new file mode 100644 index 000000000..148594068 --- /dev/null +++ b/lambda-events/lambda-events-examples/examples/comprehensive-builders.rs @@ -0,0 +1,103 @@ +// Example demonstrating builder pattern usage for AWS Lambda events +use aws_lambda_events::event::{ + dynamodb::{Event as DynamoDbEvent, EventRecord as DynamoDbEventRecord, StreamRecord}, + kinesis::{KinesisEvent, KinesisEventRecord, KinesisRecord, KinesisEncryptionType}, + s3::{S3Event, S3EventRecord, S3Entity, S3Bucket, S3Object, S3RequestParameters, S3UserIdentity}, + secretsmanager::SecretsManagerSecretRotationEvent, + sns::{SnsEvent, SnsRecord, SnsMessage}, + sqs::{SqsEvent, SqsMessage}, +}; +use std::collections::HashMap; + +fn main() { + // S3 Event - Object storage notifications with nested structures + let s3_record = S3EventRecord::builder() + .event_time(chrono::Utc::now()) + .principal_id(S3UserIdentity::builder().build()) + .request_parameters(S3RequestParameters::builder().build()) + .response_elements(HashMap::new()) + .s3(S3Entity::builder() + .bucket(S3Bucket::builder().name("my-bucket".to_string()).build()) + .object(S3Object::builder().key("file.txt".to_string()).size(1024).build()) + .build()) + .build(); + let _s3_event = S3Event::builder().records(vec![s3_record]).build(); + + // Kinesis Event - Stream processing with data + let kinesis_record = KinesisEventRecord::builder() + .kinesis(KinesisRecord::builder() + .data(serde_json::from_str("\"SGVsbG8gV29ybGQ=\"").unwrap()) + .partition_key("key-1".to_string()) + .sequence_number("12345".to_string()) + .approximate_arrival_timestamp(serde_json::from_str("1234567890.0").unwrap()) + .encryption_type(KinesisEncryptionType::None) + .build()) + .build(); + let _kinesis_event = KinesisEvent::builder().records(vec![kinesis_record]).build(); + + // DynamoDB Event - Database change streams with item data + let mut keys = HashMap::new(); + keys.insert("id".to_string(), serde_dynamo::AttributeValue::S("123".to_string())); + + let dynamodb_record = DynamoDbEventRecord::builder() + .aws_region("us-east-1".to_string()) + .change(StreamRecord::builder() + .approximate_creation_date_time(chrono::Utc::now()) + .keys(keys.into()) + .new_image(HashMap::new().into()) + .old_image(HashMap::new().into()) + .size_bytes(100) + .build()) + .event_id("event-123".to_string()) + .event_name("INSERT".to_string()) + .build(); + let _dynamodb_event = DynamoDbEvent::builder().records(vec![dynamodb_record]).build(); + + // SNS Event - Pub/sub messaging with message details + let sns_record = SnsRecord::builder() + .event_source("aws:sns".to_string()) + .event_version("1.0".to_string()) + .event_subscription_arn("arn:aws:sns:us-east-1:123456789012:topic".to_string()) + .sns(SnsMessage::builder() + .message("Hello from SNS".to_string()) + .sns_message_type("Notification".to_string()) + .message_id("msg-123".to_string()) + .topic_arn("arn:aws:sns:us-east-1:123456789012:topic".to_string()) + .timestamp(chrono::Utc::now()) + .signature_version("1".to_string()) + .signature("sig".to_string()) + .signing_cert_url("https://cert.url".to_string()) + .unsubscribe_url("https://unsub.url".to_string()) + .message_attributes(HashMap::new()) + .build()) + .build(); + let _sns_event = SnsEvent::builder().records(vec![sns_record]).build(); + + // SQS Event - Queue messaging with attributes + let mut attrs = HashMap::new(); + attrs.insert("ApproximateReceiveCount".to_string(), "1".to_string()); + attrs.insert("SentTimestamp".to_string(), "1234567890".to_string()); + + let sqs_message = SqsMessage::builder() + .attributes(attrs) + .message_attributes(HashMap::new()) + .body("message body".to_string()) + .message_id("msg-456".to_string()) + .build(); + + #[cfg(feature = "catch-all-fields")] + let _sqs_event = SqsEvent::builder() + .records(vec![sqs_message]) + .other(serde_json::Map::new()) + .build(); + + #[cfg(not(feature = "catch-all-fields"))] + let _sqs_event = SqsEvent::builder().records(vec![sqs_message]).build(); + + // Secrets Manager Event - Secret rotation + let _secrets_event = SecretsManagerSecretRotationEvent::builder() + .step("createSecret".to_string()) + .secret_id("test-secret".to_string()) + .client_request_token("token-123".to_string()) + .build(); +} diff --git a/lambda-events/lambda-events-examples/examples/lambda-runtime-authorizer-builder.rs b/lambda-events/lambda-events-examples/examples/lambda-runtime-authorizer-builder.rs new file mode 100644 index 000000000..f92762c90 --- /dev/null +++ b/lambda-events/lambda-events-examples/examples/lambda-runtime-authorizer-builder.rs @@ -0,0 +1,96 @@ +// Example showing how builders work with generic types and custom context structs +// +// Demonstrates: +// 1. Generic types (ApiGatewayV2CustomAuthorizerSimpleResponse) +// 2. Custom context struct WITHOUT Default implementation +// 3. Custom context struct WITH Default implementation + +use aws_lambda_events::event::apigw::{ + ApiGatewayV2CustomAuthorizerSimpleResponse, ApiGatewayV2CustomAuthorizerV2Request, +}; +use lambda_runtime::{Error, LambdaEvent}; +use serde::{Deserialize, Serialize}; + +// Custom context WITHOUT Default - requires builder pattern +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContextWithoutDefault { + pub user_id: String, + pub api_key: String, + pub permissions: Vec, +} + +// Custom context WITH Default - works both ways +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct ContextWithDefault { + pub user_id: String, + pub role: String, +} + +// Handler using context WITHOUT Default - builder pattern required +pub async fn handler_without_default( + _event: LambdaEvent, +) -> Result, Error> { + let context = ContextWithoutDefault { + user_id: "user-123".to_string(), + api_key: "secret-key".to_string(), + permissions: vec!["read".to_string()], + }; + + let response = ApiGatewayV2CustomAuthorizerSimpleResponse::builder() + .is_authorized(true) + .context(context) + .build(); + + Ok(response) +} + +// Handler using context WITH Default - builder pattern still preferred +pub async fn handler_with_default( + _event: LambdaEvent, +) -> Result, Error> { + let context = ContextWithDefault { + user_id: "user-456".to_string(), + role: "admin".to_string(), + }; + + let response = ApiGatewayV2CustomAuthorizerSimpleResponse::builder() + .is_authorized(true) + .context(context) + .build(); + + Ok(response) +} + +fn main() { + // Example 1: Context WITHOUT Default + let context_no_default = ContextWithoutDefault { + user_id: "user-123".to_string(), + api_key: "secret-key".to_string(), + permissions: vec!["read".to_string(), "write".to_string()], + }; + + let response1 = ApiGatewayV2CustomAuthorizerSimpleResponse::builder() + .is_authorized(true) + .context(context_no_default) + .build(); + + println!("Response with context WITHOUT Default:"); + println!(" User: {}", response1.context.user_id); + println!(" Authorized: {}", response1.is_authorized); + + // Example 2: Context WITH Default + let context_with_default = ContextWithDefault { + user_id: "user-456".to_string(), + role: "admin".to_string(), + }; + + let response2 = ApiGatewayV2CustomAuthorizerSimpleResponse::builder() + .is_authorized(false) + .context(context_with_default) + .build(); + + println!("\nResponse with context WITH Default:"); + println!(" User: {}", response2.context.user_id); + println!(" Role: {}", response2.context.role); + println!(" Authorized: {}", response2.is_authorized); +} diff --git a/lambda-events/lambda-events-examples/examples/lambda-runtime-sqs-batch-builder.rs b/lambda-events/lambda-events-examples/examples/lambda-runtime-sqs-batch-builder.rs new file mode 100644 index 000000000..7330402d0 --- /dev/null +++ b/lambda-events/lambda-events-examples/examples/lambda-runtime-sqs-batch-builder.rs @@ -0,0 +1,64 @@ +// Example showing how builders simplify SQS batch response construction +// when handling partial batch failures + +use aws_lambda_events::event::sqs::{BatchItemFailure, SqsBatchResponse, SqsEvent}; +use lambda_runtime::{Error, LambdaEvent}; + +// Simulate processing a record +#[allow(dead_code)] +async fn process_record(record: &aws_lambda_events::event::sqs::SqsMessage) -> Result<(), String> { + // Simulate some processing logic + if let Some(body) = &record.body { + if body.contains("error") { + return Err(format!("Failed to process message: {}", body)); + } + } + Ok(()) +} + +// Lambda handler using builder pattern +#[allow(dead_code)] +pub async fn function_handler(event: LambdaEvent) -> Result { + let mut batch_item_failures = Vec::new(); + + for record in event.payload.records { + match process_record(&record).await { + Ok(_) => (), + Err(_) => { + let item = BatchItemFailure::builder() + .item_identifier(record.message_id.unwrap()) + .build(); + + batch_item_failures.push(item) + } + } + } + + let response = SqsBatchResponse::builder() + .batch_item_failures(batch_item_failures) + .build(); + + Ok(response) +} + +fn main() { + // Demonstrate builder usage with sample data + let failures = vec![ + BatchItemFailure::builder() + .item_identifier("msg-123".to_string()) + .build(), + BatchItemFailure::builder() + .item_identifier("msg-456".to_string()) + .build(), + ]; + + let response = SqsBatchResponse::builder().batch_item_failures(failures).build(); + + println!( + "Built SQS batch response with {} failed items", + response.batch_item_failures.len() + ); + for failure in &response.batch_item_failures { + println!(" Failed message: {}", failure.item_identifier); + } +} diff --git a/lambda-events/src/custom_serde/codebuild_time.rs b/lambda-events/src/custom_serde/codebuild_time.rs new file mode 100644 index 000000000..8d90203fc --- /dev/null +++ b/lambda-events/src/custom_serde/codebuild_time.rs @@ -0,0 +1,109 @@ +use chrono::{DateTime, NaiveDateTime, Utc}; +use serde::{ + de::{Deserializer, Error as DeError, Visitor}, + ser::Serializer, + Deserialize, +}; +use std::fmt; + +// Jan 2, 2006 3:04:05 PM +const CODEBUILD_TIME_FORMAT: &str = "%b %e, %Y %l:%M:%S %p"; + +struct TimeVisitor; +impl Visitor<'_> for TimeVisitor { + type Value = DateTime; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "valid codebuild time: {CODEBUILD_TIME_FORMAT}") + } + + fn visit_str(self, val: &str) -> Result { + NaiveDateTime::parse_from_str(val, CODEBUILD_TIME_FORMAT) + .map(|naive| naive.and_utc()) + .map_err(|e| DeError::custom(format!("Parse error {e} for {val}"))) + } +} + +pub(crate) mod str_time { + use super::*; + + pub(crate) fn deserialize<'de, D>(d: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + d.deserialize_str(TimeVisitor) + } + + pub fn serialize(date: &DateTime, ser: S) -> Result { + let s = format!("{}", date.format(CODEBUILD_TIME_FORMAT)); + ser.serialize_str(&s) + } +} + +pub(crate) mod optional_time { + use super::*; + + pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> + where + D: Deserializer<'de>, + { + let s: Option = Option::deserialize(deserializer)?; + if let Some(val) = s { + let visitor = TimeVisitor {}; + return visitor.visit_str(&val).map(Some); + } + + Ok(None) + } + + pub fn serialize(date: &Option>, ser: S) -> Result { + if let Some(date) = date { + return str_time::serialize(date, ser); + } + + ser.serialize_none() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + type TestTime = DateTime; + + #[test] + fn test_deserialize_codebuild_time() { + #[derive(Deserialize)] + struct Test { + #[serde(with = "str_time")] + pub date: TestTime, + } + let data = serde_json::json!({ + "date": "Sep 1, 2017 4:12:29 PM" + }); + + let expected = NaiveDateTime::parse_from_str("Sep 1, 2017 4:12:29 PM", CODEBUILD_TIME_FORMAT) + .unwrap() + .and_utc(); + let decoded: Test = serde_json::from_value(data).unwrap(); + assert_eq!(expected, decoded.date); + } + + #[test] + fn test_deserialize_codebuild_optional_time() { + #[derive(Deserialize)] + struct Test { + #[serde(with = "optional_time")] + pub date: Option, + } + let data = serde_json::json!({ + "date": "Sep 1, 2017 4:12:29 PM" + }); + + let expected = NaiveDateTime::parse_from_str("Sep 1, 2017 4:12:29 PM", CODEBUILD_TIME_FORMAT) + .unwrap() + .and_utc(); + let decoded: Test = serde_json::from_value(data).unwrap(); + assert_eq!(Some(expected), decoded.date); + } +} diff --git a/lambda-events/src/custom_serde/float_unix_epoch.rs b/lambda-events/src/custom_serde/float_unix_epoch.rs new file mode 100644 index 000000000..164b4c7e6 --- /dev/null +++ b/lambda-events/src/custom_serde/float_unix_epoch.rs @@ -0,0 +1,106 @@ +use serde::{de, ser}; +use std::fmt; + +use chrono::{offset::TimeZone, DateTime, LocalResult, Utc}; + +enum SerdeError { + NonExistent { timestamp: V }, + Ambiguous { timestamp: V, min: D, max: D }, +} + +fn ne_timestamp(ts: T) -> SerdeError { + SerdeError::NonExistent:: { timestamp: ts } +} + +impl fmt::Debug for SerdeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "ChronoSerdeError({self})") + } +} + +impl fmt::Display for SerdeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + SerdeError::NonExistent { ref timestamp } => { + write!(f, "value is not a legal timestamp: {timestamp}") + } + SerdeError::Ambiguous { + ref timestamp, + ref min, + ref max, + } => write!( + f, + "value is an ambiguous timestamp: {timestamp}, could be either of {min}, {max}", + ), + } + } +} + +fn serde_from(me: LocalResult, ts: &V) -> Result +where + E: de::Error, + V: fmt::Display, + T: fmt::Display, +{ + match me { + LocalResult::None => Err(E::custom(ne_timestamp(ts))), + LocalResult::Ambiguous(min, max) => Err(E::custom(SerdeError::Ambiguous { + timestamp: ts, + min, + max, + })), + LocalResult::Single(val) => Ok(val), + } +} + +struct SecondsFloatTimestampVisitor; + +/// Serialize a UTC datetime into an float number of seconds since the epoch +pub fn serialize(dt: &DateTime, serializer: S) -> Result +where + S: ser::Serializer, +{ + serializer.serialize_i64(dt.timestamp_millis() / 1000) +} + +/// Deserialize a `DateTime` from a float seconds timestamp +pub fn deserialize<'de, D>(d: D) -> Result, D::Error> +where + D: de::Deserializer<'de>, +{ + d.deserialize_f64(SecondsFloatTimestampVisitor) +} + +impl de::Visitor<'_> for SecondsFloatTimestampVisitor { + type Value = DateTime; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a unix timestamp as a float") + } + + /// Deserialize a timestamp in seconds since the epoch + fn visit_u64(self, value: u64) -> Result, E> + where + E: de::Error, + { + serde_from(Utc.timestamp_opt(value as i64, 0), &value) + } + + /// Deserialize a timestamp in seconds since the epoch + fn visit_i64(self, value: i64) -> Result, E> + where + E: de::Error, + { + serde_from(Utc.timestamp_opt(value, 0), &value) + } + + /// Deserialize a timestamp in seconds since the epoch + fn visit_f64(self, value: f64) -> Result, E> + where + E: de::Error, + { + let time_ms = (value.fract() * 1_000_000.).floor() as u32; + let time_s = value.trunc() as i64; + serde_from(Utc.timestamp_opt(time_s, time_ms), &value) + } +} diff --git a/lambda-events/src/custom_serde/headers.rs b/lambda-events/src/custom_serde/headers.rs new file mode 100644 index 000000000..b343180df --- /dev/null +++ b/lambda-events/src/custom_serde/headers.rs @@ -0,0 +1,294 @@ +use http::{header::HeaderName, HeaderMap, HeaderValue}; +use serde::{ + de::{self, Deserializer, Error as DeError, MapAccess, Unexpected, Visitor}, + ser::{Error as SerError, SerializeMap, Serializer}, +}; +use std::{borrow::Cow, fmt}; + +/// Serialize a http::HeaderMap into a serde str => `Vec` map +pub(crate) fn serialize_multi_value_headers(headers: &HeaderMap, serializer: S) -> Result +where + S: Serializer, +{ + let mut map = serializer.serialize_map(Some(headers.keys_len()))?; + for key in headers.keys() { + let mut map_values = Vec::new(); + for value in headers.get_all(key) { + map_values.push(String::from_utf8(value.as_bytes().to_vec()).map_err(S::Error::custom)?) + } + map.serialize_entry(key.as_str(), &map_values)?; + } + map.end() +} + +/// Serialize a http::HeaderMap into a serde str => str map +pub(crate) fn serialize_headers(headers: &HeaderMap, serializer: S) -> Result +where + S: Serializer, +{ + let mut map = serializer.serialize_map(Some(headers.keys_len()))?; + for key in headers.keys() { + let map_value = String::from_utf8(headers[key].as_bytes().to_vec()).map_err(S::Error::custom)?; + map.serialize_entry(key.as_str(), &map_value)?; + } + map.end() +} + +/// Serialize a HeaderMap with multiple values per header combined as comma-separated strings. +/// Used by VPC Lattice V1 which expects multi-value headers as "value1, value2". +#[cfg(feature = "vpc_lattice")] +pub(crate) fn serialize_comma_separated_headers(headers: &HeaderMap, serializer: S) -> Result +where + S: Serializer, +{ + let mut map = serializer.serialize_map(Some(headers.keys_len()))?; + for key in headers.keys() { + let values: Vec<&str> = headers.get_all(key).iter().filter_map(|v| v.to_str().ok()).collect(); + if !values.is_empty() { + let combined_value = values.join(", "); + map.serialize_entry(key.as_str(), &combined_value)?; + } + } + map.end() +} + +#[derive(serde::Deserialize)] +#[serde(untagged)] +enum OneOrMore<'a> { + One(Cow<'a, str>), + Strings(Vec>), + Bytes(Vec>), +} + +struct HeaderMapVisitor { + is_human_readable: bool, + split_comma_separated: bool, +} + +impl<'de> Visitor<'de> for HeaderMapVisitor { + type Value = HeaderMap; + + // Format a message stating what data this Visitor expects to receive. + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("lots of things can go wrong with HeaderMap") + } + + fn visit_unit(self) -> Result + where + E: DeError, + { + Ok(HeaderMap::default()) + } + + fn visit_none(self) -> Result + where + E: DeError, + { + Ok(HeaderMap::default()) + } + + fn visit_some(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_map(self) + } + + fn visit_map(self, mut access: M) -> Result + where + M: MapAccess<'de>, + { + let mut map = HeaderMap::with_capacity(access.size_hint().unwrap_or(0)); + + if !self.is_human_readable { + while let Some((key, arr)) = access.next_entry::, Vec>>()? { + let key = HeaderName::from_bytes(key.as_bytes()) + .map_err(|_| de::Error::invalid_value(Unexpected::Str(&key), &self))?; + for val in arr { + let val = HeaderValue::from_bytes(&val) + .map_err(|_| de::Error::invalid_value(Unexpected::Bytes(&val), &self))?; + map.append(&key, val); + } + } + } else { + while let Some((key, val)) = access.next_entry::, OneOrMore<'_>>()? { + let key = HeaderName::from_bytes(key.as_bytes()) + .map_err(|_| de::Error::invalid_value(Unexpected::Str(&key), &self))?; + match val { + OneOrMore::One(val) => { + if self.split_comma_separated && val.contains(',') { + split_and_append_header(&mut map, &key, &val, &self)?; + } else { + let val = val + .parse() + .map_err(|_| de::Error::invalid_value(Unexpected::Str(&val), &self))?; + map.insert(key, val); + } + } + OneOrMore::Strings(arr) => { + for val in arr { + if self.split_comma_separated && val.contains(',') { + split_and_append_header(&mut map, &key, &val, &self)?; + } else { + let val = val + .parse() + .map_err(|_| de::Error::invalid_value(Unexpected::Str(&val), &self))?; + map.append(&key, val); + } + } + } + OneOrMore::Bytes(arr) => { + for val in arr { + let val = HeaderValue::from_bytes(&val) + .map_err(|_| de::Error::invalid_value(Unexpected::Bytes(&val), &self))?; + map.append(&key, val); + } + } + }; + } + } + Ok(map) + } +} + +fn split_and_append_header( + map: &mut HeaderMap, + key: &HeaderName, + value: &str, + visitor: &HeaderMapVisitor, +) -> Result<(), E> +where + E: DeError, +{ + for split_val in value.split(',') { + let trimmed_val = split_val.trim(); + if !trimmed_val.is_empty() { + let header_val = trimmed_val + .parse() + .map_err(|_| de::Error::invalid_value(Unexpected::Str(trimmed_val), visitor))?; + map.append(key, header_val); + } + } + Ok(()) +} + +/// Implementation detail. +pub(crate) fn deserialize_headers<'de, D>(de: D) -> Result +where + D: Deserializer<'de>, +{ + let is_human_readable = de.is_human_readable(); + de.deserialize_option(HeaderMapVisitor { + is_human_readable, + split_comma_separated: false, + }) +} + +/// Deserialize headers, splitting comma-separated values into multiple header values. +/// Used by VPC Lattice V1 which sends multi-value headers as "value1, value2". +#[cfg(feature = "vpc_lattice")] +pub(crate) fn deserialize_comma_separated_headers<'de, D>(de: D) -> Result +where + D: Deserializer<'de>, +{ + let is_human_readable = de.is_human_readable(); + de.deserialize_option(HeaderMapVisitor { + is_human_readable, + split_comma_separated: true, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::{Deserialize, Serialize}; + + #[test] + fn test_deserialize_missing_http_headers() { + #[derive(Deserialize)] + struct Test { + #[serde(deserialize_with = "deserialize_headers", default)] + pub headers: HeaderMap, + } + let data = serde_json::json!({ + "not_headers": {} + }); + + let expected = HeaderMap::new(); + + let decoded: Test = serde_json::from_value(data).unwrap(); + assert_eq!(expected, decoded.headers); + } + + #[test] + fn test_serialize_headers() { + #[derive(Deserialize, Serialize)] + struct Test { + #[serde(deserialize_with = "deserialize_headers", default)] + #[serde(serialize_with = "serialize_multi_value_headers")] + headers: HeaderMap, + } + let data = serde_json::json!({ + "headers": { + "Accept": ["*/*"] + } + }); + let decoded: Test = serde_json::from_value(data).unwrap(); + assert_eq!(&"*/*", decoded.headers.get("Accept").unwrap()); + + let recoded = serde_json::to_value(decoded).unwrap(); + let decoded: Test = serde_json::from_value(recoded).unwrap(); + assert_eq!(&"*/*", decoded.headers.get("Accept").unwrap()); + } + + #[test] + fn test_null_headers() { + #[derive(Deserialize)] + struct Test { + #[serde(deserialize_with = "deserialize_headers")] + headers: HeaderMap, + } + let data = serde_json::json!({ "headers": null }); + + let decoded: Test = serde_json::from_value(data).unwrap(); + assert!(decoded.headers.is_empty()); + } + + #[test] + fn test_serialize_utf8_headers() { + #[derive(Deserialize, Serialize)] + struct Test { + #[serde(deserialize_with = "deserialize_headers", default)] + #[serde(serialize_with = "serialize_headers")] + pub headers: HeaderMap, + #[serde(deserialize_with = "deserialize_headers", default)] + #[serde(serialize_with = "serialize_multi_value_headers")] + pub multi_value_headers: HeaderMap, + } + + let content_disposition = + "inline; filename=\"Schillers schönste Szenenanweisungen -Kabale und Liebe.mp4.avif\""; + let data = serde_json::json!({ + "headers": { + "Content-Disposition": content_disposition + }, + "multi_value_headers": { + "Content-Disposition": content_disposition + } + }); + let decoded: Test = serde_json::from_value(data).unwrap(); + assert_eq!(content_disposition, decoded.headers.get("Content-Disposition").unwrap()); + assert_eq!( + content_disposition, + decoded.multi_value_headers.get("Content-Disposition").unwrap() + ); + + let recoded = serde_json::to_value(decoded).unwrap(); + let decoded: Test = serde_json::from_value(recoded).unwrap(); + assert_eq!(content_disposition, decoded.headers.get("Content-Disposition").unwrap()); + assert_eq!( + content_disposition, + decoded.multi_value_headers.get("Content-Disposition").unwrap() + ); + } +} diff --git a/lambda-events/src/custom_serde/http_method.rs b/lambda-events/src/custom_serde/http_method.rs new file mode 100644 index 000000000..1332c7775 --- /dev/null +++ b/lambda-events/src/custom_serde/http_method.rs @@ -0,0 +1,106 @@ +use http::Method; +use serde::{ + de::{Deserialize, Deserializer, Error as DeError, Unexpected, Visitor}, + ser::Serializer, +}; +use std::fmt; + +pub fn serialize(method: &Method, ser: S) -> Result { + ser.serialize_str(method.as_str()) +} + +struct MethodVisitor; +impl Visitor<'_> for MethodVisitor { + type Value = Method; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "valid method name") + } + + fn visit_str(self, val: &str) -> Result { + if val.is_empty() { + Ok(Method::GET) + } else { + val.parse() + .map_err(|_| DeError::invalid_value(Unexpected::Str(val), &self)) + } + } +} + +pub fn deserialize<'de, D>(de: D) -> Result +where + D: Deserializer<'de>, +{ + de.deserialize_str(MethodVisitor) +} + +pub fn deserialize_optional<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s: Option = Option::deserialize(deserializer)?; + if let Some(val) = s { + let visitor = MethodVisitor {}; + return visitor.visit_str(&val).map(Some); + } + + Ok(None) +} + +pub fn serialize_optional(method: &Option, ser: S) -> Result { + if let Some(method) = method { + return serialize(method, ser); + } + + ser.serialize_none() +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::{Deserialize, Serialize}; + + #[test] + fn test_http_method_serializer() { + #[derive(Deserialize, Serialize)] + struct Test { + #[serde(with = "crate::custom_serde::http_method")] + pub method: http::Method, + } + let data = serde_json::json!({ + "method": "DELETE" + }); + let decoded: Test = serde_json::from_value(data.clone()).unwrap(); + assert_eq!(http::Method::DELETE, decoded.method); + + let recoded = serde_json::to_value(decoded).unwrap(); + assert_eq!(data, recoded); + } + + #[test] + fn test_http_optional_method_serializer() { + #[derive(Deserialize, Serialize)] + struct Test { + #[serde(deserialize_with = "deserialize_optional")] + #[serde(serialize_with = "serialize_optional")] + #[serde(default)] + pub method: Option, + } + let data = serde_json::json!({ + "method": "DELETE" + }); + let decoded: Test = serde_json::from_value(data.clone()).unwrap(); + assert_eq!(Some(http::Method::DELETE), decoded.method); + + let recoded = serde_json::to_value(decoded).unwrap(); + assert_eq!(data, recoded); + + let data = serde_json::json!({ "method": null }); + let decoded: Test = serde_json::from_value(data).unwrap(); + assert_eq!(None, decoded.method); + + let data = serde_json::json!({}); + let decoded: Test = serde_json::from_value(data).unwrap(); + assert_eq!(None, decoded.method); + } +} diff --git a/lambda-events/src/custom_serde/mod.rs b/lambda-events/src/custom_serde/mod.rs new file mode 100644 index 000000000..d7b340f27 --- /dev/null +++ b/lambda-events/src/custom_serde/mod.rs @@ -0,0 +1,203 @@ +use base64::Engine; +use serde::{ + de::{Deserialize, Deserializer, Error as DeError}, + ser::Serializer, +}; + +#[cfg(feature = "codebuild")] +pub(crate) mod codebuild_time; +#[cfg(feature = "codebuild")] +#[cfg_attr(docsrs, doc(cfg(feature = "codebuild")))] +pub type CodeBuildNumber = f32; + +#[cfg(any( + feature = "alb", + feature = "apigw", + feature = "s3", + feature = "iot", + feature = "lambda_function_urls", + feature = "vpc_lattice" +))] +mod headers; +#[cfg(any( + feature = "alb", + feature = "apigw", + feature = "s3", + feature = "iot", + feature = "lambda_function_urls", + feature = "vpc_lattice" +))] +pub(crate) use self::headers::*; + +#[cfg(feature = "dynamodb")] +pub(crate) mod float_unix_epoch; + +#[cfg(any(feature = "alb", feature = "apigw", feature = "vpc_lattice"))] +pub(crate) mod http_method; + +#[cfg(feature = "alb")] +mod query_string_parameters; +#[cfg(feature = "alb")] +pub(crate) use self::query_string_parameters::*; + +pub(crate) fn deserialize_base64<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s: String = String::deserialize(deserializer)?; + base64::engine::general_purpose::STANDARD + .decode(s) + .map_err(DeError::custom) +} + +pub(crate) fn serialize_base64(value: &[u8], serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&base64::engine::general_purpose::STANDARD.encode(value)) +} + +/// Deserializes any `Default` type, mapping JSON `null` to `T::default()`. +/// +/// **Note** null-to-empty semantics are usually clear for container types (Map, Vec, etc). +/// For most other data types, prefer modeling fields as ```Option``` with #[serde(default)] +/// instead of using this deserializer. Option preserves information about the message +/// for the application, and default semantics for the target data type may change +/// over time without warning. +pub(crate) fn deserialize_nullish<'de, D, T>(deserializer: D) -> Result +where + D: Deserializer<'de>, + T: Default + Deserialize<'de>, +{ + let opt = Option::deserialize(deserializer)?; + Ok(opt.unwrap_or_default()) +} + +#[cfg(test)] +#[allow(deprecated)] +mod test { + use super::*; + + use serde::{Deserialize, Serialize}; + use std::collections::HashMap; + + #[test] + fn test_deserialize_base64() { + #[derive(Deserialize)] + struct Test { + #[serde(deserialize_with = "deserialize_base64")] + v: Vec, + } + let data = serde_json::json!({ + "v": "SGVsbG8gV29ybGQ=", + }); + let decoded: Test = serde_json::from_value(data).unwrap(); + assert_eq!(String::from_utf8(decoded.v).unwrap(), "Hello World".to_string()); + } + + #[test] + fn test_serialize_base64() { + #[derive(Serialize)] + struct Test { + #[serde(serialize_with = "serialize_base64")] + v: Vec, + } + let instance = Test { + v: "Hello World".as_bytes().to_vec(), + }; + let encoded = serde_json::to_string(&instance).unwrap(); + assert_eq!(encoded, r#"{"v":"SGVsbG8gV29ybGQ="}"#.to_string()); + } + + #[test] + fn test_deserialize_map() { + #[derive(Deserialize)] + struct Test { + #[serde(deserialize_with = "deserialize_nullish")] + v: HashMap, + } + let input = serde_json::json!({ + "v": {}, + }); + let decoded: Test = serde_json::from_value(input).unwrap(); + assert_eq!(HashMap::new(), decoded.v); + + let input = serde_json::json!({ + "v": null, + }); + let decoded: Test = serde_json::from_value(input).unwrap(); + assert_eq!(HashMap::new(), decoded.v); + } + + #[cfg(feature = "dynamodb")] + #[test] + fn test_deserialize_lambda_dynamodb_item() { + #[derive(Deserialize, Debug)] + struct Test { + #[serde(deserialize_with = "deserialize_nullish")] + v: serde_dynamo::Item, + } + let input = serde_json::json!({ + "v": {}, + }); + let decoded: Test = serde_json::from_value(input).unwrap(); + assert_eq!(serde_dynamo::Item::from(HashMap::new()), decoded.v); + + let input = serde_json::json!({ + "v": null, + }); + let decoded: Test = serde_json::from_value(input).unwrap(); + assert_eq!(serde_dynamo::Item::from(HashMap::new()), decoded.v); + + let input = serde_json::json!({}); + let failure = serde_json::from_value::(input); + assert!(failure.is_err(), "Missing field should not default: {failure:?}") + } + + #[test] + fn test_deserialize_nullish() { + #[derive(Debug, Default, Deserialize, PartialEq)] + struct Inner { + x: u32, + } + #[derive(Deserialize)] + struct Test { + #[serde(default, deserialize_with = "deserialize_nullish")] + v: Inner, + } + + let decoded: Test = serde_json::from_str(r#"{"v": null}"#).unwrap(); + assert_eq!(decoded.v, Inner::default()); + + let decoded: Test = serde_json::from_str(r#"{}"#).unwrap(); + assert_eq!(decoded.v, Inner::default()); + + let decoded: Test = serde_json::from_str(r#"{"v": {"x": 42}}"#).unwrap(); + assert_eq!(decoded.v, Inner { x: 42 }); + } + + #[test] + fn test_deserialize_nullish_boolean() { + #[derive(Deserialize)] + struct Test { + #[serde(default, deserialize_with = "deserialize_nullish")] + v: bool, + } + + let test = r#"{"v": null}"#; + let decoded: Test = serde_json::from_str(test).unwrap(); + assert!(!decoded.v); + + let test = r#"{}"#; + let decoded: Test = serde_json::from_str(test).unwrap(); + assert!(!decoded.v); + + let test = r#"{"v": true}"#; + let decoded: Test = serde_json::from_str(test).unwrap(); + assert!(decoded.v); + + let test = r#"{"v": false}"#; + let decoded: Test = serde_json::from_str(test).unwrap(); + assert!(!decoded.v); + } +} diff --git a/lambda-events/src/custom_serde/query_string_parameters.rs b/lambda-events/src/custom_serde/query_string_parameters.rs new file mode 100644 index 000000000..a7144edab --- /dev/null +++ b/lambda-events/src/custom_serde/query_string_parameters.rs @@ -0,0 +1,63 @@ +use query_map::QueryMap; +use serde::{ser::SerializeMap, Serializer}; +use std::collections::HashMap; + +/// Serializes `QueryMap`, converting value from `Vec` to `String` using the last value. +pub fn serialize_query_string_parameters(value: &QueryMap, serializer: S) -> Result +where + S: Serializer, +{ + let mut query_string_parameters = HashMap::new(); + + if let Some((mut last_key, mut last_value)) = value.iter().next() { + // insert the last value for each key + value.iter().for_each(|(k, v)| { + if k != last_key { + query_string_parameters.insert(last_key, last_value); + last_key = k; + } + last_value = v; + }); + // insert the last pair + query_string_parameters.insert(last_key, last_value); + } + + let mut map = serializer.serialize_map(Some(query_string_parameters.len()))?; + for (k, v) in &query_string_parameters { + map.serialize_entry(k, v)?; + } + map.end() +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::Serialize; + use serde_json::Value; + + #[test] + fn test_serialize_query_string_parameters() { + #[derive(Serialize)] + struct Test { + #[serde(serialize_with = "serialize_query_string_parameters")] + pub v: QueryMap, + } + + fn s(value: &str) -> String { + value.to_string() + } + + let query = QueryMap::from(HashMap::from([ + (s("key1"), vec![s("value1"), s("value2"), s("value3")]), + (s("key2"), vec![s("value4")]), + (s("key3"), vec![s("value5"), s("value6")]), + ])); + + let serialized = serde_json::to_string(&Test { v: query }).unwrap(); + let parsed: Value = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(parsed["v"]["key1"], Value::String("value3".to_string())); + assert_eq!(parsed["v"]["key2"], Value::String("value4".to_string())); + assert_eq!(parsed["v"]["key3"], Value::String("value6".to_string())); + } +} diff --git a/lambda-events/src/encodings/http.rs b/lambda-events/src/encodings/http.rs new file mode 100644 index 000000000..32137a759 --- /dev/null +++ b/lambda-events/src/encodings/http.rs @@ -0,0 +1,337 @@ +use base64::display::Base64Display; +use bytes::Bytes; +use http_body::{Body as HttpBody, SizeHint}; +use serde::{ + de::{Deserialize, Deserializer, Error as DeError, Visitor}, + ser::{Error as SerError, Serialize, Serializer}, +}; +use std::{borrow::Cow, mem::take, ops::Deref, pin::Pin, task::Poll}; + +/// Representation of http request and response bodies as supported +/// by API Gateway and ALBs. +/// +/// These come in three flavors +/// * `Empty` ( no body ) +/// * `Text` ( text data ) +/// * `Binary` ( binary data ) +/// +/// Body types can be `Deref` and `AsRef`'d into `[u8]` types much like the [hyper crate](https://crates.io/crates/hyper) +/// +/// # Examples +/// +/// Body types are inferred with `From` implementations. +/// +/// ## Text +/// +/// Types like `String`, `str` whose type reflects +/// text produce `Body::Text` variants +/// +/// ``` +/// assert!(match aws_lambda_events::encodings::Body::from("text") { +/// aws_lambda_events::encodings::Body::Text(_) => true, +/// _ => false +/// }) +/// ``` +/// +/// ## Binary +/// +/// Types like `Vec` and `&[u8]` whose types reflect raw bytes produce `Body::Binary` variants +/// +/// ``` +/// assert!(match aws_lambda_events::encodings::Body::from("text".as_bytes()) { +/// aws_lambda_events::encodings::Body::Binary(_) => true, +/// _ => false +/// }) +/// ``` +/// +/// `Binary` responses bodies will automatically get based64 encoded to meet API Gateway's response expectations. +/// +/// ## Empty +/// +/// The unit type ( `()` ) whose type represents an empty value produces `Body::Empty` variants +/// +/// ``` +/// assert!(match aws_lambda_events::encodings::Body::from(()) { +/// aws_lambda_events::encodings::Body::Empty => true, +/// _ => false +/// }) +/// ``` +/// +/// +/// For more information about API Gateway's body types, +/// refer to [this documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-payload-encodings.html). +#[non_exhaustive] +#[derive(Debug, Default, Eq, PartialEq)] +pub enum Body { + /// An empty body + #[default] + Empty, + /// A body containing string data + Text(String), + /// A body containing binary data + Binary(Vec), +} + +impl Body { + /// Decodes body, if needed. + /// + /// # Panics + /// + /// Panics when aws communicates to handler that request is base64 encoded but + /// it can not be base64 decoded + pub fn from_maybe_encoded(is_base64_encoded: bool, body: &str) -> Body { + use base64::Engine; + + if is_base64_encoded { + Body::from( + ::base64::engine::general_purpose::STANDARD + .decode(body) + .expect("failed to decode aws base64 encoded body"), + ) + } else { + Body::from(body) + } + } +} + +impl From<()> for Body { + fn from(_: ()) -> Self { + Body::Empty + } +} + +impl<'a> From<&'a str> for Body { + fn from(s: &'a str) -> Self { + Body::Text(s.into()) + } +} + +impl From for Body { + fn from(b: String) -> Self { + Body::Text(b) + } +} + +impl From> for Body { + #[inline] + fn from(cow: Cow<'static, str>) -> Body { + match cow { + Cow::Borrowed(b) => Body::from(b.to_owned()), + Cow::Owned(o) => Body::from(o), + } + } +} + +impl From> for Body { + #[inline] + fn from(cow: Cow<'static, [u8]>) -> Body { + match cow { + Cow::Borrowed(b) => Body::from(b), + Cow::Owned(o) => Body::from(o), + } + } +} + +impl From> for Body { + fn from(b: Vec) -> Self { + Body::Binary(b) + } +} + +impl<'a> From<&'a [u8]> for Body { + fn from(b: &'a [u8]) -> Self { + Body::Binary(b.to_vec()) + } +} + +impl Deref for Body { + type Target = [u8]; + + #[inline] + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + +impl AsRef<[u8]> for Body { + #[inline] + fn as_ref(&self) -> &[u8] { + match self { + Body::Empty => &[], + Body::Text(ref bytes) => bytes.as_ref(), + Body::Binary(ref bytes) => bytes.as_ref(), + } + } +} + +impl Clone for Body { + fn clone(&self) -> Self { + match self { + Body::Empty => Body::Empty, + Body::Text(ref bytes) => Body::Text(bytes.clone()), + Body::Binary(ref bytes) => Body::Binary(bytes.clone()), + } + } +} + +impl Serialize for Body { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + Body::Text(data) => { + serializer.serialize_str(::std::str::from_utf8(data.as_ref()).map_err(S::Error::custom)?) + } + Body::Binary(data) => { + serializer.collect_str(&Base64Display::new(data, &base64::engine::general_purpose::STANDARD)) + } + Body::Empty => serializer.serialize_unit(), + } + } +} + +impl<'de> Deserialize<'de> for Body { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct BodyVisitor; + + impl Visitor<'_> for BodyVisitor { + type Value = Body; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("string") + } + + fn visit_str(self, value: &str) -> Result + where + E: DeError, + { + Ok(Body::from(value)) + } + } + + deserializer.deserialize_str(BodyVisitor) + } +} + +impl HttpBody for Body { + type Data = Bytes; + type Error = super::Error; + + fn is_end_stream(&self) -> bool { + matches!(self, Body::Empty) + } + + fn size_hint(&self) -> SizeHint { + match self { + Body::Empty => SizeHint::default(), + Body::Text(ref s) => SizeHint::with_exact(s.len() as u64), + Body::Binary(ref b) => SizeHint::with_exact(b.len() as u64), + } + } + + fn poll_frame( + self: Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> Poll, Self::Error>>> { + let body = take(self.get_mut()); + Poll::Ready(match body { + Body::Empty => None, + Body::Text(s) => Some(Ok(http_body::Frame::data(s.into()))), + Body::Binary(b) => Some(Ok(http_body::Frame::data(b.into()))), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + + #[test] + fn body_has_default() { + assert_eq!(Body::default(), Body::Empty); + } + + #[test] + fn from_unit() { + assert_eq!(Body::from(()), Body::Empty); + } + + #[test] + fn from_str() { + match Body::from(String::from("foo").as_str()) { + Body::Text(_) => (), + not => panic!("expected Body::Text(...) got {not:?}"), + } + } + + #[test] + fn from_string() { + match Body::from(String::from("foo")) { + Body::Text(_) => (), + not => panic!("expected Body::Text(...) got {not:?}"), + } + } + + #[test] + fn from_cow_str() { + match Body::from(Cow::from("foo")) { + Body::Text(_) => (), + not => panic!("expected Body::Text(...) got {not:?}"), + } + } + + #[test] + fn from_cow_bytes() { + match Body::from(Cow::from("foo".as_bytes())) { + Body::Binary(_) => (), + not => panic!("expected Body::Binary(...) got {not:?}"), + } + } + + #[test] + fn from_bytes() { + match Body::from("foo".as_bytes()) { + Body::Binary(_) => (), + not => panic!("expected Body::Binary(...) got {not:?}"), + } + } + + #[test] + fn serialize_text() { + let mut map = HashMap::new(); + map.insert("foo", Body::from("bar")); + assert_eq!(serde_json::to_string(&map).unwrap(), r#"{"foo":"bar"}"#); + } + + #[test] + fn serialize_binary() { + let mut map = HashMap::new(); + map.insert("foo", Body::from("bar".as_bytes())); + assert_eq!(serde_json::to_string(&map).unwrap(), r#"{"foo":"YmFy"}"#); + } + + #[test] + fn serialize_empty() { + let mut map = HashMap::new(); + map.insert("foo", Body::Empty); + assert_eq!(serde_json::to_string(&map).unwrap(), r#"{"foo":null}"#); + } + + #[test] + fn serialize_from_maybe_encoded() { + match Body::from_maybe_encoded(false, "foo") { + Body::Text(_) => (), + not => panic!("expected Body::Text(...) got {not:?}"), + } + + match Body::from_maybe_encoded(true, "Zm9v") { + Body::Binary(b) => assert_eq!(&[102, 111, 111], b.as_slice()), + not => panic!("expected Body::Text(...) got {not:?}"), + } + } +} diff --git a/lambda-events/src/encodings/mod.rs b/lambda-events/src/encodings/mod.rs new file mode 100644 index 000000000..8b3089477 --- /dev/null +++ b/lambda-events/src/encodings/mod.rs @@ -0,0 +1,40 @@ +use serde::{Deserialize, Serialize}; +use std::ops::{Deref, DerefMut}; + +#[cfg(feature = "chrono")] +mod time; +use crate::custom_serde::{deserialize_base64, serialize_base64}; + +#[cfg(feature = "chrono")] +#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] +pub use self::time::*; +#[cfg(feature = "http")] +mod http; +#[cfg(feature = "http")] +#[cfg_attr(docsrs, doc(cfg(feature = "http")))] +pub use self::http::*; + +pub type Error = Box; + +/// Binary data encoded in base64. +#[non_exhaustive] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct Base64Data( + #[serde(deserialize_with = "deserialize_base64")] + #[serde(serialize_with = "serialize_base64")] + pub Vec, +); + +impl Deref for Base64Data { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Base64Data { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/lambda-events/src/encodings/time.rs b/lambda-events/src/encodings/time.rs new file mode 100644 index 000000000..c7ca04a61 --- /dev/null +++ b/lambda-events/src/encodings/time.rs @@ -0,0 +1,364 @@ +use chrono::{DateTime, TimeDelta, TimeZone, Utc}; +use serde::{ + de::{Deserializer, Error as DeError}, + ser::Serializer, + Deserialize, Serialize, +}; +use std::ops::{Deref, DerefMut}; + +/// Timestamp with millisecond precision. +#[derive(Clone, Default, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct MillisecondTimestamp( + #[serde(deserialize_with = "deserialize_milliseconds")] + #[serde(serialize_with = "serialize_milliseconds")] + pub DateTime, +); + +impl Deref for MillisecondTimestamp { + type Target = DateTime; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for MillisecondTimestamp { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +/// Timestamp with second precision. +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct SecondTimestamp( + #[serde(deserialize_with = "deserialize_seconds")] + #[serde(serialize_with = "serialize_seconds")] + pub DateTime, +); + +impl Deref for SecondTimestamp { + type Target = DateTime; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SecondTimestamp { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +/// Duration with second precision. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct SecondDuration( + #[serde(deserialize_with = "deserialize_duration_seconds")] + #[serde(serialize_with = "serialize_duration_seconds")] + pub TimeDelta, +); + +impl Deref for SecondDuration { + type Target = TimeDelta; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SecondDuration { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +/// Duration with minute precision. +#[derive(Clone, Default, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct MinuteDuration( + #[serde(deserialize_with = "deserialize_duration_minutes")] + #[serde(serialize_with = "serialize_duration_minutes")] + pub TimeDelta, +); + +impl Deref for MinuteDuration { + type Target = TimeDelta; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for MinuteDuration { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +fn serialize_milliseconds(date: &DateTime, serializer: S) -> Result +where + S: Serializer, +{ + let ts_with_millis = date.timestamp_millis(); + serializer.serialize_str(&ts_with_millis.to_string()) +} + +fn deserialize_milliseconds<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let (whole, frac) = normalize_timestamp(deserializer)?; + assert_eq!(frac, 0); + let seconds: f64 = whole as f64 / 1000.0; + let milliseconds: u32 = (seconds.fract() * 1000f64) as u32; + let nanos = milliseconds * 1_000_000; + Utc.timestamp_opt(seconds as i64, nanos) + .latest() + .ok_or_else(|| D::Error::custom("invalid timestamp")) +} + +fn serialize_seconds(date: &DateTime, serializer: S) -> Result +where + S: Serializer, +{ + let seconds = date.timestamp(); + let milliseconds = date.timestamp_subsec_millis(); + let whole_seconds = seconds + (milliseconds as i64 / 1000); + let subsec_millis = milliseconds % 1000; + if milliseconds > 0 { + let combined = format!("{whole_seconds}.{subsec_millis:03}"); + serializer.serialize_str(&combined) + } else { + serializer.serialize_str(&whole_seconds.to_string()) + } +} + +fn deserialize_seconds<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let (whole, frac) = normalize_timestamp(deserializer)?; + let seconds = whole; + let nanos = frac * 1_000_000; + Utc.timestamp_opt(seconds as i64, nanos as u32) + .latest() + .ok_or_else(|| D::Error::custom("invalid timestamp")) +} + +fn serialize_duration_seconds(duration: &TimeDelta, serializer: S) -> Result +where + S: Serializer, +{ + let seconds = duration.num_seconds(); + + serializer.serialize_i64(seconds) +} + +fn deserialize_duration_seconds<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let seconds = f64::deserialize(deserializer)?; + TimeDelta::try_seconds(seconds as i64) + .ok_or_else(|| D::Error::custom(format!("invalid time delta seconds `{seconds}`"))) +} + +fn serialize_duration_minutes(duration: &TimeDelta, serializer: S) -> Result +where + S: Serializer, +{ + let minutes = duration.num_minutes(); + + serializer.serialize_i64(minutes) +} + +fn deserialize_duration_minutes<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let minutes = f64::deserialize(deserializer)?; + TimeDelta::try_minutes(minutes as i64) + .ok_or_else(|| D::Error::custom(format!("invalid time delta minutes `{minutes}`"))) +} + +fn normalize_timestamp<'de, D>(deserializer: D) -> Result<(u64, u64), D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrNumber { + String(String), + Float(f64), + Int(u64), + } + + let input: f64 = match StringOrNumber::deserialize(deserializer)? { + StringOrNumber::String(s) => s.parse::().map_err(DeError::custom)?, + StringOrNumber::Float(f) => f, + StringOrNumber::Int(i) => i as f64, + }; + + // We need to do this due to floating point issues. + let input_as_string = input.to_string(); + let parts: Result, _> = input_as_string + .split('.') + .map(|x| x.parse::().map_err(DeError::custom)) + .collect(); + let parts = parts?; + if parts.len() > 1 { + Ok((parts[0], parts[1])) + } else { + Ok((parts[0], 0)) + } +} + +#[cfg(test)] +#[allow(deprecated)] +mod test { + use super::*; + use chrono::TimeZone; + + #[test] + fn test_deserialize_milliseconds() { + #[derive(Deserialize)] + struct Test { + #[serde(deserialize_with = "deserialize_milliseconds")] + v: DateTime, + } + let expected = Utc.ymd(2017, 10, 5).and_hms_nano(15, 33, 44, 302_000_000); + + // Test parsing strings. + let data = serde_json::json!({ + "v": "1507217624302", + }); + let decoded: Test = serde_json::from_value(data).unwrap(); + assert_eq!(expected, decoded.v,); + // Test parsing ints. + let decoded: Test = serde_json::from_slice(r#"{"v":1507217624302}"#.as_bytes()).unwrap(); + assert_eq!(expected, decoded.v,); + // Test parsing floats. + let data = serde_json::json!({ + "v": 1507217624302.0, + }); + let decoded: Test = serde_json::from_value(data).unwrap(); + assert_eq!(expected, decoded.v,); + } + + #[test] + fn test_serialize_milliseconds() { + #[derive(Serialize)] + struct Test { + #[serde(serialize_with = "serialize_milliseconds")] + v: DateTime, + } + let instance = Test { + v: Utc.ymd(1983, 7, 22).and_hms_nano(1, 0, 0, 99_888_777), + }; + let encoded = serde_json::to_string(&instance).unwrap(); + assert_eq!(encoded, String::from(r#"{"v":"427683600099"}"#)); + } + + #[test] + fn test_serialize_seconds() { + #[derive(Serialize)] + struct Test { + #[serde(serialize_with = "serialize_seconds")] + v: DateTime, + } + + // Make sure nanoseconds are chopped off. + let instance = Test { + v: Utc.ymd(1983, 7, 22).and_hms_nano(1, 0, 0, 99), + }; + let encoded = serde_json::to_string(&instance).unwrap(); + assert_eq!(encoded, String::from(r#"{"v":"427683600"}"#)); + + // Make sure milliseconds are included. + let instance = Test { + v: Utc.ymd(1983, 7, 22).and_hms_nano(1, 0, 0, 2_000_000), + }; + let encoded = serde_json::to_string(&instance).unwrap(); + assert_eq!(encoded, String::from(r#"{"v":"427683600.002"}"#)); + + // Make sure leap seconds are included. + let instance = Test { + v: Utc.ymd(1983, 7, 22).and_hms_nano(23, 59, 59, 1_999_999_999), + }; + let encoded = serde_json::to_string(&instance).unwrap(); + assert_eq!(encoded, String::from(r#"{"v":"427766400.999"}"#)); + } + + #[test] + fn test_deserialize_duration_seconds() { + #[derive(Deserialize)] + struct Test { + #[serde(deserialize_with = "deserialize_duration_seconds")] + v: TimeDelta, + } + + let expected = TimeDelta::try_seconds(36).unwrap(); + + let data = serde_json::json!({ + "v": 36, + }); + let decoded: Test = serde_json::from_value(data).unwrap(); + assert_eq!(expected, decoded.v,); + + let data = serde_json::json!({ + "v": 36.1, + }); + let decoded: Test = serde_json::from_value(data).unwrap(); + assert_eq!(expected, decoded.v,); + } + + #[test] + fn test_serialize_duration_seconds() { + #[derive(Serialize)] + struct Test { + #[serde(serialize_with = "serialize_duration_seconds")] + v: TimeDelta, + } + let instance = Test { + v: TimeDelta::try_seconds(36).unwrap(), + }; + let encoded = serde_json::to_string(&instance).unwrap(); + assert_eq!(encoded, String::from(r#"{"v":36}"#)); + } + + #[test] + fn test_deserialize_duration_minutes() { + #[derive(Deserialize)] + struct Test { + #[serde(deserialize_with = "deserialize_duration_minutes")] + v: TimeDelta, + } + + let expected = TimeDelta::try_minutes(36).unwrap(); + + let data = serde_json::json!({ + "v": 36, + }); + let decoded: Test = serde_json::from_value(data).unwrap(); + assert_eq!(expected, decoded.v,); + + let data = serde_json::json!({ + "v": 36.1, + }); + let decoded: Test = serde_json::from_value(data).unwrap(); + assert_eq!(expected, decoded.v,); + } + + #[test] + fn test_serialize_duration_minutes() { + #[derive(Serialize)] + struct Test { + #[serde(serialize_with = "serialize_duration_minutes")] + v: TimeDelta, + } + let instance = Test { + v: TimeDelta::try_minutes(36).unwrap(), + }; + let encoded = serde_json::to_string(&instance).unwrap(); + assert_eq!(encoded, String::from(r#"{"v":36}"#)); + } +} diff --git a/lambda-events/src/event/activemq/mod.rs b/lambda-events/src/event/activemq/mod.rs new file mode 100644 index 000000000..dcf297dac --- /dev/null +++ b/lambda-events/src/event/activemq/mod.rs @@ -0,0 +1,110 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +use std::collections::HashMap; + +use crate::custom_serde::deserialize_nullish; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ActiveMqEvent { + #[serde(default)] + pub event_source: Option, + #[serde(default)] + pub event_source_arn: Option, + pub messages: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ActiveMqMessage { + #[serde(default)] + #[serde(rename = "messageID")] + pub message_id: Option, + #[serde(default)] + pub message_type: Option, + pub timestamp: i64, + pub delivery_mode: i64, + #[serde(default)] + #[serde(rename = "correlationID")] + pub correlation_id: Option, + #[serde(default)] + pub reply_to: Option, + pub destination: ActiveMqDestination, + pub redelivered: bool, + #[serde(default)] + pub type_: Option, + pub expiration: i64, + pub priority: i64, + #[serde(default)] + pub data: Option, + pub broker_in_time: i64, + pub broker_out_time: i64, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub properties: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ActiveMqDestination { + #[serde(default)] + pub physical_name: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + use crate::fixtures::verify_serde_roundtrip; + + #[test] + #[cfg(feature = "activemq")] + fn example_activemq_event() { + verify_serde_roundtrip::(include_bytes!("../../fixtures/example-activemq-event.json")); + } + + #[test] + #[cfg(feature = "activemq")] + fn example_activemq_event_null_properties() { + let event: ActiveMqEvent = verify_serde_roundtrip(include_bytes!( + "../../fixtures/example-activemq-event-null-properties.json" + )); + assert_eq!( + 0, + event.messages[0].properties.len(), + "null properties should deserialize to empty map" + ) + } +} diff --git a/lambda-events/src/event/alb/mod.rs b/lambda-events/src/event/alb/mod.rs new file mode 100644 index 000000000..429f4deb9 --- /dev/null +++ b/lambda-events/src/event/alb/mod.rs @@ -0,0 +1,163 @@ +use crate::{ + custom_serde::{ + deserialize_headers, deserialize_nullish, http_method, serialize_headers, serialize_multi_value_headers, + serialize_query_string_parameters, + }, + encodings::Body, +}; +#[cfg(feature = "builders")] +use bon::Builder; +use http::{HeaderMap, Method}; +use query_map::QueryMap; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +/// `AlbTargetGroupRequest` contains data originating from the ALB Lambda target group integration +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AlbTargetGroupRequest { + #[serde(with = "http_method")] + pub http_method: Method, + #[serde(default)] + pub path: Option, + #[serde(default)] + #[serde(serialize_with = "serialize_query_string_parameters")] + pub query_string_parameters: QueryMap, + #[serde(default)] + pub multi_value_query_string_parameters: QueryMap, + #[serde(deserialize_with = "deserialize_headers", default)] + #[serde(serialize_with = "serialize_headers")] + pub headers: HeaderMap, + #[serde(deserialize_with = "deserialize_headers", default)] + #[serde(serialize_with = "serialize_multi_value_headers")] + pub multi_value_headers: HeaderMap, + pub request_context: AlbTargetGroupRequestContext, + #[serde(default, deserialize_with = "deserialize_nullish")] + pub is_base64_encoded: bool, + pub body: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `AlbTargetGroupRequestContext` contains the information to identify the load balancer invoking the lambda +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AlbTargetGroupRequestContext { + pub elb: ElbContext, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ElbContext` contains the information to identify the ARN invoking the lambda +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ElbContext { + /// nolint: stylecheck + #[serde(default)] + pub target_group_arn: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `AlbTargetGroupResponse` configures the response to be returned by the ALB Lambda target group for the request +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AlbTargetGroupResponse { + pub status_code: i64, + #[serde(default)] + pub status_description: Option, + #[serde(deserialize_with = "http_serde::header_map::deserialize", default)] + #[serde(serialize_with = "serialize_headers")] + pub headers: HeaderMap, + #[serde(deserialize_with = "http_serde::header_map::deserialize", default)] + #[serde(serialize_with = "serialize_multi_value_headers")] + pub multi_value_headers: HeaderMap, + #[serde(skip_serializing_if = "Option::is_none")] + pub body: Option, + #[serde(default, deserialize_with = "deserialize_nullish")] + pub is_base64_encoded: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + use serde_json::Value; + + #[test] + #[cfg(feature = "alb")] + fn example_alb_lambda_target_request_headers_only() { + let data = include_bytes!("../../fixtures/example-alb-lambda-target-request-headers-only.json"); + let parsed: AlbTargetGroupRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: AlbTargetGroupRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "alb")] + fn example_alb_lambda_target_request_multivalue_headers() { + let data = include_bytes!("../../fixtures/example-alb-lambda-target-request-multivalue-headers.json"); + let parsed: AlbTargetGroupRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: AlbTargetGroupRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "alb")] + fn ensure_alb_lambda_target_request_query_string_parameter_value_is_string() { + let data = include_bytes!("../../fixtures/example-alb-lambda-target-request-headers-only.json"); + let parsed: AlbTargetGroupRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: Value = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!( + reparsed["queryStringParameters"]["key"], + Value::String("hello".to_string()) + ); + } + + #[test] + #[cfg(feature = "alb")] + fn example_alb_lambda_target_response() { + let data = include_bytes!("../../fixtures/example-alb-lambda-target-response.json"); + let parsed: AlbTargetGroupResponse = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: AlbTargetGroupResponse = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/apigw/mod.rs b/lambda-events/src/event/apigw/mod.rs new file mode 100644 index 000000000..fe7e4ac6e --- /dev/null +++ b/lambda-events/src/event/apigw/mod.rs @@ -0,0 +1,1467 @@ +use crate::{ + custom_serde::{ + deserialize_headers, deserialize_nullish, http_method, serialize_headers, serialize_multi_value_headers, + }, + encodings::Body, + iam::IamPolicyStatement, +}; +#[cfg(feature = "builders")] +use bon::Builder; +use http::{HeaderMap, Method}; +use query_map::QueryMap; +use serde::{de::DeserializeOwned, ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::Value; +use std::collections::HashMap; + +/// `ApiGatewayProxyRequest` contains data coming from the API Gateway proxy +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayProxyRequest { + /// The resource path defined in API Gateway + #[serde(default)] + pub resource: Option, + /// The url path for the caller + #[serde(default)] + pub path: Option, + #[serde(with = "http_method")] + pub http_method: Method, + #[serde(deserialize_with = "deserialize_headers", default)] + #[serde(serialize_with = "serialize_headers")] + pub headers: HeaderMap, + #[serde(deserialize_with = "deserialize_headers", default)] + #[serde(serialize_with = "serialize_multi_value_headers")] + pub multi_value_headers: HeaderMap, + #[serde(default, deserialize_with = "query_map::serde::standard::deserialize_empty")] + #[serde(serialize_with = "query_map::serde::aws_api_gateway_v1::serialize_query_string_parameters")] + pub query_string_parameters: QueryMap, + #[serde(default, deserialize_with = "query_map::serde::standard::deserialize_empty")] + pub multi_value_query_string_parameters: QueryMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub path_parameters: HashMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub stage_variables: HashMap, + #[serde(bound = "")] + pub request_context: ApiGatewayProxyRequestContext, + #[serde(default)] + pub body: Option, + #[serde(default, deserialize_with = "deserialize_nullish")] + pub is_base64_encoded: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayProxyResponse` configures the response to be returned by API Gateway for the request +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayProxyResponse { + pub status_code: i64, + #[serde(deserialize_with = "deserialize_headers", default)] + #[serde(serialize_with = "serialize_headers")] + pub headers: HeaderMap, + #[serde(deserialize_with = "deserialize_headers", default)] + #[serde(serialize_with = "serialize_multi_value_headers")] + pub multi_value_headers: HeaderMap, + #[serde(skip_serializing_if = "Option::is_none")] + pub body: Option, + #[serde(default, deserialize_with = "deserialize_nullish")] + pub is_base64_encoded: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayProxyRequestContext` contains the information to identify the AWS account and resources invoking the +/// Lambda function. It also includes Cognito identity information for the caller. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayProxyRequestContext { + #[serde(default)] + pub account_id: Option, + #[serde(default)] + pub resource_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub operation_name: Option, + #[serde(default)] + pub stage: Option, + #[serde(default)] + pub domain_name: Option, + #[serde(default)] + pub domain_prefix: Option, + #[serde(default)] + pub request_id: Option, + #[serde(default)] + pub protocol: Option, + #[serde(default)] + pub identity: ApiGatewayRequestIdentity, + #[serde(default)] + pub resource_path: Option, + #[serde(default)] + pub path: Option, + #[serde( + default, + deserialize_with = "deserialize_authorizer_fields", + serialize_with = "serialize_authorizer_fields", + skip_serializing_if = "ApiGatewayRequestAuthorizer::is_empty" + )] + pub authorizer: ApiGatewayRequestAuthorizer, + #[serde(with = "http_method")] + pub http_method: Method, + #[serde(default)] + pub request_time: Option, + #[serde(default)] + pub request_time_epoch: i64, + /// The API Gateway rest API Id + #[serde(default)] + #[serde(rename = "apiId")] + pub apiid: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayV2httpRequest` contains data coming from the new HTTP API Gateway +/// +/// # Important Note +/// +/// For standard HTTP API v2 requests, the HTTP method is located in `request_context.http.method`, +/// not in the top-level `http_method` field. The top-level `http_method` field (and other +/// authorizer-specific fields) are only present in Custom Authorizer v1 payloads. +/// +/// **For standard HTTP API v2 requests, use `request_context.http.method` to get the correct HTTP method.** +/// +/// For Custom Authorizer v1 payloads, use the dedicated `ApiGatewayV2CustomAuthorizerV1Request` struct instead. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayV2httpRequest { + /// The type field from Custom Authorizer v1 requests. + /// + /// This field is not present in standard HTTP API v2 requests. + /// For Custom Authorizer v1 payloads, use `ApiGatewayV2CustomAuthorizerV1Request` instead. + #[deprecated( + since = "1.1.0", + note = "This field is only present in Custom Authorizer v1 requests. Use `ApiGatewayV2CustomAuthorizerV1Request` for authorizer payloads." + )] + #[serde(default, rename = "type")] + pub kind: Option, + /// The method ARN from Custom Authorizer v1 requests. + /// + /// This field is not present in standard HTTP API v2 requests. + /// For Custom Authorizer v1 payloads, use `ApiGatewayV2CustomAuthorizerV1Request` instead. + #[deprecated( + since = "1.1.0", + note = "This field is only present in Custom Authorizer v1 requests. Use `ApiGatewayV2CustomAuthorizerV1Request` for authorizer payloads." + )] + #[serde(default)] + pub method_arn: Option, + /// The HTTP method field. + /// + /// **Warning**: This field defaults to `GET` if not present in the payload, which may lead to + /// incorrect behavior. For standard HTTP API v2 requests, this field is not present in the JSON payload. + /// + /// **Use `request_context.http.method` instead for standard HTTP API v2 requests**, which contains + /// the actual HTTP method for the request. + /// + /// This top-level `http_method` field is only present in Custom Authorizer v1 requests. + /// For those payloads, use `ApiGatewayV2CustomAuthorizerV1Request` instead. + #[deprecated( + since = "1.1.0", + note = "For standard HTTP API v2 requests, use `request_context.http.method` instead. This field defaults to GET when missing and may return incorrect values. For Custom Authorizer v1 payloads, use `ApiGatewayV2CustomAuthorizerV1Request`." + )] + #[serde(with = "http_method", default = "default_http_method")] + pub http_method: Method, + /// The identity source from Custom Authorizer v1 requests. + /// + /// This field is not present in standard HTTP API v2 requests. + /// For Custom Authorizer v1 payloads, use `ApiGatewayV2CustomAuthorizerV1Request` instead. + #[deprecated( + since = "1.1.0", + note = "This field is only present in Custom Authorizer v1 requests. Use `ApiGatewayV2CustomAuthorizerV1Request` for authorizer payloads." + )] + #[serde(default)] + pub identity_source: Option, + /// The authorization token from Custom Authorizer v1 requests. + /// + /// This field is not present in standard HTTP API v2 requests. + /// For Custom Authorizer v1 payloads, use `ApiGatewayV2CustomAuthorizerV1Request` instead. + #[deprecated( + since = "1.1.0", + note = "This field is only present in Custom Authorizer v1 requests. Use `ApiGatewayV2CustomAuthorizerV1Request` for authorizer payloads." + )] + #[serde(default)] + pub authorization_token: Option, + #[serde(default)] + pub resource: Option, + #[serde(default)] + pub version: Option, + #[serde(default)] + pub route_key: Option, + #[serde(default, alias = "path")] + pub raw_path: Option, + #[serde(default)] + pub raw_query_string: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cookies: Option>, + #[serde(deserialize_with = "deserialize_headers", default)] + #[serde(serialize_with = "serialize_headers")] + pub headers: HeaderMap, + #[serde( + default, + deserialize_with = "query_map::serde::aws_api_gateway_v2::deserialize_empty" + )] + #[serde(skip_serializing_if = "QueryMap::is_empty")] + #[serde(serialize_with = "query_map::serde::aws_api_gateway_v2::serialize_query_string_parameters")] + pub query_string_parameters: QueryMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + #[serde(skip_serializing_if = "HashMap::is_empty")] + pub path_parameters: HashMap, + pub request_context: ApiGatewayV2httpRequestContext, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub stage_variables: HashMap, + #[serde(skip_serializing_if = "Option::is_none")] + pub body: Option, + #[serde(default)] + pub is_base64_encoded: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayV2httpRequestContext` contains the information to identify the AWS account and resources invoking the Lambda function. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayV2httpRequestContext { + #[serde(default)] + pub route_key: Option, + #[serde(default)] + pub account_id: Option, + #[serde(default)] + pub stage: Option, + #[serde(default)] + pub request_id: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub authorizer: Option, + /// The API Gateway HTTP API Id + #[serde(default)] + #[serde(rename = "apiId")] + pub apiid: Option, + #[serde(default)] + pub domain_name: Option, + #[serde(default)] + pub domain_prefix: Option, + #[serde(default)] + pub time: Option, + #[serde(default)] + pub time_epoch: i64, + pub http: ApiGatewayV2httpRequestContextHttpDescription, + #[serde(skip_serializing_if = "Option::is_none")] + pub authentication: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayRequestAuthorizer` contains authorizer information for the request context. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct ApiGatewayRequestAuthorizer { + #[serde(skip_serializing_if = "Option::is_none")] + pub jwt: Option, + #[serde( + bound = "", + rename = "lambda", + default, + skip_serializing_if = "HashMap::is_empty", + deserialize_with = "deserialize_nullish" + )] + pub fields: HashMap, + #[serde(skip_serializing_if = "Option::is_none")] + pub iam: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayRequestAuthorizerJwtDescription` contains JWT authorizer information for the request context. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayRequestAuthorizerJwtDescription { + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub claims: HashMap, + #[serde(skip_serializing_if = "Option::is_none")] + pub scopes: Option>, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayRequestAuthorizerIamDescription` contains IAM information for the request context. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayRequestAuthorizerIamDescription { + #[serde(default)] + pub access_key: Option, + #[serde(default)] + pub account_id: Option, + #[serde(default)] + pub caller_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub cognito_identity: Option, + #[serde(default)] + pub principal_org_id: Option, + #[serde(default)] + pub user_arn: Option, + #[serde(default)] + pub user_id: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayRequestAuthorizerCognitoIdentity` contains Cognito identity information for the request context. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayRequestAuthorizerCognitoIdentity { + pub amr: Vec, + #[serde(default)] + pub identity_id: Option, + #[serde(default)] + pub identity_pool_id: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayV2httpRequestContextHttpDescription` contains HTTP information for the request context. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayV2httpRequestContextHttpDescription { + #[serde(with = "http_method")] + pub method: Method, + #[serde(default)] + pub path: Option, + #[serde(default)] + pub protocol: Option, + #[serde(default)] + pub source_ip: Option, + #[serde(default)] + pub user_agent: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayV2httpResponse` configures the response to be returned by API Gateway V2 for the request +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayV2httpResponse { + pub status_code: i64, + #[serde(deserialize_with = "deserialize_headers", default)] + #[serde(serialize_with = "serialize_headers")] + pub headers: HeaderMap, + #[serde(deserialize_with = "deserialize_headers", default)] + #[serde(serialize_with = "serialize_multi_value_headers")] + pub multi_value_headers: HeaderMap, + #[serde(skip_serializing_if = "Option::is_none")] + pub body: Option, + #[serde(default, deserialize_with = "deserialize_nullish")] + pub is_base64_encoded: bool, + pub cookies: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayRequestIdentity` contains identity information for the request caller. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayRequestIdentity { + #[serde(default)] + pub cognito_identity_pool_id: Option, + #[serde(default)] + pub account_id: Option, + #[serde(default)] + pub cognito_identity_id: Option, + #[serde(default)] + pub caller: Option, + #[serde(default)] + pub api_key: Option, + #[serde(default)] + pub api_key_id: Option, + #[serde(default)] + pub access_key: Option, + #[serde(default)] + pub source_ip: Option, + #[serde(default)] + pub cognito_authentication_type: Option, + #[serde(default)] + pub cognito_authentication_provider: Option, + /// nolint: stylecheck + #[serde(default)] + pub user_arn: Option, + #[serde(default)] + pub user_agent: Option, + #[serde(default)] + pub user: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayWebsocketProxyRequest` contains data coming from the API Gateway proxy +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayWebsocketProxyRequest { + /// The resource path defined in API Gateway + #[serde(default)] + pub resource: Option, + /// The url path for the caller + #[serde(default)] + pub path: Option, + #[serde(deserialize_with = "http_method::deserialize_optional")] + #[serde(serialize_with = "http_method::serialize_optional")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub http_method: Option, + #[serde(deserialize_with = "deserialize_headers", default)] + #[serde(serialize_with = "serialize_headers")] + pub headers: HeaderMap, + #[serde(deserialize_with = "deserialize_headers", default)] + #[serde(serialize_with = "serialize_multi_value_headers")] + pub multi_value_headers: HeaderMap, + #[serde(default, deserialize_with = "query_map::serde::standard::deserialize_empty")] + pub query_string_parameters: QueryMap, + #[serde(default, deserialize_with = "query_map::serde::standard::deserialize_empty")] + pub multi_value_query_string_parameters: QueryMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub path_parameters: HashMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub stage_variables: HashMap, + #[serde(bound = "")] + pub request_context: ApiGatewayWebsocketProxyRequestContext, + #[serde(default)] + pub body: Option, + #[serde(default, deserialize_with = "deserialize_nullish")] + pub is_base64_encoded: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayWebsocketProxyRequestContext` contains the information to identify +/// the AWS account and resources invoking the Lambda function. It also includes +/// Cognito identity information for the caller. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayWebsocketProxyRequestContext { + #[serde(default)] + pub account_id: Option, + #[serde(default)] + pub resource_id: Option, + #[serde(default)] + pub stage: Option, + #[serde(default)] + pub request_id: Option, + #[serde(default)] + pub identity: ApiGatewayRequestIdentity, + #[serde(default)] + pub resource_path: Option, + #[serde( + default, + deserialize_with = "deserialize_authorizer_fields", + serialize_with = "serialize_authorizer_fields", + skip_serializing_if = "ApiGatewayRequestAuthorizer::is_empty" + )] + pub authorizer: ApiGatewayRequestAuthorizer, + #[serde(deserialize_with = "http_method::deserialize_optional")] + #[serde(serialize_with = "http_method::serialize_optional")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub http_method: Option, + /// The API Gateway rest API Id + #[serde(default)] + #[serde(rename = "apiId")] + pub apiid: Option, + pub connected_at: i64, + #[serde(default)] + pub connection_id: Option, + #[serde(default)] + pub domain_name: Option, + #[serde(default)] + pub error: Option, + #[serde(default)] + pub event_type: Option, + #[serde(default)] + pub extended_request_id: Option, + #[serde(default)] + pub integration_latency: Option, + #[serde(default)] + pub message_direction: Option, + #[serde(bound = "")] + pub message_id: Option, + #[serde(default)] + pub request_time: Option, + pub request_time_epoch: i64, + #[serde(default)] + pub route_key: Option, + #[serde(default)] + pub status: Option, + #[serde(default)] + pub disconnect_status_code: Option, + #[serde(default)] + pub disconnect_reason: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayCustomAuthorizerRequestTypeRequestIdentity` contains identity information for the request caller including certificate information if using mTLS. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayCustomAuthorizerRequestTypeRequestIdentity { + #[serde(default)] + pub api_key_id: Option, + #[serde(default)] + pub api_key: Option, + #[serde(default)] + pub source_ip: Option, + #[serde(default)] + pub client_cert: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayCustomAuthorizerRequestTypeRequestIdentityClientCert` contains certificate information for the request caller if using mTLS. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayCustomAuthorizerRequestTypeRequestIdentityClientCert { + #[serde(default)] + pub client_cert_pem: Option, + #[serde(default)] + #[serde(rename = "issuerDN")] + pub issuer_dn: Option, + #[serde(default)] + pub serial_number: Option, + #[serde(default)] + #[serde(rename = "subjectDN")] + pub subject_dn: Option, + pub validity: ApiGatewayCustomAuthorizerRequestTypeRequestIdentityClientCertValidity, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayCustomAuthorizerRequestTypeRequestIdentityClientCertValidity` contains certificate validity information for the request caller if using mTLS. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayCustomAuthorizerRequestTypeRequestIdentityClientCertValidity { + #[serde(default)] + pub not_after: Option, + #[serde(default)] + pub not_before: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayV2httpRequestContextAuthentication` contains authentication context information for the request caller including client certificate information if using mTLS. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayV2httpRequestContextAuthentication { + #[serde(default)] + pub client_cert: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayV2httpRequestContextAuthenticationClientCert` contains client certificate information for the request caller if using mTLS. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayV2httpRequestContextAuthenticationClientCert { + #[serde(default)] + pub client_cert_pem: Option, + #[serde(default)] + #[serde(rename = "issuerDN")] + pub issuer_dn: Option, + #[serde(default)] + pub serial_number: Option, + #[serde(default)] + #[serde(rename = "subjectDN")] + pub subject_dn: Option, + pub validity: ApiGatewayV2httpRequestContextAuthenticationClientCertValidity, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayV2httpRequestContextAuthenticationClientCertValidity` contains client certificate validity information for the request caller if using mTLS. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayV2httpRequestContextAuthenticationClientCertValidity { + #[serde(default)] + pub not_after: Option, + #[serde(default)] + pub not_before: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayV2CustomAuthorizerV1RequestTypeRequestContext { + #[serde(default)] + pub path: Option, + #[serde(default)] + pub account_id: Option, + #[serde(default)] + pub resource_id: Option, + #[serde(default)] + pub stage: Option, + #[serde(default)] + pub request_id: Option, + pub identity: ApiGatewayCustomAuthorizerRequestTypeRequestIdentity, + #[serde(default)] + pub resource_path: Option, + #[serde(with = "http_method")] + pub http_method: Method, + #[serde(default)] + #[serde(rename = "apiId")] + pub apiid: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayV2CustomAuthorizerV1Request { + #[serde(default)] + pub version: Option, + #[serde(default)] + pub type_: Option, + /// nolint: stylecheck + #[serde(default)] + pub method_arn: Option, + #[serde(default)] + pub identity_source: Option, + #[serde(default)] + pub authorization_token: Option, + #[serde(default)] + pub resource: Option, + #[serde(default)] + pub path: Option, + #[serde(with = "http_method")] + pub http_method: Method, + #[serde(deserialize_with = "http_serde::header_map::deserialize", default)] + #[serde(serialize_with = "serialize_headers")] + pub headers: HeaderMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub query_string_parameters: HashMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub path_parameters: HashMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub stage_variables: HashMap, + pub request_context: ApiGatewayV2CustomAuthorizerV1RequestTypeRequestContext, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayV2CustomAuthorizerV2Request { + #[serde(default)] + pub version: Option, + #[serde(default)] + pub type_: Option, + /// nolint: stylecheck + #[serde(default)] + pub route_arn: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub identity_source: Option>, + #[serde(default)] + pub route_key: Option, + #[serde(default)] + pub raw_path: Option, + #[serde(default)] + pub raw_query_string: Option, + #[serde(default)] + pub cookies: Vec, + #[serde(deserialize_with = "http_serde::header_map::deserialize", default)] + #[serde(serialize_with = "serialize_headers")] + pub headers: HeaderMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub query_string_parameters: HashMap, + pub request_context: ApiGatewayV2httpRequestContext, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub path_parameters: HashMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub stage_variables: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayCustomAuthorizerContext` represents the expected format of an API Gateway custom authorizer response. +/// Deprecated. Code should be updated to use the Authorizer map from APIGatewayRequestIdentity. Ex: Authorizer["principalId"] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayCustomAuthorizerContext { + pub principal_id: Option, + pub string_key: Option, + pub num_key: Option, + #[serde(default, deserialize_with = "deserialize_nullish")] + pub bool_key: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayCustomAuthorizerRequestTypeRequestContext` represents the expected format of an API Gateway custom authorizer response. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayCustomAuthorizerRequestTypeRequestContext { + #[serde(default)] + pub path: Option, + #[serde(default)] + pub account_id: Option, + #[serde(default)] + pub resource_id: Option, + #[serde(default)] + pub stage: Option, + #[serde(default)] + pub request_id: Option, + #[serde(default)] + pub identity: Option, + #[serde(default)] + pub resource_path: Option, + #[serde(deserialize_with = "http_method::deserialize_optional")] + #[serde(serialize_with = "http_method::serialize_optional")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub http_method: Option, + #[serde(default)] + #[serde(rename = "apiId")] + pub apiid: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayCustomAuthorizerRequest` contains data coming in to a custom API Gateway authorizer function. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayCustomAuthorizerRequest { + #[serde(default)] + pub type_: Option, + #[serde(default)] + pub authorization_token: Option, + /// nolint: stylecheck + #[serde(default)] + pub method_arn: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayCustomAuthorizerRequestTypeRequest` contains data coming in to a custom API Gateway authorizer function. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayCustomAuthorizerRequestTypeRequest { + #[serde(default)] + pub type_: Option, + /// nolint: stylecheck + #[serde(default)] + pub method_arn: Option, + #[serde(default)] + pub resource: Option, + #[serde(default)] + pub path: Option, + #[serde(deserialize_with = "http_method::deserialize_optional")] + #[serde(serialize_with = "http_method::serialize_optional")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub http_method: Option, + #[serde(deserialize_with = "deserialize_headers", default)] + #[serde(serialize_with = "serialize_headers")] + pub headers: HeaderMap, + #[serde(deserialize_with = "deserialize_headers", default)] + #[serde(serialize_with = "serialize_multi_value_headers")] + pub multi_value_headers: HeaderMap, + #[serde(default, deserialize_with = "query_map::serde::standard::deserialize_empty")] + pub query_string_parameters: QueryMap, + #[serde(default, deserialize_with = "query_map::serde::standard::deserialize_empty")] + pub multi_value_query_string_parameters: QueryMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub path_parameters: HashMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub stage_variables: HashMap, + pub request_context: ApiGatewayCustomAuthorizerRequestTypeRequestContext, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayCustomAuthorizerResponse` represents the expected format of an API Gateway authorization response. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayCustomAuthorizerResponse +where + T1: DeserializeOwned, + T1: Serialize, +{ + #[serde(default)] + pub principal_id: Option, + pub policy_document: ApiGatewayCustomAuthorizerPolicy, + #[serde(bound = "", default)] + pub context: T1, + pub usage_identifier_key: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayV2CustomAuthorizerSimpleResponse` represents the simple format of an API Gateway V2 authorization response. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayV2CustomAuthorizerSimpleResponse +where + T1: DeserializeOwned, + T1: Serialize, +{ + pub is_authorized: bool, + #[serde(bound = "", default)] + pub context: T1, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiGatewayV2CustomAuthorizerIamPolicyResponse +where + T1: DeserializeOwned, + T1: Serialize, +{ + #[serde(default)] + pub principal_id: Option, + pub policy_document: ApiGatewayCustomAuthorizerPolicy, + #[serde(bound = "", default)] + pub context: T1, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ApiGatewayCustomAuthorizerPolicy` represents an IAM policy +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct ApiGatewayCustomAuthorizerPolicy { + #[serde(default)] + pub version: Option, + pub statement: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +fn default_http_method() -> Method { + Method::GET +} + +#[deprecated = "use `ApiGatewayRequestAuthorizer` instead"] +pub type ApiGatewayV2httpRequestContextAuthorizerDescription = ApiGatewayRequestAuthorizer; +#[deprecated = "use `ApiGatewayRequestAuthorizerJwtDescription` instead"] +pub type ApiGatewayV2httpRequestContextAuthorizerJwtDescription = ApiGatewayRequestAuthorizerJwtDescription; +#[deprecated = "use `ApiGatewayRequestAuthorizerIamDescription` instead"] +pub type ApiGatewayV2httpRequestContextAuthorizerIamDescription = ApiGatewayRequestAuthorizerIamDescription; +#[deprecated = "use `ApiGatewayRequestAuthorizerCognitoIdentity` instead"] +pub type ApiGatewayV2httpRequestContextAuthorizerCognitoIdentity = ApiGatewayRequestAuthorizerCognitoIdentity; + +impl ApiGatewayRequestAuthorizer { + fn is_empty(&self) -> bool { + self.fields.is_empty() + } +} + +fn deserialize_authorizer_fields<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let fields: Option> = Option::deserialize(deserializer)?; + let mut authorizer = ApiGatewayRequestAuthorizer::default(); + if let Some(fields) = fields { + authorizer.fields = fields; + } + + Ok(authorizer) +} + +pub fn serialize_authorizer_fields( + authorizer: &ApiGatewayRequestAuthorizer, + ser: S, +) -> Result { + let mut map = ser.serialize_map(Some(authorizer.fields.len()))?; + for (k, v) in &authorizer.fields { + map.serialize_entry(k, v)?; + } + map.end() +} + +#[cfg(test)] +mod test { + use super::*; + use crate::fixtures::verify_serde_roundtrip; + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_custom_auth_request_type_request() { + let data = include_bytes!("../../fixtures/example-apigw-custom-auth-request-type-request.json"); + let parsed: ApiGatewayCustomAuthorizerRequestTypeRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayCustomAuthorizerRequestTypeRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_custom_auth_request_type_request_websocket() { + let data = include_bytes!("../../fixtures/example-apigw-v2-custom-authorizer-websocket-request.json"); + let parsed: ApiGatewayCustomAuthorizerRequestTypeRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayCustomAuthorizerRequestTypeRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_custom_auth_request() { + let data = include_bytes!("../../fixtures/example-apigw-custom-auth-request.json"); + let parsed: ApiGatewayCustomAuthorizerRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayCustomAuthorizerRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_custom_auth_response() { + let data = include_bytes!("../../fixtures/example-apigw-custom-auth-response.json"); + let parsed: ApiGatewayCustomAuthorizerResponse = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayCustomAuthorizerResponse = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_custom_auth_response_with_single_value_action() { + let data = include_bytes!("../../fixtures/example-apigw-custom-auth-response-with-single-value-action.json"); + let parsed: ApiGatewayCustomAuthorizerResponse = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayCustomAuthorizerResponse = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_custom_auth_response_with_single_value_resource() { + let data = include_bytes!("../../fixtures/example-apigw-custom-auth-response-with-single-value-resource.json"); + let parsed: ApiGatewayCustomAuthorizerResponse = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayCustomAuthorizerResponse = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_request() { + let data = include_bytes!("../../fixtures/example-apigw-request.json"); + let parsed: ApiGatewayProxyRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayProxyRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_response() { + let data = include_bytes!("../../fixtures/example-apigw-response.json"); + let parsed: ApiGatewayProxyResponse = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayProxyResponse = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_request_multi_value_parameters() { + let data = include_bytes!("../../fixtures/example-apigw-request-multi-value-parameters.json"); + let parsed: ApiGatewayProxyRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayProxyRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + + assert!(output.contains(r#""multiValueQueryStringParameters":{"name":["me","me2"]}"#)); + assert!(output.contains(r#""queryStringParameters":{"name":"me"}"#)); + assert!(output.contains(r#""headername":["headerValue","headerValue2"]"#)); + assert!(output.contains(r#""headername":"headerValue2""#)); + } + + #[test] + #[cfg(all(feature = "apigw", feature = "catch-all-fields"))] + fn example_apigw_request_catch_all() { + use serde_json::json; + + let data = include_bytes!("../../fixtures/example-apigw-request-catch-all.json"); + let parsed: ApiGatewayProxyRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayProxyRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + + assert_eq!(parsed.other.get("otherField"), Some(&json!("foobar"))); + assert_eq!( + parsed.request_context.identity.other.get("otherField"), + Some(&json!(2345)) + ); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_restapi_openapi_request() { + let data = include_bytes!("../../fixtures/example-apigw-restapi-openapi-request.json"); + let parsed: ApiGatewayProxyRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayProxyRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_v2_request_iam() { + let data = include_bytes!("../../fixtures/example-apigw-v2-request-iam.json"); + let parsed: ApiGatewayV2httpRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayV2httpRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_v2_request_jwt_authorizer() { + let data = include_bytes!("../../fixtures/example-apigw-v2-request-jwt-authorizer.json"); + let parsed: ApiGatewayV2httpRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayV2httpRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_v2_request_lambda_authorizer() { + let data = include_bytes!("../../fixtures/example-apigw-v2-request-lambda-authorizer.json"); + let parsed: ApiGatewayV2httpRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayV2httpRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_v2_request_multi_value_parameters() { + let data = include_bytes!("../../fixtures/example-apigw-v2-request-multi-value-parameters.json"); + let parsed: ApiGatewayV2httpRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayV2httpRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + + assert!(output.contains(r#""header2":"value1,value2""#)); + assert!(output.contains(r#""queryStringParameters":{"Parameter1":"value1,value2"}"#)); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_v2_request_no_authorizer() { + let data = include_bytes!("../../fixtures/example-apigw-v2-request-no-authorizer.json"); + let parsed: ApiGatewayV2httpRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayV2httpRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "apigw")] + fn apigw_v2_correct_http_method_usage() { + // This test demonstrates the correct way to access the HTTP method + // for standard HTTP API v2 requests: use request_context.http.method + let data = include_bytes!("../../fixtures/example-apigw-v2-request-no-authorizer.json"); + let parsed: ApiGatewayV2httpRequest = serde_json::from_slice(data).unwrap(); + + // The correct way to get the HTTP method for standard HTTP API v2 requests + assert_eq!(parsed.request_context.http.method, Method::GET); + + // Verify that request_context.http.method contains the actual method from the JSON + // In the fixture, requestContext.http.method is "GET" + assert_eq!(parsed.request_context.http.method, Method::GET); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_websocket_request() { + let data = include_bytes!("../../fixtures/example-apigw-websocket-request.json"); + let parsed: ApiGatewayWebsocketProxyRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayWebsocketProxyRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_console_test_request() { + let data = include_bytes!("../../fixtures/example-apigw-console-test-request.json"); + let parsed: ApiGatewayProxyRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayProxyRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_websocket_request_without_method() { + let data = include_bytes!("../../fixtures/example-apigw-websocket-request-without-method.json"); + let parsed: ApiGatewayWebsocketProxyRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayWebsocketProxyRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_websocket_request_disconnect_route() { + let data = include_bytes!("../../fixtures/example-apigw-websocket-request-disconnect-route.json"); + let parsed: ApiGatewayWebsocketProxyRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayWebsocketProxyRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_v2_custom_authorizer_v1_request() { + let data = include_bytes!("../../fixtures/example-apigw-v2-custom-authorizer-v1-request.json"); + let parsed: ApiGatewayV2httpRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayV2httpRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + assert_eq!("REQUEST", parsed.kind.unwrap()); + assert_eq!(Method::GET, parsed.http_method); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_v2_custom_authorizer_v2_request() { + let data = include_bytes!("../../fixtures/example-apigw-v2-custom-authorizer-v2-request.json"); + verify_serde_roundtrip::(data); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_v2_custom_authorizer_v2_request_null_maps() { + let data = include_bytes!("../../fixtures/example-apigw-v2-custom-authorizer-v2-request-null-maps.json"); + let parsed: ApiGatewayV2CustomAuthorizerV2Request = verify_serde_roundtrip(data); + assert_eq!(0, parsed.path_parameters.len()); + assert_eq!(0, parsed.query_string_parameters.len()); + assert_eq!(0, parsed.stage_variables.len()); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_v2_custom_authorizer_v2_request_without_cookies() { + let data = include_bytes!("../../fixtures/example-apigw-v2-custom-authorizer-v2-request-without-cookies.json"); + verify_serde_roundtrip::(data); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_v2_custom_authorizer_v2_request_without_identity_source() { + let data = + include_bytes!("../../fixtures/example-apigw-v2-custom-authorizer-v2-request-without-identity-source.json"); + verify_serde_roundtrip::(data); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_console_request() { + let data = include_bytes!("../../fixtures/example-apigw-console-request.json"); + let parsed: ApiGatewayProxyRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayProxyRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_request_authorizer_fields() { + let data = include_bytes!("../../fixtures/example-apigw-request.json"); + let parsed: ApiGatewayProxyRequest = serde_json::from_slice(data).unwrap(); + + let fields = parsed.request_context.authorizer.fields; + assert_eq!(Some("admin"), fields.get("principalId").unwrap().as_str()); + assert_eq!(Some(1), fields.get("clientId").unwrap().as_u64()); + assert_eq!(Some("Exata"), fields.get("clientName").unwrap().as_str()); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_custom_auth_response_with_statement_condition() { + use crate::iam::IamPolicyEffect; + + let data = include_bytes!("../../fixtures/example-apigw-custom-auth-response-with-condition.json"); + let parsed: ApiGatewayCustomAuthorizerResponse = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayCustomAuthorizerResponse = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + + let statement = parsed.policy_document.statement.first().unwrap(); + assert_eq!(IamPolicyEffect::Deny, statement.effect); + + let condition = statement.condition.as_ref().unwrap(); + assert_eq!(vec!["xxx"], condition["StringEquals"]["aws:SourceIp"]); + } +} diff --git a/lambda-events/src/event/appsync/mod.rs b/lambda-events/src/event/appsync/mod.rs new file mode 100644 index 000000000..16332f5dd --- /dev/null +++ b/lambda-events/src/event/appsync/mod.rs @@ -0,0 +1,410 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; + +use crate::custom_serde::deserialize_nullish; + +/// Deprecated: `AppSyncResolverTemplate` does not represent resolver events sent by AppSync. Instead directly model your input schema, or use `map[string]string`, `json.RawMessage`,` interface{}`, etc.. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AppSyncResolverTemplate +where + T1: DeserializeOwned, + T1: Serialize, +{ + #[serde(default)] + pub version: Option, + pub operation: AppSyncOperation, + #[serde(bound = "")] + pub payload: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `AppSyncIamIdentity` contains information about the caller authed via IAM. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AppSyncIamIdentity { + #[serde(default)] + pub account_id: Option, + #[serde(default)] + pub cognito_identity_auth_provider: Option, + #[serde(default)] + pub cognito_identity_auth_type: Option, + #[serde(default)] + pub cognito_identity_pool_id: Option, + #[serde(default)] + pub cognito_identity_id: Option, + pub source_ip: Vec, + #[serde(default)] + pub username: Option, + #[serde(default)] + pub user_arn: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `AppSyncCognitoIdentity` contains information about the caller authed via Cognito. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[allow(clippy::should_implement_trait)] +pub struct AppSyncCognitoIdentity +where + T1: DeserializeOwned, + T1: Serialize, +{ + #[serde(default)] + pub sub: Option, + #[serde(default)] + pub issuer: Option, + #[serde(default)] + pub username: Option, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + #[serde(bound = "")] + pub claims: HashMap, + pub source_ip: Vec, + #[serde(default)] + pub default_auth_strategy: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +pub type AppSyncOperation = String; + +/// `AppSyncLambdaAuthorizerRequest` contains an authorization request from AppSync. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AppSyncLambdaAuthorizerRequest { + #[serde(default)] + pub authorization_token: Option, + pub request_context: AppSyncLambdaAuthorizerRequestContext, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `AppSyncLambdaAuthorizerRequestContext` contains the parameters of the AppSync invocation which triggered +/// this authorization request. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AppSyncLambdaAuthorizerRequestContext +where + T1: DeserializeOwned, + T1: Serialize, +{ + #[serde(default)] + #[serde(rename = "apiId")] + pub apiid: Option, + #[serde(default)] + pub account_id: Option, + #[serde(default)] + pub request_id: Option, + #[serde(default)] + pub query_string: Option, + #[serde(default)] + pub operation_name: Option, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + #[serde(bound = "")] + pub variables: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `AppSyncLambdaAuthorizerResponse` represents the expected format of an authorization response to AppSync. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AppSyncLambdaAuthorizerResponse +where + T1: DeserializeOwned, + T1: Serialize, +{ + pub is_authorized: bool, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + #[serde(bound = "")] + pub resolver_context: HashMap, + pub denied_fields: Option>, + pub ttl_override: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `AppSyncDirectResolverEvent` represents the default payload structure sent by AWS AppSync +/// when using **Direct Lambda Resolvers** (i.e., when both request and response mapping +/// templates are disabled). +/// +/// This structure includes the full AppSync **Context object**, as described in the +/// [AppSync Direct Lambda resolver reference](https://docs.aws.amazon.com/appsync/latest/devguide/direct-lambda-reference.html). +/// +/// It is recommended when working without VTL templates and relying on the standard +/// AppSync-to-Lambda event format. +/// +/// See also: +/// - [AppSync resolver mapping template context reference](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html) +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +pub struct AppSyncDirectResolverEvent +where + TArguments: Serialize + DeserializeOwned, + TSource: Serialize + DeserializeOwned, + TStash: Serialize + DeserializeOwned, +{ + #[serde(bound = "")] + pub arguments: Option, + pub identity: Option, + #[serde(bound = "")] + pub source: Option, + pub request: AppSyncRequest, + pub info: AppSyncInfo, + #[serde(default)] + pub prev: Option, + #[serde(bound = "")] + pub stash: TStash, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `AppSyncRequest` contains request-related metadata for a resolver invocation, +/// including client-sent headers and optional custom domain name. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AppSyncRequest { + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + #[serde(bound = "")] + pub headers: HashMap>, + #[serde(default)] + pub domain_name: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `AppSyncInfo` contains metadata about the current GraphQL field being resolved. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AppSyncInfo +where + T: Serialize + DeserializeOwned, +{ + #[serde(default)] + pub selection_set_list: Vec, + #[serde(rename = "selectionSetGraphQL")] + pub selection_set_graphql: String, + pub parent_type_name: String, + pub field_name: String, + #[serde(bound = "")] + pub variables: T, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `AppSyncPrevResult` contains the result of the previous step in a pipeline resolver. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +pub struct AppSyncPrevResult +where + T: Serialize + DeserializeOwned, +{ + #[serde(bound = "")] + pub result: T, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `AppSyncIdentity` represents the identity of the caller as determined by the +/// configured AppSync authorization mechanism (IAM, Cognito, OIDC, or Lambda). +#[non_exhaustive] +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(untagged, rename_all = "camelCase")] +pub enum AppSyncIdentity { + IAM(AppSyncIamIdentity), + Cognito(AppSyncCognitoIdentity), + OIDC(AppSyncIdentityOIDC), + Lambda(AppSyncIdentityLambda), +} + +impl Default for AppSyncIdentity { + fn default() -> Self { + AppSyncIdentity::IAM(AppSyncIamIdentity::default()) + } +} + +/// `AppSyncIdentityOIDC` represents identity information when using OIDC-based authorization. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[allow(clippy::should_implement_trait)] +pub struct AppSyncIdentityOIDC +where + T: Serialize + DeserializeOwned, +{ + #[serde(bound = "")] + pub claims: T, + pub issuer: String, + pub sub: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `AppSyncIdentityLambda` represents identity information when using AWS Lambda +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AppSyncIdentityLambda +where + T: Serialize + DeserializeOwned, +{ + #[serde(bound = "")] + pub resolver_context: T, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "appsync")] + fn example_appsync_identity_cognito() { + let data = include_bytes!("../../fixtures/example-appsync-identity-cognito.json"); + let parsed: AppSyncCognitoIdentity = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: AppSyncCognitoIdentity = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "appsync")] + fn example_appsync_identity_iam() { + let data = include_bytes!("../../fixtures/example-appsync-identity-iam.json"); + let parsed: AppSyncIamIdentity = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: AppSyncIamIdentity = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "appsync")] + fn example_appsync_lambda_auth_request() { + let data = include_bytes!("../../fixtures/example-appsync-lambda-auth-request.json"); + let parsed: AppSyncLambdaAuthorizerRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: AppSyncLambdaAuthorizerRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "appsync")] + fn example_appsync_lambda_auth_response() { + let data = include_bytes!("../../fixtures/example-appsync-lambda-auth-response.json"); + let parsed: AppSyncLambdaAuthorizerResponse = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: AppSyncLambdaAuthorizerResponse = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "appsync")] + fn example_appsync_direct_resolver() { + let data = include_bytes!("../../fixtures/example-appsync-direct-resolver.json"); + let parsed: AppSyncDirectResolverEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: AppSyncDirectResolverEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/autoscaling/mod.rs b/lambda-events/src/event/autoscaling/mod.rs new file mode 100644 index 000000000..d14bc1b00 --- /dev/null +++ b/lambda-events/src/event/autoscaling/mod.rs @@ -0,0 +1,121 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use chrono::{DateTime, Utc}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; + +use crate::custom_serde::deserialize_nullish; + +/// `AutoScalingEvent` struct is used to parse the json for auto scaling event types // +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AutoScalingEvent +where + T1: DeserializeOwned, + T1: Serialize, +{ + /// The version of event data + #[serde(default)] + pub version: Option, + /// The unique ID of the event + #[serde(default)] + pub id: Option, + /// Details about event type + #[serde(default)] + #[serde(rename = "detail-type")] + pub detail_type: Option, + /// Source of the event + #[serde(default)] + pub source: Option, + /// AccountId + #[serde(default)] + #[serde(rename = "account")] + pub account_id: Option, + /// Event timestamp + pub time: DateTime, + /// Region of event + #[serde(default)] + pub region: Option, + /// Information about resources impacted by event + pub resources: Vec, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + #[serde(bound = "")] + pub detail: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "autoscaling")] + fn example_autoscaling_event_launch_successful() { + let data = include_bytes!("../../fixtures/example-autoscaling-event-launch-successful.json"); + let parsed: AutoScalingEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: AutoScalingEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "autoscaling")] + fn example_autoscaling_event_launch_unsuccessful() { + let data = include_bytes!("../../fixtures/example-autoscaling-event-launch-unsuccessful.json"); + let parsed: AutoScalingEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: AutoScalingEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "autoscaling")] + fn example_autoscaling_event_lifecycle_action() { + let data = include_bytes!("../../fixtures/example-autoscaling-event-lifecycle-action.json"); + let parsed: AutoScalingEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: AutoScalingEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "autoscaling")] + fn example_autoscaling_event_terminate_action() { + let data = include_bytes!("../../fixtures/example-autoscaling-event-terminate-action.json"); + let parsed: AutoScalingEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: AutoScalingEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "autoscaling")] + fn example_autoscaling_event_terminate_successful() { + let data = include_bytes!("../../fixtures/example-autoscaling-event-terminate-successful.json"); + let parsed: AutoScalingEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: AutoScalingEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "autoscaling")] + fn example_autoscaling_event_terminate_unsuccessful() { + let data = include_bytes!("../../fixtures/example-autoscaling-event-terminate-unsuccessful.json"); + let parsed: AutoScalingEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: AutoScalingEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/bedrock_agent_runtime/mod.rs b/lambda-events/src/event/bedrock_agent_runtime/mod.rs new file mode 100644 index 000000000..ba67b3fab --- /dev/null +++ b/lambda-events/src/event/bedrock_agent_runtime/mod.rs @@ -0,0 +1,179 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +use std::collections::HashMap; + +/// The Event sent to Lambda from Agents for Amazon Bedrock. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentEvent { + ///The version of the message that identifies the format of the event data going into the Lambda function and the expected format of the response from a Lambda function. Amazon Bedrock only supports version 1.0. + pub message_version: String, + ///Contains information about the name, ID, alias, and version of the agent that the action group belongs to. + pub agent: Agent, + ///The user input for the conversation turn. + pub input_text: String, + /// The unique identifier of the agent session. + pub session_id: String, + /// The name of the action group. + pub action_group: String, + /// The path to the API operation, as defined in the OpenAPI schema. + pub api_path: String, + /// The method of the API operation, as defined in the OpenAPI schema. + pub http_method: String, + /// Contains a list of objects. Each object contains the name, type, and value of a parameter in the API operation, as defined in the OpenAPI schema. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub parameters: Option>, + /// Contains the request body and its properties, as defined in the OpenAPI schema. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub request_body: Option, + /// Contains session attributes and their values. + pub session_attributes: HashMap, + /// Contains prompt attributes and their values. + pub prompt_session_attributes: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RequestBody { + /// Contains the request body and its properties + pub content: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Content { + /// The content of the request body + pub properties: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Property { + /// The name of the parameter + pub name: String, + /// The type of the parameter + pub r#type: String, + /// The value of the parameter + pub value: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Parameter { + /// The name of the parameter + pub name: String, + /// The type of the parameter + pub r#type: String, + /// The value of the parameter + pub value: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Agent { + /// The name of the agent. + pub name: String, + /// The unique identifier of the agent. + pub id: String, + /// The alias of the agent. + pub alias: String, + /// The version of the agent. + pub version: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod tests { + + use crate::event::bedrock_agent_runtime::AgentEvent; + + #[test] + #[cfg(feature = "bedrock_agent_runtime")] + fn example_bedrock_agent_runtime_event() { + let data = include_bytes!("../../fixtures/example-bedrock-agent-runtime-event.json"); + let parsed: AgentEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: AgentEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + #[test] + #[cfg(feature = "bedrock_agent_runtime")] + fn example_bedrock_agent_runtime_event_without_parameters() { + let data = include_bytes!("../../fixtures/example-bedrock-agent-runtime-event-without-parameters.json"); + let parsed: AgentEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: AgentEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + #[test] + #[cfg(feature = "bedrock_agent_runtime")] + fn example_bedrock_agent_runtime_event_without_request_body() { + let data = include_bytes!("../../fixtures/example-bedrock-agent-runtime-event-without-request-body.json"); + let parsed: AgentEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: AgentEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/chime_bot/mod.rs b/lambda-events/src/event/chime_bot/mod.rs new file mode 100644 index 000000000..92a6d3910 --- /dev/null +++ b/lambda-events/src/event/chime_bot/mod.rs @@ -0,0 +1,97 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChimeBotEvent { + #[serde(rename = "Sender")] + pub sender: ChimeBotEventSender, + #[serde(rename = "Discussion")] + pub discussion: ChimeBotEventDiscussion, + #[serde(default)] + #[serde(rename = "EventType")] + pub event_type: Option, + #[serde(rename = "InboundHttpsEndpoint")] + pub inbound_https_endpoint: Option, + #[serde(rename = "EventTimestamp")] + pub event_timestamp: DateTime, + #[serde(rename = "Message")] + pub message: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChimeBotEventSender { + #[serde(default)] + #[serde(rename = "SenderId")] + pub sender_id: Option, + #[serde(default)] + #[serde(rename = "SenderIdType")] + pub sender_id_type: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChimeBotEventDiscussion { + #[serde(default)] + #[serde(rename = "DiscussionId")] + pub discussion_id: Option, + #[serde(default)] + #[serde(rename = "DiscussionType")] + pub discussion_type: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChimeBotEventInboundHttpsEndpoint { + #[serde(default)] + #[serde(rename = "EndpointType")] + pub endpoint_type: Option, + #[serde(default)] + #[serde(rename = "Url")] + pub url: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/clientvpn/mod.rs b/lambda-events/src/event/clientvpn/mod.rs new file mode 100644 index 000000000..d3a6f2e42 --- /dev/null +++ b/lambda-events/src/event/clientvpn/mod.rs @@ -0,0 +1,85 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientVpnConnectionHandlerRequest { + #[serde(default)] + #[serde(rename = "connection-id")] + pub connection_id: Option, + #[serde(default)] + #[serde(rename = "endpoint-id")] + pub endpoint_id: Option, + #[serde(default)] + #[serde(rename = "common-name")] + pub common_name: Option, + #[serde(default)] + pub username: Option, + #[serde(default)] + #[serde(rename = "platform")] + pub os_platform: Option, + #[serde(default)] + #[serde(rename = "platform-version")] + pub os_platform_version: Option, + #[serde(default)] + #[serde(rename = "public-ip")] + pub public_ip: Option, + #[serde(default)] + #[serde(rename = "client-openvpn-version")] + pub client_open_vpn_version: Option, + #[serde(default)] + #[serde(rename = "schema-version")] + pub schema_version: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientVpnConnectionHandlerResponse { + pub allow: bool, + #[serde(default)] + #[serde(rename = "error-msg-on-failed-posture-compliance")] + pub error_msg_on_failed_posture_compliance: Option, + #[serde(rename = "posture-compliance-statuses")] + pub posture_compliance_statuses: Vec, + #[serde(default)] + #[serde(rename = "schema-version")] + pub schema_version: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "clientvpn")] + fn example_clientvpn_connectionhandler_request() { + let data = include_bytes!("../../fixtures/example-clientvpn-connectionhandler-request.json"); + let parsed: ClientVpnConnectionHandlerRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ClientVpnConnectionHandlerRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/cloudformation/mod.rs b/lambda-events/src/event/cloudformation/mod.rs new file mode 100644 index 000000000..ac937ac3b --- /dev/null +++ b/lambda-events/src/event/cloudformation/mod.rs @@ -0,0 +1,221 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; + +pub mod provider; + +#[non_exhaustive] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(tag = "RequestType")] +pub enum CloudFormationCustomResourceRequest +where + P1: DeserializeOwned + Serialize, + P2: DeserializeOwned + Serialize, +{ + #[serde(bound = "")] + Create(CreateRequest), + #[serde(bound = "")] + Update(UpdateRequest), + #[serde(bound = "")] + Delete(DeleteRequest), +} + +impl Default for CloudFormationCustomResourceRequest { + fn default() -> Self { + CloudFormationCustomResourceRequest::Create(CreateRequest::default()) + } +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Default, Debug, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct CreateRequest +where + P2: DeserializeOwned + Serialize, +{ + #[serde(default)] + pub service_token: Option, + pub request_id: String, + #[serde(rename = "ResponseURL")] + pub response_url: String, + pub stack_id: String, + pub resource_type: String, + pub logical_resource_id: String, + #[serde(bound = "")] + pub resource_properties: P2, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Default, Debug, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct UpdateRequest +where + P1: DeserializeOwned + Serialize, + P2: DeserializeOwned + Serialize, +{ + #[serde(default)] + pub service_token: Option, + pub request_id: String, + #[serde(rename = "ResponseURL")] + pub response_url: String, + pub stack_id: String, + pub resource_type: String, + pub logical_resource_id: String, + pub physical_resource_id: String, + #[serde(bound = "")] + pub resource_properties: P2, + #[serde(bound = "")] + pub old_resource_properties: P1, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Default, Debug, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct DeleteRequest +where + P2: DeserializeOwned + Serialize, +{ + #[serde(default)] + pub service_token: Option, + pub request_id: String, + #[serde(rename = "ResponseURL")] + pub response_url: String, + pub stack_id: String, + pub resource_type: String, + pub logical_resource_id: String, + pub physical_resource_id: String, + #[serde(bound = "")] + pub resource_properties: P2, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Default, Debug, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct CloudFormationCustomResourceResponse { + pub status: CloudFormationCustomResourceResponseStatus, + pub reason: Option, + pub physical_resource_id: String, + pub stack_id: String, + pub request_id: String, + pub logical_resource_id: String, + pub no_echo: bool, + pub data: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[derive(Clone, Default, Debug, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CloudFormationCustomResourceResponseStatus { + #[default] + Success, + Failed, +} + +#[cfg(test)] +mod test { + use std::collections::HashMap; + + use super::{CloudFormationCustomResourceRequest::*, *}; + + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] + #[serde(rename_all = "PascalCase")] + struct TestProperties { + key_1: String, + key_2: Vec, + key_3: HashMap, + } + + type TestRequest = CloudFormationCustomResourceRequest; + + #[test] + fn example_cloudformation_custom_resource_create_request() { + let data = include_bytes!("../../fixtures/example-cloudformation-custom-resource-create-request.json"); + let parsed: TestRequest = serde_json::from_slice(data).unwrap(); + + match parsed { + Create(_) => (), + _ => panic!("expected Create request"), + } + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: TestRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_cloudformation_custom_resource_update_request() { + let data = include_bytes!("../../fixtures/example-cloudformation-custom-resource-update-request.json"); + let parsed: TestRequest = serde_json::from_slice(data).unwrap(); + + match parsed { + Update(_) => (), + _ => panic!("expected Update request"), + } + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: TestRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_cloudformation_custom_resource_delete_request() { + let data = include_bytes!("../../fixtures/example-cloudformation-custom-resource-delete-request.json"); + let parsed: TestRequest = serde_json::from_slice(data).unwrap(); + + match parsed { + Delete(_) => (), + _ => panic!("expected Delete request"), + } + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: TestRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_cloudformation_custom_resource_response() { + let data = include_bytes!("../../fixtures/example-cloudformation-custom-resource-response.json"); + let parsed: CloudFormationCustomResourceResponse = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CloudFormationCustomResourceResponse = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/cloudformation/provider.rs b/lambda-events/src/event/cloudformation/provider.rs new file mode 100644 index 000000000..4c9cbc131 --- /dev/null +++ b/lambda-events/src/event/cloudformation/provider.rs @@ -0,0 +1,196 @@ +//! These events are to be used with the CDK custom resource provider framework. +//! +//! Note that they are similar (but not the same) as the events in the `super` module. +//! +//! See for details. + +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::Value; + +#[non_exhaustive] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(tag = "RequestType")] +pub enum CloudFormationCustomResourceRequest +where + P1: DeserializeOwned + Serialize, + P2: DeserializeOwned + Serialize, +{ + #[serde(bound = "")] + Create(CreateRequest), + #[serde(bound = "")] + Update(UpdateRequest), + #[serde(bound = "")] + Delete(DeleteRequest), +} + +impl Default for CloudFormationCustomResourceRequest { + fn default() -> Self { + CloudFormationCustomResourceRequest::Create(CreateRequest::default()) + } +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Default, Debug, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct CreateRequest +where + P2: DeserializeOwned + Serialize, +{ + #[serde(flatten, bound = "")] + pub common: CommonRequestParams, + // No `other` catch-all here; any additional fields will be caught in `common.other` instead +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Default, Debug, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct UpdateRequest +where + P1: DeserializeOwned + Serialize, + P2: DeserializeOwned + Serialize, +{ + pub physical_resource_id: String, + + #[serde(bound = "")] + pub old_resource_properties: P1, + + #[serde(flatten, bound = "")] + pub common: CommonRequestParams, + // No `other` catch-all here; any additional fields will be caught in `common.other` instead +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Default, Debug, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct DeleteRequest +where + P2: DeserializeOwned + Serialize, +{ + pub physical_resource_id: String, + + #[serde(flatten, bound = "")] + pub common: CommonRequestParams, + // No `other` catch-all here; any additional fields will be caught in `common.other` instead +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Default, Debug, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct CommonRequestParams +where + P2: DeserializeOwned + Serialize, +{ + pub logical_resource_id: String, + #[serde(bound = "")] + pub resource_properties: P2, + pub resource_type: String, + pub request_id: String, + pub stack_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Default, Debug, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct CloudFormationCustomResourceResponse +where + D: DeserializeOwned + Serialize, +{ + pub physical_resource_id: Option, + #[serde(bound = "")] + pub data: D, + pub no_echo: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use std::collections::HashMap; + + use super::{CloudFormationCustomResourceRequest::*, *}; + + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] + #[serde(rename_all = "PascalCase")] + struct TestProperties { + key_1: String, + key_2: Vec, + key_3: HashMap, + } + + type TestRequest = CloudFormationCustomResourceRequest; + + #[test] + fn example_create_request() { + let data = include_bytes!("../../fixtures/example-cloudformation-custom-resource-provider-create-request.json"); + let parsed: TestRequest = serde_json::from_slice(data).unwrap(); + + match parsed { + Create(_) => (), + _ => panic!("expected Create request"), + } + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: TestRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_update_request() { + let data = include_bytes!("../../fixtures/example-cloudformation-custom-resource-provider-update-request.json"); + let parsed: TestRequest = serde_json::from_slice(data).unwrap(); + + match parsed { + Update(_) => (), + _ => panic!("expected Update request"), + } + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: TestRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_delete_request() { + let data = include_bytes!("../../fixtures/example-cloudformation-custom-resource-provider-delete-request.json"); + let parsed: TestRequest = serde_json::from_slice(data).unwrap(); + + match parsed { + Delete(_) => (), + _ => panic!("expected Delete request"), + } + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: TestRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_response() { + let data = include_bytes!("../../fixtures/example-cloudformation-custom-resource-provider-response.json"); + let parsed: CloudFormationCustomResourceResponse = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CloudFormationCustomResourceResponse = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/cloudwatch_alarms/mod.rs b/lambda-events/src/event/cloudwatch_alarms/mod.rs new file mode 100644 index 000000000..aa66a7de1 --- /dev/null +++ b/lambda-events/src/event/cloudwatch_alarms/mod.rs @@ -0,0 +1,454 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use std::collections::HashMap; + +use chrono::{DateTime, Utc}; +use serde::{ + de::{DeserializeOwned, Visitor}, + ser::Error as SerError, + Deserialize, Serialize, +}; +use serde_json::Value; + +/// `CloudWatchAlarm` is the generic outer structure of an event triggered by a CloudWatch Alarm. +/// You probably want to use `CloudWatchMetricAlarm` or `CloudWatchCompositeAlarm` if you know which kind of alarm your function is receiving. +/// For examples of events that come via CloudWatch Alarms, +/// see +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CloudWatchAlarm +where + C: DeserializeOwned, + C: Serialize, + R: DeserializeOwned, + R: Serialize, +{ + #[serde(default)] + pub account_id: Option, + #[serde(default)] + pub alarm_arn: Option, + #[serde(default)] + pub source: Option, + #[serde(default)] + pub region: Option, + pub time: DateTime, + + #[serde(default, bound = "")] + pub alarm_data: CloudWatchAlarmData, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CloudWatchMetricAlarm` is the structure of an event triggered by CloudWatch metric alarms. +pub type CloudWatchMetricAlarm = + CloudWatchAlarm; + +/// `CloudWatchCompositeAlarm` is the structure of an event triggered by CloudWatch composite alarms. +pub type CloudWatchCompositeAlarm = + CloudWatchAlarm; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CloudWatchAlarmData +where + C: DeserializeOwned, + C: Serialize, + R: DeserializeOwned, + R: Serialize, +{ + pub alarm_name: String, + #[serde(default, bound = "")] + pub state: Option>, + #[serde(default, bound = "")] + pub previous_state: Option>, + #[serde(bound = "")] + pub configuration: C, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CloudWatchAlarmState +where + R: DeserializeOwned, + R: Serialize, +{ + #[serde(default)] + pub value: CloudWatchAlarmStateValue, + pub reason: String, + #[serde(default, bound = "")] + pub reason_data: Option, + pub timestamp: DateTime, + pub actions_suppressed_by: Option, + pub actions_suppressed_reason: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CloudWatchMetricAlarmConfiguration { + #[serde(default)] + pub description: Option, + #[serde(default)] + pub metrics: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CloudWatchMetricDefinition { + pub id: String, + #[serde(default)] + pub return_data: bool, + pub metric_stat: CloudWatchMetricStatDefinition, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CloudWatchMetricStatDefinition { + #[serde(default)] + pub unit: Option, + #[serde(default)] + pub stat: Option, + pub period: u16, + pub metric: CloudWatchMetricStatMetricDefinition, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CloudWatchMetricStatMetricDefinition { + #[serde(default)] + pub namespace: Option, + pub name: String, + pub dimensions: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CloudWatchCompositeAlarmConfiguration { + pub alarm_rule: String, + pub actions_suppressor: String, + pub actions_suppressor_wait_period: u16, + pub actions_suppressor_extension_period: u16, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CloudWatchAlarmStateValue { + #[default] + Ok, + Alarm, + InsufficientData, +} + +#[non_exhaustive] +#[derive(Clone, Debug, PartialEq)] +pub enum CloudWatchAlarmStateReasonData { + Metric(CloudWatchAlarmStateReasonDataMetric), + Composite(ClodWatchAlarmStateReasonDataComposite), + Generic(Value), +} + +impl Default for CloudWatchAlarmStateReasonData { + fn default() -> Self { + Self::Generic(Value::String(String::new())) + } +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CloudWatchAlarmStateReasonDataMetric { + pub version: String, + #[serde(default)] + pub query_date: Option>, + #[serde(default)] + pub start_date: Option>, + #[serde(default)] + pub unit: Option, + #[serde(default)] + pub statistic: Option, + pub period: u16, + #[serde(default)] + pub recent_datapoints: Vec, + #[serde(default)] + pub recent_lower_thresholds: Vec, + #[serde(default)] + pub recent_upper_thresholds: Vec, + pub threshold: f64, + #[serde(default)] + pub evaluated_datapoints: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CloudWatchAlarmStateEvaluatedDatapoint { + pub timestamp: DateTime, + #[serde(default)] + pub sample_count: Option, + #[serde(default)] + pub value: Option, + #[serde(default)] + pub threshold: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ClodWatchAlarmStateReasonDataComposite { + #[serde(default)] + pub triggering_alarms: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CloudWatchAlarmStateTriggeringAlarm { + pub arn: String, + pub state: CloudWatchAlarmStateTriggeringAlarmState, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CloudWatchAlarmStateTriggeringAlarmState { + pub timestamp: DateTime, + #[serde(default)] + pub value: CloudWatchAlarmStateValue, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +impl<'de> Deserialize<'de> for CloudWatchAlarmStateReasonData { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_any(ReasonDataVisitor) + } +} + +impl Serialize for CloudWatchAlarmStateReasonData { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let r = match self { + Self::Metric(m) => serde_json::to_string(m), + Self::Composite(m) => serde_json::to_string(m), + Self::Generic(m) => serde_json::to_string(m), + }; + let s = r.map_err(|e| SerError::custom(format!("failed to serialize struct as string {e}")))?; + + serializer.serialize_str(&s) + } +} + +struct ReasonDataVisitor; + +impl Visitor<'_> for ReasonDataVisitor { + type Value = CloudWatchAlarmStateReasonData; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("a string with the alarm state reason data") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + if let Ok(metric) = serde_json::from_str::(v) { + return Ok(CloudWatchAlarmStateReasonData::Metric(metric)); + } + if let Ok(aggregate) = serde_json::from_str::(v) { + return Ok(CloudWatchAlarmStateReasonData::Composite(aggregate)); + } + Ok(CloudWatchAlarmStateReasonData::Generic(Value::String(v.to_owned()))) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "cloudwatch_alarms")] + fn example_cloudwatch_alarm_metric() { + let data = include_bytes!("../../fixtures/example-cloudwatch-alarm-metric.json"); + let parsed: CloudWatchMetricAlarm = serde_json::from_slice(data).unwrap(); + let state = parsed.alarm_data.previous_state.clone().unwrap(); + let data = state.reason_data.unwrap(); + match &data { + CloudWatchAlarmStateReasonData::Metric(d) => { + assert_eq!("1.0", d.version); + assert_eq!(5, d.evaluated_datapoints.len()); + } + _ => panic!("unexpected reason data {data:?}"), + } + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CloudWatchMetricAlarm = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "cloudwatch_alarms")] + fn example_cloudwatch_alarm_composite() { + let data = include_bytes!("../../fixtures/example-cloudwatch-alarm-composite.json"); + let parsed: CloudWatchCompositeAlarm = serde_json::from_slice(data).unwrap(); + + let state = parsed.alarm_data.state.clone().unwrap(); + let data = state.reason_data.unwrap(); + match &data { + CloudWatchAlarmStateReasonData::Composite(d) => { + assert_eq!(1, d.triggering_alarms.len()); + assert_eq!( + CloudWatchAlarmStateValue::Alarm, + d.triggering_alarms.first().unwrap().state.value + ); + } + _ => panic!("unexpected reason data {data:?}"), + } + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CloudWatchCompositeAlarm = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "cloudwatch_alarms")] + fn example_cloudwatch_alarm_composite_with_suppressor_alarm() { + let data = include_bytes!("../../fixtures/example-cloudwatch-alarm-composite-with-suppressor-alarm.json"); + let parsed: CloudWatchCompositeAlarm = serde_json::from_slice(data).unwrap(); + let state = parsed.alarm_data.state.clone().unwrap(); + assert_eq!("WaitPeriod", state.actions_suppressed_by.unwrap()); + assert_eq!( + "Actions suppressed by WaitPeriod", + state.actions_suppressed_reason.unwrap() + ); + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CloudWatchCompositeAlarm = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/cloudwatch_events/cloudtrail.rs b/lambda-events/src/event/cloudwatch_events/cloudtrail.rs new file mode 100644 index 000000000..2b4d8f378 --- /dev/null +++ b/lambda-events/src/event/cloudwatch_events/cloudtrail.rs @@ -0,0 +1,187 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AWSAPICall { + pub event_version: String, + pub user_identity: UserIdentity, + pub event_time: String, + pub event_source: String, + pub event_name: String, + pub aws_region: String, + #[serde(rename = "sourceIPAddress")] + pub source_ipaddress: String, + pub user_agent: String, + pub request_parameters: I, + pub response_elements: Option, + #[serde(default)] + pub additional_event_data: Option, + #[serde(rename = "requestID")] + pub request_id: String, + #[serde(rename = "eventID")] + pub event_id: String, + pub event_type: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionIssuer { + pub r#type: String, + pub user_name: Option, + pub principal_id: String, + pub arn: String, + pub account_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WebIdFederationData { + pub federated_provider: Option, + pub attributes: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Attributes { + pub mfa_authenticated: String, + pub creation_date: DateTime, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionContext { + pub session_issuer: Option, + pub web_id_federation_data: Option, + pub attributes: Attributes, + pub source_identity: Option, + pub ec2_role_delivery: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OnBehalfOf { + pub user_id: String, + pub identity_store_arn: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +// https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-event-reference-user-identity.html +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UserIdentity { + pub r#type: String, + pub account_id: Option, + pub arn: Option, + pub credential_id: Option, + pub invoked_by: Option, + pub principal_id: Option, + pub session_context: Option, + pub user_name: Option, + pub on_behalf_of: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod tests { + use super::AWSAPICall; + + #[test] + #[cfg(feature = "cloudwatch_events")] + fn example_cloudwatch_cloudtrail_unknown_assumed_role() { + let data = include_bytes!("../../fixtures/example-cloudwatch-cloudtrail-assumed-role.json"); + let parsed: AWSAPICall = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: AWSAPICall = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + #[test] + #[cfg(feature = "cloudwatch_events")] + fn example_cloudwatch_cloudtrail_unknown_federate() { + let data = include_bytes!("../../fixtures/example-cloudwatch-cloudtrail-unknown-federate.json"); + let parsed: AWSAPICall = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: AWSAPICall = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + #[test] + #[cfg(feature = "cloudwatch_events")] + fn example_cloudwatch_cloudtrail_assumed_role() { + let data = include_bytes!("../../fixtures/example-cloudwatch-cloudtrail-unknown-user-auth.json"); + let parsed: AWSAPICall = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: AWSAPICall = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/cloudwatch_events/codedeploy.rs b/lambda-events/src/event/cloudwatch_events/codedeploy.rs new file mode 100644 index 000000000..7542844c1 --- /dev/null +++ b/lambda-events/src/event/cloudwatch_events/codedeploy.rs @@ -0,0 +1,68 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StateChangeNotification { + pub instance_group_id: String, + pub region: String, + pub application: String, + pub deployment_id: String, + pub state: String, + pub deployment_group: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DeploymentStateChangeNotification { + pub instance_id: String, + pub region: String, + pub state: String, + pub application: String, + pub deployment_id: String, + pub instance_group_id: String, + pub deployment_group: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InstanceStateChangeNotification { + pub pipeline: String, + pub version: String, + pub state: String, + #[serde(rename = "execution-id")] + pub execution_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/cloudwatch_events/codepipeline.rs b/lambda-events/src/event/cloudwatch_events/codepipeline.rs new file mode 100644 index 000000000..a647281ea --- /dev/null +++ b/lambda-events/src/event/cloudwatch_events/codepipeline.rs @@ -0,0 +1,90 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PipelineExecutionStateChange { + pub pipeline: String, + pub version: String, + pub state: String, + #[serde(rename = "execution-id")] + pub execution_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StageExecutionStateChange { + pub pipeline: String, + pub version: String, + #[serde(rename = "execution-id")] + pub execution_id: String, + pub stage: String, + pub state: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ActionExecutionStateChange { + pub pipeline: String, + pub version: i64, + #[serde(rename = "execution-id")] + pub execution_id: String, + pub stage: String, + pub action: String, + pub state: String, + pub region: String, + #[serde(rename = "type")] + pub type_field: ActionExecutionStateChangeType, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ActionExecutionStateChangeType { + pub owner: String, + pub category: String, + pub provider: String, + pub version: i64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/cloudwatch_events/ec2.rs b/lambda-events/src/event/cloudwatch_events/ec2.rs new file mode 100644 index 000000000..ba668ac38 --- /dev/null +++ b/lambda-events/src/event/cloudwatch_events/ec2.rs @@ -0,0 +1,23 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InstanceStateChange { + #[serde(rename = "instance-id")] + pub instance_id: String, + pub state: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/cloudwatch_events/emr.rs b/lambda-events/src/event/cloudwatch_events/emr.rs new file mode 100644 index 000000000..d60573810 --- /dev/null +++ b/lambda-events/src/event/cloudwatch_events/emr.rs @@ -0,0 +1,93 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AutoScalingPolicyStateChange { + pub resource_id: String, + pub cluster_id: String, + pub state: String, + pub message: String, + pub scaling_resource_type: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ClusterStateChange { + pub severity: String, + pub state_change_reason: String, + pub name: String, + pub cluster_id: String, + pub state: String, + pub message: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InstanceGroupStateChange { + pub market: String, + pub severity: String, + pub requested_instance_count: String, + pub instance_type: String, + pub instance_group_type: String, + pub instance_group_id: String, + pub cluster_id: String, + pub running_instance_count: String, + pub state: String, + pub message: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StepStatusChange { + pub severity: String, + pub action_on_failure: String, + pub step_id: String, + pub name: String, + pub cluster_id: String, + pub state: String, + pub message: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/cloudwatch_events/gamelift.rs b/lambda-events/src/event/cloudwatch_events/gamelift.rs new file mode 100644 index 000000000..9ff2e431d --- /dev/null +++ b/lambda-events/src/event/cloudwatch_events/gamelift.rs @@ -0,0 +1,242 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +use crate::custom_serde::deserialize_nullish; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MatchmakingSearching { + pub tickets: Vec, + pub estimated_wait_millis: String, + pub r#type: String, + pub game_session_info: GameSessionInfo, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Ticket { + pub ticket_id: String, + pub start_time: String, + pub players: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Player { + pub player_id: String, + pub team: Option, + #[serde(default, deserialize_with = "deserialize_nullish")] + pub accepted: bool, + pub player_session_id: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GameSessionInfo { + pub players: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PotentialMatchCreated { + pub tickets: Vec, + pub acceptance_timeout: i64, + pub rule_evaluation_metrics: Vec, + pub acceptance_required: bool, + pub r#type: String, + pub game_session_info: GameSessionInfo, + pub match_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RuleEvaluationMetric { + pub rule_name: String, + pub passed_count: i64, + pub failed_count: i64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AcceptMatch { + pub tickets: Vec, + pub r#type: String, + pub game_session_info: GameSessionInfo, + pub match_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AcceptMatchCompleted { + pub tickets: Vec, + pub acceptance: String, + pub r#type: String, + pub game_session_info: GameSessionInfo, + pub match_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MatchmakingSucceeded { + pub tickets: Vec, + pub r#type: String, + pub game_session_info: GameSessionInfo, + pub match_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MatchmakingTimedOut { + pub reason: String, + pub tickets: Vec, + pub rule_evaluation_metrics: Vec, + pub r#type: String, + pub message: String, + pub game_session_info: GameSessionInfo, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MatchmakingCancelled { + pub reason: String, + pub tickets: Vec, + pub rule_evaluation_metrics: Vec, + pub r#type: String, + pub message: String, + pub game_session_info: GameSessionInfo, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MatchmakingFailed { + pub tickets: Vec, + pub custom_event_data: String, + pub r#type: String, + pub reason: String, + pub message: String, + pub game_session_info: GameSessionInfo, + pub match_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/cloudwatch_events/glue.rs b/lambda-events/src/event/cloudwatch_events/glue.rs new file mode 100644 index 000000000..d85eebd9c --- /dev/null +++ b/lambda-events/src/event/cloudwatch_events/glue.rs @@ -0,0 +1,172 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct JobRunStateChange { + pub job_name: String, + pub severity: String, + pub state: String, + pub job_run_id: String, + pub message: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CrawlerStarted { + pub account_id: String, + pub crawler_name: String, + pub start_time: String, + pub state: String, + pub message: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CrawlerSucceeded { + pub tables_created: String, + pub warning_message: String, + pub partitions_updated: String, + pub tables_updated: String, + pub message: String, + pub partitions_deleted: String, + pub account_id: String, + #[serde(rename = "runningTime (sec)")] + pub running_time_sec: String, + pub tables_deleted: String, + pub crawler_name: String, + pub completion_date: String, + pub state: String, + pub partitions_created: String, + pub cloud_watch_log_link: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CrawlerFailed { + pub crawler_name: String, + pub error_message: String, + pub account_id: String, + pub cloud_watch_log_link: String, + pub state: String, + pub message: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct JobRunStatus { + pub job_name: String, + pub severity: String, + pub notification_condition: NotificationCondition, + pub state: String, + pub job_run_id: String, + pub message: String, + pub started_on: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NotificationCondition { + #[serde(rename = "NotifyDelayAfter")] + pub notify_delay_after: f64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DataCatalogTableStateChange { + pub database_name: String, + pub changed_partitions: Vec, + pub type_of_change: String, + pub table_name: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DataCatalogDatabaseStateChange { + pub database_name: String, + pub type_of_change: String, + pub changed_tables: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/cloudwatch_events/health.rs b/lambda-events/src/event/cloudwatch_events/health.rs new file mode 100644 index 000000000..727c2e546 --- /dev/null +++ b/lambda-events/src/event/cloudwatch_events/health.rs @@ -0,0 +1,63 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +use std::collections::HashMap; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Event { + pub event_arn: String, + pub service: String, + pub event_type_code: String, + pub event_type_category: String, + pub start_time: String, + pub end_time: String, + pub event_description: Vec, + pub affected_entities: Option>, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EventDescription { + pub language: String, + pub latest_description: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Entity { + pub entity_value: String, + pub tags: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/cloudwatch_events/kms.rs b/lambda-events/src/event/cloudwatch_events/kms.rs new file mode 100644 index 000000000..23e124562 --- /dev/null +++ b/lambda-events/src/event/cloudwatch_events/kms.rs @@ -0,0 +1,22 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CMKEvent { + #[serde(rename = "key-id")] + pub key_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/cloudwatch_events/macie.rs b/lambda-events/src/event/cloudwatch_events/macie.rs new file mode 100644 index 000000000..a715070f2 --- /dev/null +++ b/lambda-events/src/event/cloudwatch_events/macie.rs @@ -0,0 +1,406 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +#[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] +use serde_json::Value; +use std::collections::HashMap; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Alert { + #[serde(rename = "notification-type")] + pub notification_type: String, + pub name: String, + pub tags: Vec, + pub url: String, + #[serde(rename = "alert-arn")] + pub alert_arn: String, + #[serde(rename = "risk-score")] + pub risk_score: i64, + pub trigger: Trigger, + #[serde(rename = "created-at")] + pub created_at: String, + pub actor: String, + pub summary: T, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +pub type BucketScanAlert = Alert; +pub type BucketWritableAlert = Alert; +pub type BucketContainsHighRiskObjectAlert = Alert; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Trigger { + #[serde(rename = "rule-arn")] + pub rule_arn: String, + #[serde(rename = "alert-type")] + pub alert_type: String, + #[serde(rename = "created-at")] + pub created_at: String, + pub description: String, + pub risk: i64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BucketScanSummary { + #[serde(rename = "Description")] + pub description: String, + #[serde(rename = "IP")] + pub ip: Ip, + #[serde(rename = "Time Range")] + pub time_range: Vec, + #[serde(rename = "Source ARN")] + pub source_arn: String, + #[serde(rename = "Record Count")] + pub record_count: i64, + #[serde(rename = "Location")] + pub location: Location, + #[serde(rename = "Event Count")] + pub event_count: i64, + #[serde(rename = "Events")] + pub events: HashMap, + pub recipient_account_id: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Ip { + #[serde(rename = "34.199.185.34")] + pub n34_199_185_34: i64, + #[serde(rename = "34.205.153.2")] + pub n34_205_153_2: i64, + #[serde(rename = "72.21.196.70")] + pub n72_21_196_70: i64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TimeRange { + pub count: i64, + pub start: String, + pub end: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Location { + #[serde(rename = "us-east-1")] + pub us_east_1: i64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ActionInfo { + pub count: i64, + #[serde(rename = "ISP")] + pub isp: HashMap, + pub error_code: Option>, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BucketWritableSummary { + #[serde(rename = "Description")] + pub description: String, + #[serde(rename = "Bucket")] + pub bucket: Bucket, + #[serde(rename = "Record Count")] + pub record_count: i64, + #[serde(rename = "ACL")] + pub acl: Acl, + #[serde(rename = "Event Count")] + pub event_count: i64, + #[serde(rename = "Timestamps")] + pub timestamps: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Bucket { + #[serde(rename = "secret-bucket-name")] + pub secret_bucket_name: i64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Acl { + #[serde(rename = "secret-bucket-name")] + pub secret_bucket_name: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SecretBucketName { + #[serde(rename = "Owner")] + pub owner: Owner, + #[serde(rename = "Grants")] + pub grants: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Owner { + #[serde(rename = "DisplayName")] + pub display_name: String, + #[serde(rename = "ID")] + pub id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Grant { + #[serde(rename = "Grantee")] + pub grantee: Grantee, + #[serde(rename = "Permission")] + pub permission: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Grantee { + pub r#type: String, + #[serde(rename = "URI")] + pub uri: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BucketContainsHighRiskObjectSummary { + #[serde(rename = "Description")] + pub description: String, + #[serde(rename = "Object")] + pub object: HashMap, + #[serde(rename = "Record Count")] + pub record_count: i64, + #[serde(rename = "Themes")] + pub themes: HashMap, + #[serde(rename = "Event Count")] + pub event_count: i64, + #[serde(rename = "DLP risk")] + pub dlp_risk: HashMap, + #[serde(rename = "Owner")] + pub owner: HashMap, + #[serde(rename = "Timestamps")] + pub timestamps: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AlertUpdated { + #[serde(rename = "notification-type")] + pub notification_type: String, + pub name: String, + pub tags: Vec, + pub url: String, + #[serde(rename = "alert-arn")] + pub alert_arn: String, + #[serde(rename = "risk-score")] + pub risk_score: i64, + #[serde(rename = "created-at")] + pub created_at: String, + pub actor: String, + pub trigger: UpdatedTrigger, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdatedTrigger { + #[serde(rename = "alert-type")] + pub alert_type: String, + pub features: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FeatureInfo { + pub name: String, + pub description: String, + pub narrative: String, + pub anomalous: bool, + pub multiplier: f64, + #[serde(rename = "excession_times")] + pub excession_times: Vec, + pub risk: i64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/cloudwatch_events/mod.rs b/lambda-events/src/event/cloudwatch_events/mod.rs new file mode 100644 index 000000000..da93b2bc9 --- /dev/null +++ b/lambda-events/src/event/cloudwatch_events/mod.rs @@ -0,0 +1,61 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use chrono::{DateTime, Utc}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::Value; + +pub mod cloudtrail; +pub mod codedeploy; +pub mod codepipeline; +pub mod ec2; +pub mod emr; +pub mod gamelift; +pub mod glue; +pub mod health; +pub mod kms; +pub mod macie; +pub mod opsworks; +pub mod signin; +pub mod sms; +pub mod ssm; +pub mod tag; +pub mod trustedadvisor; + +/// `CloudWatchEvent` is the outer structure of an event sent via CloudWatch Events. +/// For examples of events that come via CloudWatch Events, see +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Default, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CloudWatchEvent +where + T1: DeserializeOwned, + T1: Serialize, +{ + #[serde(default)] + pub version: Option, + #[serde(default)] + pub id: Option, + #[serde(default)] + #[serde(rename = "detail-type")] + pub detail_type: Option, + #[serde(default)] + pub source: Option, + #[serde(default)] + #[serde(rename = "account")] + pub account_id: Option, + pub time: DateTime, + #[serde(default)] + pub region: Option, + pub resources: Vec, + #[serde(bound = "")] + pub detail: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/cloudwatch_events/opsworks.rs b/lambda-events/src/event/cloudwatch_events/opsworks.rs new file mode 100644 index 000000000..aeb210a02 --- /dev/null +++ b/lambda-events/src/event/cloudwatch_events/opsworks.rs @@ -0,0 +1,98 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InstanceStateChange { + #[serde(rename = "initiated_by")] + pub initiated_by: String, + pub hostname: String, + #[serde(rename = "stack-id")] + pub stack_id: String, + #[serde(rename = "layer-ids")] + pub layer_ids: Vec, + #[serde(rename = "instance-id")] + pub instance_id: String, + #[serde(rename = "ec2-instance-id")] + pub ec2_instance_id: String, + pub status: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CommandStateChange { + #[serde(rename = "command-id")] + pub command_id: String, + #[serde(rename = "instance-id")] + pub instance_id: String, + pub r#type: String, + pub status: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DeploymentStateChange { + pub duration: i64, + #[serde(rename = "stack-id")] + pub stack_id: String, + #[serde(rename = "instance-ids")] + pub instance_ids: Vec, + #[serde(rename = "deployment-id")] + pub deployment_id: String, + pub command: String, + pub status: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Alert { + #[serde(rename = "stack-id")] + pub stack_id: String, + #[serde(rename = "instance-id")] + pub instance_id: String, + pub r#type: String, + pub message: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/cloudwatch_events/signin.rs b/lambda-events/src/event/cloudwatch_events/signin.rs new file mode 100644 index 000000000..4864f5374 --- /dev/null +++ b/lambda-events/src/event/cloudwatch_events/signin.rs @@ -0,0 +1,91 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SignIn { + pub event_version: String, + pub user_identity: UserIdentity, + pub event_time: String, + pub event_source: String, + pub event_name: String, + pub aws_region: String, + #[serde(rename = "sourceIPAddress")] + pub source_ipaddress: String, + pub user_agent: String, + pub request_parameters: Value, + pub response_elements: ResponseElements, + pub additional_event_data: AdditionalEventData, + #[serde(rename = "eventID")] + pub event_id: String, + pub event_type: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UserIdentity { + pub r#type: String, + pub principal_id: String, + pub arn: String, + pub account_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ResponseElements { + #[serde(rename = "ConsoleLogin")] + pub console_login: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AdditionalEventData { + #[serde(rename = "LoginTo")] + pub login_to: String, + #[serde(rename = "MobileVersion")] + pub mobile_version: String, + #[serde(rename = "MFAUsed")] + pub mfaused: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/cloudwatch_events/sms.rs b/lambda-events/src/event/cloudwatch_events/sms.rs new file mode 100644 index 000000000..3a63acae9 --- /dev/null +++ b/lambda-events/src/event/cloudwatch_events/sms.rs @@ -0,0 +1,28 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct JobStateChange { + pub state: String, + #[serde(rename = "replication-run-id")] + pub replication_run_id: String, + #[serde(rename = "replication-job-id")] + pub replication_job_id: String, + #[serde(rename = "ami-id")] + pub ami_id: Option, + pub version: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/cloudwatch_events/ssm.rs b/lambda-events/src/event/cloudwatch_events/ssm.rs new file mode 100644 index 000000000..d71f03e45 --- /dev/null +++ b/lambda-events/src/event/cloudwatch_events/ssm.rs @@ -0,0 +1,382 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +use std::collections::HashMap; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EC2AutomationStepStatusChange { + #[serde(rename = "ExecutionId")] + pub execution_id: String, + #[serde(rename = "Definition")] + pub definition: String, + #[serde(rename = "DefinitionVersion")] + pub definition_version: f64, + #[serde(rename = "Status")] + pub status: String, + #[serde(rename = "EndTime")] + pub end_time: String, + #[serde(rename = "StartTime")] + pub start_time: String, + #[serde(rename = "Time")] + pub time: f64, + #[serde(rename = "StepName")] + pub step_name: String, + #[serde(rename = "Action")] + pub action: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EC2AutomationExecutionStatusChange { + #[serde(rename = "ExecutionId")] + pub execution_id: String, + #[serde(rename = "Definition")] + pub definition: String, + #[serde(rename = "DefinitionVersion")] + pub definition_version: f64, + #[serde(rename = "Status")] + pub status: String, + #[serde(rename = "StartTime")] + pub start_time: String, + #[serde(rename = "EndTime")] + pub end_time: String, + #[serde(rename = "Time")] + pub time: f64, + #[serde(rename = "ExecutedBy")] + pub executed_by: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StateChange { + pub state: String, + pub at_time: String, + pub next_transition_time: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ConfigurationComplianceStateChange { + #[serde(rename = "last-runtime")] + pub last_runtime: Option, + #[serde(rename = "compliance-status")] + pub compliance_status: String, + #[serde(rename = "resource-type")] + pub resource_type: String, + #[serde(rename = "resource-id")] + pub resource_id: String, + #[serde(rename = "compliance-type")] + pub compliance_type: String, + #[serde(rename = "patch-baseline-id")] + pub patch_baseline_id: Option, + pub serverity: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MaintenanceWindowTargetRegistration { + #[serde(rename = "window-target-id")] + pub window_target_id: String, + #[serde(rename = "window-id")] + pub window_id: String, + pub status: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MaintenanceWindowExecutionStateChange { + #[serde(rename = "start-time")] + pub start_time: String, + #[serde(rename = "end-time")] + pub end_time: String, + #[serde(rename = "window-id")] + pub window_id: String, + #[serde(rename = "window-execution-id")] + pub window_execution_id: String, + pub status: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MaintenanceWindowTaskExecutionStateChange { + #[serde(rename = "start-time")] + pub start_time: String, + #[serde(rename = "task-execution-id")] + pub task_execution_id: String, + #[serde(rename = "end-time")] + pub end_time: String, + #[serde(rename = "window-id")] + pub window_id: String, + #[serde(rename = "window-execution-id")] + pub window_execution_id: String, + pub status: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MaintenanceWindowTaskTargetInvocationStateChange { + #[serde(rename = "start-time")] + pub start_time: String, + #[serde(rename = "end-time")] + pub end_time: String, + #[serde(rename = "window-id")] + pub window_id: String, + #[serde(rename = "window-execution-id")] + pub window_execution_id: String, + #[serde(rename = "task-execution-id")] + pub task_execution_id: String, + #[serde(rename = "window-target-id")] + pub window_target_id: String, + pub status: String, + #[serde(rename = "owner-information")] + pub owner_information: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MaintenanceWindowStateChange { + #[serde(rename = "window-id")] + pub window_id: String, + pub status: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ParameterStoreStateChange { + pub operation: String, + pub name: String, + pub r#type: String, + pub description: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EC2CommandStatusChange { + #[serde(rename = "command-id")] + pub command_id: String, + #[serde(rename = "document-name")] + pub document_name: String, + #[serde(rename = "expire-after")] + pub expire_after: String, + pub parameters: HashMap, + #[serde(rename = "requested-date-time")] + pub requested_date_time: String, + pub status: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EC2CommandInvocationStatusChange { + #[serde(rename = "command-id")] + pub command_id: String, + #[serde(rename = "document-name")] + pub document_name: String, + #[serde(rename = "instance-id")] + pub instance_id: String, + #[serde(rename = "requested-date-time")] + pub requested_date_time: String, + pub status: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EC2StateManagerAssociationStateChange { + #[serde(rename = "association-id")] + pub association_id: String, + #[serde(rename = "document-name")] + pub document_name: String, + #[serde(rename = "association-version")] + pub association_version: String, + #[serde(rename = "document-version")] + pub document_version: String, + pub targets: String, + #[serde(rename = "creation-date")] + pub creation_date: String, + #[serde(rename = "last-successful-execution-date")] + pub last_successful_execution_date: String, + #[serde(rename = "last-execution-date")] + pub last_execution_date: String, + #[serde(rename = "last-updated-date")] + pub last_updated_date: String, + pub status: String, + #[serde(rename = "association-status-aggregated-count")] + pub association_status_aggregated_count: String, + #[serde(rename = "schedule-expression")] + pub schedule_expression: String, + #[serde(rename = "association-cwe-version")] + pub association_cwe_version: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EC2StateManagerInstanceAssociationStateChange { + #[serde(rename = "association-id")] + pub association_id: String, + #[serde(rename = "instance-id")] + pub instance_id: String, + #[serde(rename = "document-name")] + pub document_name: String, + #[serde(rename = "document-version")] + pub document_version: String, + pub targets: String, + #[serde(rename = "creation-date")] + pub creation_date: String, + #[serde(rename = "last-successful-execution-date")] + pub last_successful_execution_date: String, + #[serde(rename = "last-execution-date")] + pub last_execution_date: String, + pub status: String, + #[serde(rename = "detailed-status")] + pub detailed_status: String, + #[serde(rename = "error-code")] + pub error_code: String, + #[serde(rename = "execution-summary")] + pub execution_summary: String, + #[serde(rename = "output-url")] + pub output_url: String, + #[serde(rename = "instance-association-cwe-version")] + pub instance_association_cwe_version: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/cloudwatch_events/tag.rs b/lambda-events/src/event/cloudwatch_events/tag.rs new file mode 100644 index 000000000..076af690b --- /dev/null +++ b/lambda-events/src/event/cloudwatch_events/tag.rs @@ -0,0 +1,29 @@ +#[cfg(feature = "builders")] +use bon::Builder; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TagChangeOnResource { + #[serde(rename = "changed-tag-keys")] + pub changed_tag_keys: Vec, + pub service: String, + #[serde(rename = "resource-type")] + pub resource_type: String, + pub version: i64, + pub tags: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/cloudwatch_events/trustedadvisor.rs b/lambda-events/src/event/cloudwatch_events/trustedadvisor.rs new file mode 100644 index 000000000..f852c9230 --- /dev/null +++ b/lambda-events/src/event/cloudwatch_events/trustedadvisor.rs @@ -0,0 +1,29 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +use std::collections::HashMap; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CheckItemRefreshNotification { + #[serde(rename = "check-name")] + pub check_name: String, + #[serde(rename = "check-item-detail")] + pub check_item_detail: HashMap, + pub status: String, + #[serde(rename = "resource_id")] + pub resource_id: String, + pub uuid: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/cloudwatch_logs/mod.rs b/lambda-events/src/event/cloudwatch_logs/mod.rs new file mode 100644 index 000000000..70a9361aa --- /dev/null +++ b/lambda-events/src/event/cloudwatch_logs/mod.rs @@ -0,0 +1,200 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{ + de::{Error, MapAccess, Visitor}, + ser::{Error as SeError, SerializeStruct}, + Deserialize, Deserializer, Serialize, Serializer, +}; +#[cfg(feature = "catch-all-fields")] +#[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] +use serde_json::Value; + +use std::{fmt, io::BufReader}; + +/// `LogsEvent` represents the raw event sent by CloudWatch +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct LogsEvent { + /// `aws_logs` is gzipped and base64 encoded, it needs a custom deserializer + #[serde(rename = "awslogs")] + pub aws_logs: AwsLogs, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `AwsLogs` is an unmarshaled, ungzipped, CloudWatch logs event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct AwsLogs { + /// `data` is the log data after is decompressed + pub data: LogData, +} + +/// `LogData` represents the logs group event information +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LogData { + /// Owner of the log event + pub owner: String, + /// Log group where the event was published + pub log_group: String, + /// Log stream where the event was published + pub log_stream: String, + /// Filters applied to the event + pub subscription_filters: Vec, + /// Type of event + pub message_type: String, + /// Entries in the log batch + pub log_events: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `LogEntry` represents a log entry from cloudwatch logs +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct LogEntry { + /// Unique id for the entry + pub id: String, + /// Time when the event was published + pub timestamp: i64, + /// Message published in the application log + pub message: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +impl<'de> Deserialize<'de> for AwsLogs { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct AwsLogsVisitor; + + impl<'de> Visitor<'de> for AwsLogsVisitor { + type Value = AwsLogs; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a base64 gzipped string") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + use base64::Engine; + + let mut data = None; + while let Some(key) = map.next_key()? { + match key { + "data" => { + let bytes = map.next_value::().and_then(|string| { + base64::engine::general_purpose::STANDARD + .decode(string) + .map_err(Error::custom) + })?; + + let bytes = flate2::read::GzDecoder::new(&bytes[..]); + let mut de = serde_json::Deserializer::from_reader(BufReader::new(bytes)); + data = Some(LogData::deserialize(&mut de).map_err(Error::custom)?); + } + _ => return Err(Error::unknown_field(key, FIELDS)), + } + } + + let data = data.ok_or_else(|| Error::missing_field("data"))?; + Ok(AwsLogs { data }) + } + } + + const FIELDS: &[&str] = &["data"]; + deserializer.deserialize_struct("AwsLogs", FIELDS, AwsLogsVisitor) + } +} + +impl Serialize for AwsLogs { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let base = base64::write::EncoderWriter::new(Vec::new(), &base64::engine::general_purpose::STANDARD_NO_PAD); + let mut gzip = flate2::write::GzEncoder::new(base, flate2::Compression::default()); + + serde_json::to_writer(&mut gzip, &self.data).map_err(SeError::custom)?; + let mut base = gzip.finish().map_err(SeError::custom)?; + let data = base.finish().map_err(SeError::custom)?; + let string = std::str::from_utf8(data.as_slice()).map_err(SeError::custom)?; + + let mut state = serializer.serialize_struct("AwsLogs", 1)?; + state.serialize_field("data", &string)?; + state.end() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "cloudwatch_logs")] + fn test_deserialize_example() { + let json = r#"{ + "awslogs": { + "data": "H4sIAFETomIAA12Ry27bMBBF9/4KQuiyqsQ36Z2DqEGBGC0sdRUHAS0NExV6uCJVNw3y76Fkx03CFTH3cubwztMChRO14Jy5h+JxD9ESRZerYnW3zvJ8dZVFn4+W/tDBMImYUMaFVDrF5FVs+vuroR/3k56Yg0sa0+4qk0D50MddX8Ev98aa+wFMO3lJinWS0gTT5ObT9arI8uJWM2uUkMCpZIxiorGRtsQMiOXCgHxt5MadK4d67+u++1o3HgYXWt7M4my4nhmOw+7Kph+rg/HlQwBwM1M0W2//c2V/oPPvmzydb7OpriZqygQhFItUa6GlUkymgrNUS5EKpQhRfMpGCEzC/xgWjCpNOBMn8nM3X4fcvWmn2DDnhGNFWXiffvCdtjON3mQ/vm8KtIHfY3j6rVoiEdaxsxZizLSJd4KRWGFrYwIKqBSVMtZu/eU4mCmoJWLii2KodVt/UTcNVOiNJEMdbf0a2n54RHn9DwKYJmh9EYrmLzoJPx2EwfJY33bRmfb5mOjiefECiB5LsVgCAAA=" + } +}"#; + let event: LogsEvent = serde_json::from_str(json).expect("failed to deserialize"); + + let data = event.clone().aws_logs.data; + assert_eq!("DATA_MESSAGE", data.message_type); + assert_eq!("123456789012", data.owner); + assert_eq!("/aws/lambda/echo-nodejs", data.log_group); + assert_eq!("2019/03/13/[$LATEST]94fa867e5374431291a7fc14e2f56ae7", data.log_stream); + assert_eq!(1, data.subscription_filters.len()); + assert_eq!("LambdaStream_cloudwatchlogs-node", data.subscription_filters[0]); + assert_eq!(1, data.log_events.len()); + assert_eq!( + "34622316099697884706540976068822859012661220141643892546", + data.log_events[0].id + ); + assert_eq!(1552518348220, data.log_events[0].timestamp); + assert_eq!("REPORT RequestId: 6234bffe-149a-b642-81ff-2e8e376d8aff\tDuration: 46.84 ms\tBilled Duration: 47 ms \tMemory Size: 192 MB\tMax Memory Used: 72 MB\t\n", data.log_events[0].message); + + let new_json: String = serde_json::to_string_pretty(&event).unwrap(); + let new_event: LogsEvent = serde_json::from_str(&new_json).expect("failed to deserialize"); + assert_eq!(new_event, event); + } + + #[test] + #[cfg(feature = "cloudwatch_logs")] + fn example_cloudwatch_logs_event() { + let data = include_bytes!("../../fixtures/example-cloudwatch_logs-event.json"); + let parsed: LogsEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: LogsEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/code_commit/mod.rs b/lambda-events/src/event/code_commit/mod.rs new file mode 100644 index 000000000..dc6574a33 --- /dev/null +++ b/lambda-events/src/event/code_commit/mod.rs @@ -0,0 +1,128 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +use crate::custom_serde::deserialize_nullish; + +/// `CodeCommitEvent` represents a CodeCommit event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeCommitEvent { + #[serde(rename = "Records")] + pub records: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +pub type CodeCommitEventTime = DateTime; + +/// `CodeCommitRecord` represents a CodeCommit record +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeCommitRecord { + #[serde(default)] + pub event_id: Option, + #[serde(default)] + pub event_version: Option, + pub event_time: CodeCommitEventTime, + #[serde(default)] + pub event_trigger_name: Option, + pub event_part_number: u64, + #[serde(rename = "codecommit")] + pub code_commit: CodeCommitCodeCommit, + #[serde(default)] + pub event_name: Option, + /// nolint: stylecheck + #[serde(default)] + pub event_trigger_config_id: Option, + #[serde(default)] + #[serde(rename = "eventSourceARN")] + pub event_source_arn: Option, + #[serde(default)] + #[serde(rename = "userIdentityARN")] + pub user_identity_arn: Option, + #[serde(default)] + pub event_source: Option, + #[serde(default)] + pub aws_region: Option, + pub event_total_parts: u64, + pub custom_data: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CodeCommitCodeCommit` represents a CodeCommit object in a record +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeCommitCodeCommit { + pub references: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CodeCommitReference` represents a Reference object in a CodeCommit object +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeCommitReference { + #[serde(default)] + pub commit: Option, + #[serde(default)] + pub ref_: Option, + #[serde(default, deserialize_with = "deserialize_nullish")] + pub created: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "code_commit")] + fn example_code_commit_event() { + let data = include_bytes!("../../fixtures/example-code_commit-event.json"); + let parsed: CodeCommitEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CodeCommitEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/codebuild/mod.rs b/lambda-events/src/event/codebuild/mod.rs new file mode 100644 index 000000000..26ad4c165 --- /dev/null +++ b/lambda-events/src/event/codebuild/mod.rs @@ -0,0 +1,328 @@ +use crate::{ + custom_serde::{codebuild_time, CodeBuildNumber}, + encodings::{MinuteDuration, SecondDuration}, +}; +#[cfg(feature = "builders")] +use bon::Builder; +use chrono::{DateTime, Utc}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::Value; + +pub type CodeBuildPhaseStatus = String; + +pub type CodeBuildPhaseType = String; + +/// `CodeBuildEvent` is documented at: +/// +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeBuildEvent { + /// AccountID is the id of the AWS account from which the event originated. + #[serde(default)] + #[serde(rename = "account")] + pub account_id: Option, + /// Region is the AWS region from which the event originated. + #[serde(default)] + pub region: Option, + /// DetailType informs the schema of the Detail field. For build state-change + /// events, the value will be CodeBuildStateChangeDetailType. For phase-change + /// events, it will be CodeBuildPhaseChangeDetailType. + #[serde(default)] + #[serde(rename = "detail-type")] + pub detail_type: Option, + /// Source should be equal to CodeBuildEventSource. + #[serde(default)] + pub source: Option, + /// Version is the version of the event's schema. + #[serde(default)] + pub version: Option, + /// Time is the event's timestamp. + pub time: DateTime, + /// ID is the GUID of this event. + #[serde(default)] + pub id: Option, + /// Resources is a list of ARNs of CodeBuild builds that this event pertains to. + pub resources: Vec, + /// Detail contains information specific to a build state-change or + /// build phase-change event. + pub detail: CodeBuildEventDetail, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CodeBuildEventDetail` represents the all details related to the code build event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeBuildEventDetail { + #[serde(rename = "build-status")] + pub build_status: Option, + #[serde(default)] + #[serde(rename = "project-name")] + pub project_name: Option, + #[serde(default)] + #[serde(rename = "build-id")] + pub build_id: Option, + #[serde(rename = "additional-information")] + pub additional_information: CodeBuildEventAdditionalInformation, + #[serde(rename = "current-phase")] + pub current_phase: Option, + #[serde(default)] + #[serde(rename = "current-phase-context")] + pub current_phase_context: Option, + #[serde(default)] + pub version: Option, + #[serde(rename = "completed-phase-status")] + pub completed_phase_status: Option, + #[serde(rename = "completed-phase")] + pub completed_phase: Option, + #[serde(default)] + #[serde(rename = "completed-phase-context")] + pub completed_phase_context: Option, + #[serde(rename = "completed-phase-duration-seconds")] + pub completed_phase_duration: Option, + #[serde(rename = "completed-phase-start")] + #[serde(default)] + #[serde(with = "codebuild_time::optional_time")] + pub completed_phase_start: Option, + #[serde(rename = "completed-phase-end")] + #[serde(default)] + #[serde(with = "codebuild_time::optional_time")] + pub completed_phase_end: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CodeBuildEventAdditionalInformation` represents additional information to the code build event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeBuildEventAdditionalInformation { + pub artifact: CodeBuildArtifact, + pub environment: CodeBuildEnvironment, + #[serde(rename = "timeout-in-minutes")] + pub timeout: MinuteDuration, + #[serde(rename = "build-complete")] + pub build_complete: bool, + #[serde(rename = "build-number")] + pub build_number: Option, + #[serde(default)] + pub initiator: Option, + #[serde(rename = "build-start-time")] + #[serde(with = "codebuild_time::str_time")] + pub build_start_time: CodeBuildTime, + pub source: CodeBuildSource, + #[serde(default)] + #[serde(rename = "source-version")] + pub source_version: Option, + pub logs: CodeBuildLogs, + pub phases: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CodeBuildArtifact` represents the artifact provided to build +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeBuildArtifact { + #[serde(default)] + #[serde(rename = "md5sum")] + pub md5_sum: Option, + #[serde(default)] + #[serde(rename = "sha256sum")] + pub sha256_sum: Option, + #[serde(default)] + pub location: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CodeBuildEnvironment` represents the environment for a build +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeBuildEnvironment { + #[serde(default)] + pub image: Option, + #[serde(rename = "privileged-mode")] + pub privileged_mode: bool, + #[serde(default)] + #[serde(rename = "compute-type")] + pub compute_type: Option, + #[serde(default)] + pub type_: Option, + #[serde(rename = "environment-variables")] + pub environment_variables: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CodeBuildEnvironmentVariable` encapsulate environment variables for the code build +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeBuildEnvironmentVariable { + /// Name is the name of the environment variable. + #[serde(default)] + pub name: Option, + /// Type is PLAINTEXT or PARAMETER_STORE. + #[serde(default)] + pub type_: Option, + /// Value is the value of the environment variable. + #[serde(default)] + pub value: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CodeBuildSource` represent the code source will be build +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeBuildSource { + #[serde(default)] + pub location: Option, + #[serde(default)] + pub type_: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CodeBuildLogs` gives the log details of a code build +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeBuildLogs { + #[serde(default)] + #[serde(rename = "group-name")] + pub group_name: Option, + #[serde(default)] + #[serde(rename = "stream-name")] + pub stream_name: Option, + #[serde(default)] + #[serde(rename = "deep-link")] + pub deep_link: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CodeBuildPhase` represents the phase of a build and its details +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeBuildPhase +where + T1: DeserializeOwned, + T1: Serialize, +{ + #[serde(bound = "")] + #[serde(rename = "phase-context")] + pub phase_context: Option>, + #[serde(rename = "start-time")] + #[serde(with = "codebuild_time::str_time")] + pub start_time: CodeBuildTime, + #[serde(rename = "end-time")] + #[serde(default)] + #[serde(with = "codebuild_time::optional_time")] + pub end_time: Option, + #[serde(rename = "duration-in-seconds")] + pub duration: Option, + #[serde(rename = "phase-type")] + pub phase_type: CodeBuildPhaseType, + #[serde(rename = "phase-status")] + pub phase_status: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +pub type CodeBuildTime = DateTime; + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "codebuild")] + fn example_codebuild_phase_change() { + let data = include_bytes!("../../fixtures/example-codebuild-phase-change.json"); + let parsed: CodeBuildEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CodeBuildEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "codebuild")] + fn example_codebuild_state_change() { + let data = include_bytes!("../../fixtures/example-codebuild-state-change.json"); + let parsed: CodeBuildEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CodeBuildEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/codedeploy/mod.rs b/lambda-events/src/event/codedeploy/mod.rs new file mode 100644 index 000000000..7f3944b8d --- /dev/null +++ b/lambda-events/src/event/codedeploy/mod.rs @@ -0,0 +1,146 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +pub type CodeDeployDeploymentState = String; + +/// `CodeDeployEvent` is documented at: +/// +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeDeployEvent { + /// AccountID is the id of the AWS account from which the event originated. + #[serde(default)] + #[serde(rename = "account")] + pub account_id: Option, + /// Region is the AWS region from which the event originated. + #[serde(default)] + pub region: Option, + /// DetailType informs the schema of the Detail field. For deployment state-change + /// events, the value should be equal to CodeDeployDeploymentEventDetailType. + /// For instance state-change events, the value should be equal to + /// CodeDeployInstanceEventDetailType. + #[serde(default)] + #[serde(rename = "detail-type")] + pub detail_type: Option, + /// Source should be equal to CodeDeployEventSource. + #[serde(default)] + pub source: Option, + /// Version is the version of the event's schema. + #[serde(default)] + pub version: Option, + /// Time is the event's timestamp. + pub time: DateTime, + /// ID is the GUID of this event. + #[serde(default)] + pub id: Option, + /// Resources is a list of ARNs of CodeDeploy applications and deployment + /// groups that this event pertains to. + pub resources: Vec, + /// Detail contains information specific to a deployment event. + pub detail: CodeDeployEventDetail, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodeDeployEventDetail { + /// InstanceGroupID is the ID of the instance group. + #[serde(default)] + pub instance_group_id: Option, + /// InstanceID is the id of the instance. This field is non-empty only if + /// the DetailType of the complete event is CodeDeployInstanceEventDetailType. + pub instance_id: Option, + /// Region is the AWS region that the event originated from. + #[serde(default)] + pub region: Option, + /// Application is the name of the CodeDeploy application. + #[serde(default)] + pub application: Option, + /// DeploymentID is the id of the deployment. + #[serde(default)] + pub deployment_id: Option, + /// State is the new state of the deployment. + pub state: CodeDeployDeploymentState, + /// DeploymentGroup is the name of the deployment group. + #[serde(default)] + pub deployment_group: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Deserialize, Serialize, Eq, PartialEq)] +#[serde(rename_all = "PascalCase")] +pub struct CodeDeployLifecycleEvent { + pub deployment_id: String, + pub lifecycle_event_hook_execution_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "codedeploy")] + fn example_codedeploy_lifecycle_event() { + let data = include_bytes!("../../fixtures/example-codedeploy-lifecycle-event.json"); + let parsed: CodeDeployLifecycleEvent = serde_json::from_slice(data).unwrap(); + + assert_eq!(parsed.deployment_id, "d-deploymentId".to_string()); + assert_eq!(parsed.lifecycle_event_hook_execution_id, "eyJlbmNyeXB0ZWREYXRhIjoiY3VHU2NjdkJXUTJQUENVd2dkYUNGRVg0dWlpME9UWVdHTVhZcDRXVW5LYUVKc21EaUFPMkNLNXMwMWFrNDlYVStlbXdRb29xS3NJTUNVQ3RYRGFZSXc1VTFwUllvMDhmMzdlbDZFeDVVdjZrNFc0eU5waGh6YTRvdkNWcmVveVR6OWdERlM2SmlIYW1TZz09IiwiaXZQYXJhbWV0ZXJTcGVjIjoiTm1ZNFR6RzZxQVhHamhhLyIsIm1hdGVyaWFsU2V0U2VyaWFsIjoxfQ==".to_string()); + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CodeDeployLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "codedeploy")] + fn example_codedeploy_deployment_event() { + let data = include_bytes!("../../fixtures/example-codedeploy-deployment-event.json"); + let parsed: CodeDeployEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CodeDeployEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "codedeploy")] + fn example_codedeploy_instance_event() { + let data = include_bytes!("../../fixtures/example-codedeploy-instance-event.json"); + let parsed: CodeDeployEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CodeDeployEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/codepipeline_cloudwatch/mod.rs b/lambda-events/src/event/codepipeline_cloudwatch/mod.rs new file mode 100644 index 000000000..5ffe7a9ca --- /dev/null +++ b/lambda-events/src/event/codepipeline_cloudwatch/mod.rs @@ -0,0 +1,147 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +pub type CodePipelineStageState = String; + +pub type CodePipelineState = String; + +pub type CodePipelineActionState = String; + +/// CodePipelineEvent is documented at: +/// +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodePipelineCloudWatchEvent { + /// Version is the version of the event's schema. + #[serde(default)] + pub version: Option, + /// ID is the GUID of this event. + #[serde(default)] + pub id: Option, + /// DetailType informs the schema of the Detail field. For deployment state-change + /// events, the value should be equal to CodePipelineDeploymentEventDetailType. + /// For instance state-change events, the value should be equal to + /// CodePipelineInstanceEventDetailType. + #[serde(default)] + #[serde(rename = "detail-type")] + pub detail_type: Option, + /// Source should be equal to CodePipelineEventSource. + #[serde(default)] + pub source: Option, + /// AccountID is the id of the AWS account from which the event originated. + #[serde(default)] + #[serde(rename = "account")] + pub account_id: Option, + /// Time is the event's timestamp. + pub time: DateTime, + /// Region is the AWS region from which the event originated. + #[serde(default)] + pub region: Option, + /// Resources is a list of ARNs of CodePipeline applications and deployment + /// groups that this event pertains to. + pub resources: Vec, + /// Detail contains information specific to a deployment event. + pub detail: CodePipelineEventDetail, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodePipelineEventDetail { + #[serde(default)] + pub pipeline: Option, + /// From live testing this is always int64 not string as documented + pub version: i64, + #[serde(default)] + #[serde(rename = "execution-id")] + pub execution_id: Option, + #[serde(default)] + pub stage: Option, + #[serde(default)] + pub action: Option, + pub state: CodePipelineState, + #[serde(default)] + pub region: Option, + pub type_: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodePipelineEventDetailType { + #[serde(default)] + pub owner: Option, + #[serde(default)] + pub category: Option, + #[serde(default)] + pub provider: Option, + /// From published EventBridge schema registry this is always int64 not string as documented + pub version: i64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "codepipeline_cloudwatch")] + fn example_codepipeline_action_execution_stage_change_event() { + let data = include_bytes!("../../fixtures/example-codepipeline-action-execution-stage-change-event.json"); + let parsed: CodePipelineCloudWatchEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CodePipelineCloudWatchEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "codepipeline_cloudwatch")] + fn example_codepipeline_execution_stage_change_event() { + let data = include_bytes!("../../fixtures/example-codepipeline-execution-stage-change-event.json"); + let parsed: CodePipelineCloudWatchEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CodePipelineCloudWatchEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "codepipeline_cloudwatch")] + fn example_codepipeline_execution_state_change_event() { + let data = include_bytes!("../../fixtures/example-codepipeline-execution-state-change-event.json"); + let parsed: CodePipelineCloudWatchEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CodePipelineCloudWatchEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/codepipeline_job/mod.rs b/lambda-events/src/event/codepipeline_job/mod.rs new file mode 100644 index 000000000..388accca5 --- /dev/null +++ b/lambda-events/src/event/codepipeline_job/mod.rs @@ -0,0 +1,243 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +/// `CodePipelineJobEvent` contains data from an event sent from AWS CodePipeline +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodePipelineJobEvent { + #[serde(rename = "CodePipeline.job")] + pub code_pipeline_job: CodePipelineJob, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CodePipelineJob` represents a job from an AWS CodePipeline event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodePipelineJob { + #[serde(default)] + pub id: Option, + #[serde(default)] + pub account_id: Option, + pub data: CodePipelineData, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CodePipelineData` represents a job from an AWS CodePipeline event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodePipelineData { + pub action_configuration: CodePipelineActionConfiguration, + pub input_artifacts: Vec, + #[serde(rename = "outputArtifacts")] + pub out_put_artifacts: Vec, + pub artifact_credentials: CodePipelineArtifactCredentials, + #[serde(default)] + pub continuation_token: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CodePipelineActionConfiguration` represents an Action Configuration +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodePipelineActionConfiguration { + pub configuration: CodePipelineConfiguration, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CodePipelineConfiguration` represents a configuration for an Action Configuration +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodePipelineConfiguration { + #[serde(default)] + #[serde(rename = "FunctionName")] + pub function_name: Option, + #[serde(default)] + #[serde(rename = "UserParameters")] + pub user_parameters: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CodePipelineInputArtifact` represents an input artifact +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodePipelineInputArtifact { + pub location: CodePipelineInputLocation, + pub revision: Option, + #[serde(default)] + pub name: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CodePipelineInputLocation` represents a input location +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodePipelineInputLocation { + pub s3_location: CodePipelineS3Location, + #[serde(default)] + #[serde(rename = "type")] + pub location_type: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CodePipelineS3Location` represents an s3 input location +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodePipelineS3Location { + #[serde(default)] + pub bucket_name: Option, + #[serde(default)] + pub object_key: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CodePipelineOutputArtifact` represents an output artifact +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodePipelineOutputArtifact { + pub location: CodePipelineInputLocation, + pub revision: Option, + #[serde(default)] + pub name: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CodePipelineOutputLocation` represents a output location +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodePipelineOutputLocation { + pub s3_location: CodePipelineS3Location, + #[serde(default)] + #[serde(rename = "type")] + pub location_type: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CodePipelineArtifactCredentials` represents CodePipeline artifact credentials +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CodePipelineArtifactCredentials { + #[serde(default)] + pub secret_access_key: Option, + #[serde(default)] + pub session_token: Option, + #[serde(default)] + pub access_key_id: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "codepipeline_job")] + fn example_codepipeline_job_event() { + let data = include_bytes!("../../fixtures/example-codepipeline_job-event.json"); + let parsed: CodePipelineJobEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CodePipelineJobEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/cognito/mod.rs b/lambda-events/src/event/cognito/mod.rs new file mode 100644 index 000000000..2826fc6cd --- /dev/null +++ b/lambda-events/src/event/cognito/mod.rs @@ -0,0 +1,1514 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; + +use crate::custom_serde::deserialize_nullish; + +/// `CognitoEvent` contains data from an event sent from AWS Cognito Sync +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEvent { + #[serde(default)] + pub dataset_name: Option, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub dataset_records: HashMap, + #[serde(default)] + pub event_type: Option, + #[serde(default)] + pub identity_id: Option, + #[serde(default)] + pub identity_pool_id: Option, + #[serde(default)] + pub region: Option, + pub version: i64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoDatasetRecord` represents a record from an AWS Cognito Sync event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoDatasetRecord { + #[serde(default)] + pub new_value: Option, + #[serde(default)] + pub old_value: Option, + #[serde(default)] + pub op: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoEventUserPoolsPreSignup` is sent by AWS Cognito User Pools when a user attempts to register +/// (sign up), allowing a Lambda to perform custom validation to accept or deny the registration request +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsPreSignup { + #[serde(rename = "CognitoEventUserPoolsHeader")] + #[serde(flatten)] + pub cognito_event_user_pools_header: CognitoEventUserPoolsHeader, + pub request: CognitoEventUserPoolsPreSignupRequest, + pub response: CognitoEventUserPoolsPreSignupResponse, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)] +pub enum CognitoEventUserPoolsPreSignupTriggerSource { + #[serde(rename = "PreSignUp_SignUp")] + #[default] + SignUp, + #[serde(rename = "PreSignUp_AdminCreateUser")] + AdminCreateUser, + #[serde(rename = "PreSignUp_ExternalProvider")] + ExternalProvider, +} + +/// `CognitoEventUserPoolsPreAuthentication` is sent by AWS Cognito User Pools when a user submits their information +/// to be authenticated, allowing you to perform custom validations to accept or deny the sign in request. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsPreAuthentication { + #[serde(rename = "CognitoEventUserPoolsHeader")] + #[serde(flatten)] + pub cognito_event_user_pools_header: + CognitoEventUserPoolsHeader, + pub request: CognitoEventUserPoolsPreAuthenticationRequest, + pub response: CognitoEventUserPoolsPreAuthenticationResponse, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)] +pub enum CognitoEventUserPoolsPreAuthenticationTriggerSource { + #[serde(rename = "PreAuthentication_Authentication")] + #[default] + Authentication, +} + +/// `CognitoEventUserPoolsPostConfirmation` is sent by AWS Cognito User Pools after a user is confirmed, +/// allowing the Lambda to send custom messages or add custom logic. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsPostConfirmation +where + T: DeserializeOwned, + T: Serialize, +{ + #[serde(rename = "CognitoEventUserPoolsHeader")] + #[serde(flatten)] + pub cognito_event_user_pools_header: + CognitoEventUserPoolsHeader, + pub request: CognitoEventUserPoolsPostConfirmationRequest, + #[serde(bound = "")] + pub response: T, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)] +pub enum CognitoEventUserPoolsPostConfirmationTriggerSource { + #[serde(rename = "PostConfirmation_ConfirmForgotPassword")] + ConfirmForgotPassword, + #[serde(rename = "PostConfirmation_ConfirmSignUp")] + #[default] + ConfirmSignUp, +} + +/// `CognitoEventUserPoolsPreTokenGen` is sent by AWS Cognito User Pools when a user attempts to retrieve +/// credentials, allowing a Lambda to perform insert, suppress or override claims +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsPreTokenGen { + #[serde(rename = "CognitoEventUserPoolsHeader")] + #[serde(flatten)] + pub cognito_event_user_pools_header: CognitoEventUserPoolsHeader, + pub request: CognitoEventUserPoolsPreTokenGenRequest, + pub response: CognitoEventUserPoolsPreTokenGenResponse, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)] +pub enum CognitoEventUserPoolsPreTokenGenTriggerSource { + #[serde(rename = "TokenGeneration_HostedAuth")] + HostedAuth, + #[serde(rename = "TokenGeneration_Authentication")] + #[default] + Authentication, + #[serde(rename = "TokenGeneration_NewPasswordChallenge")] + NewPasswordChallenge, + #[serde(rename = "TokenGeneration_AuthenticateDevice")] + AuthenticateDevice, + #[serde(rename = "TokenGeneration_RefreshTokens")] + RefreshTokens, +} + +/// `CognitoEventUserPoolsPostAuthentication` is sent by AWS Cognito User Pools after a user is authenticated, +/// allowing the Lambda to add custom logic. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsPostAuthentication { + #[serde(rename = "CognitoEventUserPoolsHeader")] + #[serde(flatten)] + pub cognito_event_user_pools_header: + CognitoEventUserPoolsHeader, + pub request: CognitoEventUserPoolsPostAuthenticationRequest, + pub response: CognitoEventUserPoolsPostAuthenticationResponse, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)] +pub enum CognitoEventUserPoolsPostAuthenticationTriggerSource { + #[serde(rename = "PostAuthentication_Authentication")] + #[default] + Authentication, +} + +/// `CognitoEventUserPoolsMigrateUser` is sent by AWS Cognito User Pools when a user does not exist in the +/// user pool at the time of sign-in with a password, or in the forgot-password flow. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsMigrateUser { + #[serde(rename = "CognitoEventUserPoolsHeader")] + #[serde(flatten)] + pub cognito_event_user_pools_header: CognitoEventUserPoolsHeader, + #[serde(rename = "request")] + pub cognito_event_user_pools_migrate_user_request: CognitoEventUserPoolsMigrateUserRequest, + #[serde(rename = "response")] + pub cognito_event_user_pools_migrate_user_response: CognitoEventUserPoolsMigrateUserResponse, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)] +pub enum CognitoEventUserPoolsMigrateUserTriggerSource { + #[serde(rename = "UserMigration_Authentication")] + #[default] + Authentication, + #[serde(rename = "UserMigration_ForgotPassword")] + ForgotPassword, +} + +/// `CognitoEventUserPoolsCallerContext` contains information about the caller +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsCallerContext { + #[serde(default)] + #[serde(rename = "awsSdkVersion")] + pub awssdk_version: Option, + #[serde(default)] + pub client_id: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoEventUserPoolsHeader` contains common data from events sent by AWS Cognito User Pools +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsHeader { + #[serde(default)] + pub version: Option, + #[serde(default)] + pub trigger_source: Option, + #[serde(default)] + pub region: Option, + #[serde(default)] + pub user_pool_id: Option, + pub caller_context: CognitoEventUserPoolsCallerContext, + #[serde(default)] + pub user_name: Option, + // no `other` catch-all, because this struct is itself #[serde(flatten)]-ed + // into a different struct that contains an `other` catch-all, so any + // additional fields will be caught there instead +} + +/// `CognitoEventUserPoolsPreSignupRequest` contains the request portion of a PreSignup event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsPreSignupRequest { + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub user_attributes: HashMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub validation_data: HashMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub client_metadata: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoEventUserPoolsPreSignupResponse` contains the response portion of a PreSignup event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsPreSignupResponse { + pub auto_confirm_user: bool, + pub auto_verify_email: bool, + pub auto_verify_phone: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoEventUserPoolsPreAuthenticationRequest` contains the request portion of a PreAuthentication event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsPreAuthenticationRequest { + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub user_attributes: HashMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub validation_data: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoEventUserPoolsPreAuthenticationResponse` contains the response portion of a PreAuthentication event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct CognitoEventUserPoolsPreAuthenticationResponse { + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} +/// `CognitoEventUserPoolsPostConfirmationRequest` contains the request portion of a PostConfirmation event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsPostConfirmationRequest { + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub user_attributes: HashMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub client_metadata: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoEventUserPoolsPostConfirmationResponse` contains the response portion of a PostConfirmation event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct CognitoEventUserPoolsPostConfirmationResponse { + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoEventUserPoolsPreTokenGenRequest` contains request portion of PreTokenGen event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsPreTokenGenRequest { + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub user_attributes: HashMap, + /// Group and role overrides. Note that null or missing values in the request + /// deserialize to [`GroupConfiguration::default`]. + #[serde(default, deserialize_with = "deserialize_nullish")] + pub group_configuration: GroupConfiguration, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub client_metadata: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoEventUserPoolsPreTokenGenResponse` contains the response portion of a PreTokenGen event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsPreTokenGenResponse { + pub claims_override_details: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoEventUserPoolsPreTokenGenV2` is sent by AWS Cognito User Pools when a user attempts to retrieve +/// credentials, allowing a Lambda to perform insert, suppress or override claims. This is the Version 2 Payload +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsPreTokenGenV2 { + #[serde(rename = "CognitoEventUserPoolsHeader")] + #[serde(flatten)] + pub cognito_event_user_pools_header: CognitoEventUserPoolsHeader, + pub request: CognitoEventUserPoolsPreTokenGenRequestV2, + pub response: CognitoEventUserPoolsPreTokenGenResponseV2, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoEventUserPoolsPreTokenGenRequestV2` contains request portion of PreTokenGenV2 event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsPreTokenGenRequestV2 { + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub user_attributes: HashMap, + /// Group and role overrides. Note that null or missing values in the request + /// deserialize to [`GroupConfiguration::default`]. + #[serde(default, deserialize_with = "deserialize_nullish")] + pub group_configuration: GroupConfiguration, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub client_metadata: HashMap, + pub scopes: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsPreTokenGenResponseV2 { + pub claims_and_scope_override_details: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ClaimsAndScopeOverrideDetailsV2` allows lambda to add, suppress or override claims in the token +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ClaimsAndScopeOverrideDetailsV2 { + pub group_override_details: GroupConfiguration, + pub id_token_generation: Option, + pub access_token_generation: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoIdTokenGenerationV2` allows lambda to customize the ID Token before generation +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoIdTokenGenerationV2 { + pub claims_to_add_or_override: HashMap, + pub claims_to_suppress: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoAccessTokenGenerationV2` allows lambda to customize the Access Token before generation +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoAccessTokenGenerationV2 { + pub claims_to_add_or_override: HashMap, + pub claims_to_suppress: Vec, + pub scopes_to_add: Vec, + pub scopes_to_suppress: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoEventUserPoolsPostAuthenticationRequest` contains the request portion of a PostAuthentication event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsPostAuthenticationRequest { + pub new_device_used: bool, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub user_attributes: HashMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub client_metadata: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoEventUserPoolsPostAuthenticationResponse` contains the response portion of a PostAuthentication event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct CognitoEventUserPoolsPostAuthenticationResponse { + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} +/// `CognitoEventUserPoolsMigrateUserRequest` contains the request portion of a MigrateUser event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsMigrateUserRequest { + #[serde(default)] + pub password: Option, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub validation_data: HashMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub client_metadata: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoEventUserPoolsMigrateUserResponse` contains the response portion of a MigrateUser event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsMigrateUserResponse { + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub user_attributes: HashMap, + #[serde(default)] + pub final_user_status: Option, + #[serde(default)] + pub message_action: Option, + #[serde(default)] + pub desired_delivery_mediums: Option>, + #[serde(default, deserialize_with = "deserialize_nullish")] + pub force_alias_creation: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ClaimsOverrideDetails` allows lambda to add, suppress or override claims in the token +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ClaimsOverrideDetails { + pub group_override_details: GroupConfiguration, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub claims_to_add_or_override: HashMap, + pub claims_to_suppress: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `GroupConfiguration` allows lambda to override groups, roles and set a preferred role +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GroupConfiguration { + pub groups_to_override: Vec, + pub iam_roles_to_override: Vec, + pub preferred_role: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoEventUserPoolsChallengeResult` represents a challenge that is presented to the user in the authentication +/// process that is underway, along with the corresponding result. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsChallengeResult { + #[serde(default)] + pub challenge_name: Option, + pub challenge_result: bool, + #[serde(default)] + pub challenge_metadata: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoEventUserPoolsDefineAuthChallengeRequest` defines auth challenge request parameters +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsDefineAuthChallengeRequest { + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub user_attributes: HashMap, + pub session: Vec>, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub client_metadata: HashMap, + #[serde(default)] + pub user_not_found: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoEventUserPoolsDefineAuthChallengeResponse` defines auth challenge response parameters +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsDefineAuthChallengeResponse { + #[serde(default)] + pub challenge_name: Option, + #[serde(default, deserialize_with = "deserialize_nullish")] + pub issue_tokens: bool, + #[serde(default, deserialize_with = "deserialize_nullish")] + pub fail_authentication: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoEventUserPoolsDefineAuthChallenge` sent by AWS Cognito User Pools to initiate custom authentication flow +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsDefineAuthChallenge { + #[serde(rename = "CognitoEventUserPoolsHeader")] + #[serde(flatten)] + pub cognito_event_user_pools_header: + CognitoEventUserPoolsHeader, + pub request: CognitoEventUserPoolsDefineAuthChallengeRequest, + pub response: CognitoEventUserPoolsDefineAuthChallengeResponse, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)] +pub enum CognitoEventUserPoolsDefineAuthChallengeTriggerSource { + #[serde(rename = "DefineAuthChallenge_Authentication")] + #[default] + Authentication, +} + +/// `CognitoEventUserPoolsCreateAuthChallengeRequest` defines create auth challenge request parameters +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsCreateAuthChallengeRequest { + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub user_attributes: HashMap, + #[serde(default)] + pub challenge_name: Option, + pub session: Vec>, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub client_metadata: HashMap, + #[serde(default)] + pub user_not_found: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoEventUserPoolsCreateAuthChallengeResponse` defines create auth challenge response parameters +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsCreateAuthChallengeResponse { + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub public_challenge_parameters: HashMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub private_challenge_parameters: HashMap, + #[serde(default)] + pub challenge_metadata: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoEventUserPoolsCreateAuthChallenge` sent by AWS Cognito User Pools to create a challenge to present to the user +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsCreateAuthChallenge { + #[serde(rename = "CognitoEventUserPoolsHeader")] + #[serde(flatten)] + pub cognito_event_user_pools_header: + CognitoEventUserPoolsHeader, + pub request: CognitoEventUserPoolsCreateAuthChallengeRequest, + pub response: CognitoEventUserPoolsCreateAuthChallengeResponse, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)] +pub enum CognitoEventUserPoolsCreateAuthChallengeTriggerSource { + #[serde(rename = "CreateAuthChallenge_Authentication")] + #[default] + Authentication, +} + +/// `CognitoEventUserPoolsVerifyAuthChallengeRequest` defines verify auth challenge request parameters +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsVerifyAuthChallengeRequest +where + T1: DeserializeOwned, + T1: Serialize, +{ + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub user_attributes: HashMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub private_challenge_parameters: HashMap, + #[serde(bound = "")] + pub challenge_answer: Option, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub client_metadata: HashMap, + #[serde(default)] + pub user_not_found: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoEventUserPoolsVerifyAuthChallengeResponse` defines verify auth challenge response parameters +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsVerifyAuthChallengeResponse { + #[serde(default, deserialize_with = "deserialize_nullish")] + pub answer_correct: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoEventUserPoolsVerifyAuthChallenge` sent by AWS Cognito User Pools to verify if the response from the end user +/// for a custom Auth Challenge is valid or not +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsVerifyAuthChallenge { + #[serde(rename = "CognitoEventUserPoolsHeader")] + #[serde(flatten)] + pub cognito_event_user_pools_header: + CognitoEventUserPoolsHeader, + pub request: CognitoEventUserPoolsVerifyAuthChallengeRequest, + pub response: CognitoEventUserPoolsVerifyAuthChallengeResponse, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)] +pub enum CognitoEventUserPoolsVerifyAuthChallengeTriggerSource { + #[serde(rename = "VerifyAuthChallengeResponse_Authentication")] + #[default] + Authentication, +} + +/// `CognitoEventUserPoolsCustomMessage` is sent by AWS Cognito User Pools before a verification or MFA message is sent, +/// allowing a user to customize the message dynamically. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsCustomMessage { + #[serde(rename = "CognitoEventUserPoolsHeader")] + #[serde(flatten)] + pub cognito_event_user_pools_header: CognitoEventUserPoolsHeader, + pub request: CognitoEventUserPoolsCustomMessageRequest, + pub response: CognitoEventUserPoolsCustomMessageResponse, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)] +pub enum CognitoEventUserPoolsCustomMessageTriggerSource { + #[serde(rename = "CustomMessage_SignUp")] + #[default] + SignUp, + #[serde(rename = "CustomMessage_AdminCreateUser")] + AdminCreateUser, + #[serde(rename = "CustomMessage_ResendCode")] + ResendCode, + #[serde(rename = "CustomMessage_ForgotPassword")] + ForgotPassword, + #[serde(rename = "CustomMessage_UpdateUserAttribute")] + UpdateUserAttribute, + #[serde(rename = "CustomMessage_VerifyUserAttribute")] + VerifyUserAttribute, + #[serde(rename = "CustomMessage_Authentication")] + Authentication, +} + +/// `CognitoEventUserPoolsCustomMessageRequest` contains the request portion of a CustomMessage event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsCustomMessageRequest +where + T1: DeserializeOwned, + T1: Serialize, +{ + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + #[serde(bound = "")] + pub user_attributes: HashMap, + #[serde(default)] + pub code_parameter: Option, + #[serde(default)] + pub username_parameter: Option, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub client_metadata: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `CognitoEventUserPoolsCustomMessageResponse` contains the response portion of a CustomMessage event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CognitoEventUserPoolsCustomMessageResponse { + #[serde(default)] + pub sms_message: Option, + #[serde(default)] + pub email_message: Option, + #[serde(default)] + pub email_subject: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + use crate::fixtures::verify_serde_roundtrip; + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event() { + let data = include_bytes!("../../fixtures/example-cognito-event.json"); + let parsed: CognitoEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_create_auth_challenge() { + let data = include_bytes!("../../fixtures/example-cognito-event-userpools-create-auth-challenge.json"); + let parsed: CognitoEventUserPoolsCreateAuthChallenge = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEventUserPoolsCreateAuthChallenge = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_create_auth_challenge_user_not_found() { + let data = + include_bytes!("../../fixtures/example-cognito-event-userpools-create-auth-challenge-user-not-found.json"); + let parsed: CognitoEventUserPoolsCreateAuthChallenge = serde_json::from_slice(data).unwrap(); + + assert!(parsed.request.user_not_found); + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEventUserPoolsCreateAuthChallenge = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_custommessage() { + let data = include_bytes!("../../fixtures/example-cognito-event-userpools-custommessage.json"); + let parsed: CognitoEventUserPoolsCustomMessage = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEventUserPoolsCustomMessage = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_define_auth_challenge() { + let data = include_bytes!("../../fixtures/example-cognito-event-userpools-define-auth-challenge.json"); + let parsed: CognitoEventUserPoolsDefineAuthChallenge = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEventUserPoolsDefineAuthChallenge = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_define_auth_challenge_optional_response_fields() { + let data = include_bytes!( + "../../fixtures/example-cognito-event-userpools-define-auth-challenge-optional-response-fields.json" + ); + let parsed: CognitoEventUserPoolsDefineAuthChallenge = serde_json::from_slice(data).unwrap(); + + assert!(!parsed.response.fail_authentication); + assert!(!parsed.response.issue_tokens); + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEventUserPoolsDefineAuthChallenge = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_define_auth_challenge_user_not_found() { + let data = + include_bytes!("../../fixtures/example-cognito-event-userpools-define-auth-challenge-user-not-found.json"); + let parsed: CognitoEventUserPoolsDefineAuthChallenge = serde_json::from_slice(data).unwrap(); + + assert!(parsed.request.user_not_found); + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEventUserPoolsDefineAuthChallenge = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_migrateuser() { + let data = include_bytes!("../../fixtures/example-cognito-event-userpools-migrateuser.json"); + verify_serde_roundtrip::(data); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_migrateuser_null_fields() { + let data = include_bytes!("../../fixtures/example-cognito-event-userpools-migrateuser-null-fields.json"); + let event: CognitoEventUserPoolsMigrateUser = verify_serde_roundtrip(data); + // verify correct values substituted for nulls. + assert_eq!( + 0, + event + .cognito_event_user_pools_migrate_user_response + .user_attributes + .len() + ); + assert!( + !event + .cognito_event_user_pools_migrate_user_response + .force_alias_creation + ); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_postauthentication() { + let data = include_bytes!("../../fixtures/example-cognito-event-userpools-postauthentication.json"); + let parsed: CognitoEventUserPoolsPostAuthentication = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEventUserPoolsPostAuthentication = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_postconfirmation() { + let data = include_bytes!("../../fixtures/example-cognito-event-userpools-postconfirmation.json"); + let parsed: CognitoEventUserPoolsPostConfirmation = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEventUserPoolsPostConfirmation = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_preauthentication() { + let data = include_bytes!("../../fixtures/example-cognito-event-userpools-preauthentication.json"); + let parsed: CognitoEventUserPoolsPreAuthentication = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEventUserPoolsPreAuthentication = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_presignup() { + let data = include_bytes!("../../fixtures/example-cognito-event-userpools-presignup.json"); + let parsed: CognitoEventUserPoolsPreSignup = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEventUserPoolsPreSignup = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_pretokengen_incoming() { + let data = include_bytes!("../../fixtures/example-cognito-event-userpools-pretokengen-incoming.json"); + verify_serde_roundtrip::(data); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_pretokengen_v2_incoming() { + let data = include_bytes!("../../fixtures/example-cognito-event-userpools-pretokengen-v2-incoming.json"); + verify_serde_roundtrip::(data); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_pretokengen() { + let data = include_bytes!("../../fixtures/example-cognito-event-userpools-pretokengen.json"); + verify_serde_roundtrip::(data); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_pretokengen_null_group_configuration() { + let data = + include_bytes!("../../fixtures/example-cognito-event-userpools-pretokengen-null-group-configuration.json"); + verify_serde_roundtrip::(data); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_v2_pretokengen() { + let data = include_bytes!("../../fixtures/example-cognito-event-userpools-pretokengen-v2.json"); + verify_serde_roundtrip::(data); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_v2_pretokengen_null_group_configuration() { + let data = include_bytes!( + "../../fixtures/example-cognito-event-userpools-pretokengen-v2-null-group-configuration.json" + ); + let event: CognitoEventUserPoolsPreTokenGenV2 = verify_serde_roundtrip(data); + // fail if there are any breaking changes to GroupConfiguration::default(). + assert_eq!(0, event.request.group_configuration.groups_to_override.len()); + assert_eq!(0, event.request.group_configuration.iam_roles_to_override.len()); + assert!(event.request.group_configuration.preferred_role.is_none()); + + #[cfg(feature = "catch-all-fields")] + assert_eq!(0, event.request.group_configuration.other.len()); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_verify_auth_challenge() { + let data = include_bytes!("../../fixtures/example-cognito-event-userpools-verify-auth-challenge.json"); + let parsed: CognitoEventUserPoolsVerifyAuthChallenge = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEventUserPoolsVerifyAuthChallenge = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_verify_auth_challenge_optional_answer_correct() { + let data = include_bytes!( + "../../fixtures/example-cognito-event-userpools-verify-auth-challenge-optional-answer-correct.json" + ); + let parsed: CognitoEventUserPoolsVerifyAuthChallenge = serde_json::from_slice(data).unwrap(); + + assert!(!parsed.response.answer_correct); + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEventUserPoolsVerifyAuthChallenge = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_verify_auth_challenge_null_answer_correct() { + let data = include_bytes!( + "../../fixtures/example-cognito-event-userpools-verify-auth-challenge-null-answer-correct.json" + ); + let parsed: CognitoEventUserPoolsVerifyAuthChallenge = serde_json::from_slice(data).unwrap(); + + assert!(!parsed.response.answer_correct); + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEventUserPoolsVerifyAuthChallenge = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_verify_auth_challenge_user_not_found() { + let data = + include_bytes!("../../fixtures/example-cognito-event-userpools-verify-auth-challenge-user-not-found.json"); + let parsed: CognitoEventUserPoolsVerifyAuthChallenge = serde_json::from_slice(data).unwrap(); + + assert!(parsed.request.user_not_found); + + let output: String = serde_json::to_string_pretty(&parsed).unwrap(); + println!("output is: {output}"); + let reparsed: CognitoEventUserPoolsVerifyAuthChallenge = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} + +#[cfg(test)] +#[cfg(feature = "cognito")] +mod trigger_source_tests { + use super::*; + + fn gen_header(trigger_source: &str) -> String { + format!( + r#" +{{ + "version": "1", + "triggerSource": "{trigger_source}", + "region": "region", + "userPoolId": "userPoolId", + "userName": "userName", + "callerContext": {{ + "awsSdkVersion": "calling aws sdk with version", + "clientId": "apps client id" + }} +}}"# + ) + } + + #[test] + fn pre_sign_up() { + let possible_triggers = [ + "PreSignUp_AdminCreateUser", + "PreSignUp_AdminCreateUser", + "PreSignUp_ExternalProvider", + ]; + possible_triggers.into_iter().for_each(|trigger| { + let header = gen_header(trigger); + let parsed: CognitoEventUserPoolsHeader = + serde_json::from_str(&header).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEventUserPoolsHeader<_> = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + }); + } + + #[test] + fn pre_authentication() { + let possible_triggers = ["PreAuthentication_Authentication"]; + possible_triggers.into_iter().for_each(|trigger| { + let header = gen_header(trigger); + let parsed: CognitoEventUserPoolsHeader = + serde_json::from_str(&header).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEventUserPoolsHeader<_> = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + }); + } + #[test] + fn post_confirmation() { + let possible_triggers = [ + "PostConfirmation_ConfirmForgotPassword", + "PostConfirmation_ConfirmSignUp", + ]; + + possible_triggers.into_iter().for_each(|trigger| { + let header = gen_header(trigger); + let parsed: CognitoEventUserPoolsHeader = + serde_json::from_str(&header).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEventUserPoolsHeader<_> = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + }); + } + #[test] + fn post_authentication() { + let possible_triggers = ["PostAuthentication_Authentication"]; + + possible_triggers.into_iter().for_each(|trigger| { + let header = gen_header(trigger); + let parsed: CognitoEventUserPoolsHeader = + serde_json::from_str(&header).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEventUserPoolsHeader<_> = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + }); + } + #[test] + fn define_auth_challenge() { + let possible_triggers = ["DefineAuthChallenge_Authentication"]; + + possible_triggers.into_iter().for_each(|trigger| { + let header = gen_header(trigger); + let parsed: CognitoEventUserPoolsHeader = + serde_json::from_str(&header).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEventUserPoolsHeader<_> = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + }); + } + + #[test] + fn create_auth_challenge() { + let possible_triggers = ["CreateAuthChallenge_Authentication"]; + + possible_triggers.into_iter().for_each(|trigger| { + let header = gen_header(trigger); + let parsed: CognitoEventUserPoolsHeader = + serde_json::from_str(&header).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEventUserPoolsHeader<_> = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + }); + } + #[test] + fn verify_auth_challenge() { + let possible_triggers = ["VerifyAuthChallengeResponse_Authentication"]; + + possible_triggers.into_iter().for_each(|trigger| { + let header = gen_header(trigger); + let parsed: CognitoEventUserPoolsHeader = + serde_json::from_str(&header).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEventUserPoolsHeader<_> = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + }); + } + #[test] + fn pre_token_generation() { + let possible_triggers = [ + "TokenGeneration_HostedAuth", + "TokenGeneration_Authentication", + "TokenGeneration_NewPasswordChallenge", + "TokenGeneration_AuthenticateDevice", + "TokenGeneration_RefreshTokens", + ]; + + possible_triggers.into_iter().for_each(|trigger| { + let header = gen_header(trigger); + let parsed: CognitoEventUserPoolsHeader = + serde_json::from_str(&header).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEventUserPoolsHeader<_> = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + }); + } + #[test] + fn user_migration() { + let possible_triggers = ["UserMigration_Authentication", "UserMigration_ForgotPassword"]; + + possible_triggers.into_iter().for_each(|trigger| { + let header = gen_header(trigger); + let parsed: CognitoEventUserPoolsHeader = + serde_json::from_str(&header).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEventUserPoolsHeader<_> = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + }); + } + #[test] + fn custom_message() { + let possible_triggers = [ + "CustomMessage_SignUp", + "CustomMessage_AdminCreateUser", + "CustomMessage_ResendCode", + "CustomMessage_ForgotPassword", + "CustomMessage_UpdateUserAttribute", + "CustomMessage_VerifyUserAttribute", + "CustomMessage_Authentication", + ]; + + possible_triggers.into_iter().for_each(|trigger| { + let header = gen_header(trigger); + let parsed: CognitoEventUserPoolsHeader = + serde_json::from_str(&header).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: CognitoEventUserPoolsHeader<_> = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + }); + } +} diff --git a/lambda-events/src/event/config/mod.rs b/lambda-events/src/event/config/mod.rs new file mode 100644 index 000000000..81cfd438f --- /dev/null +++ b/lambda-events/src/event/config/mod.rs @@ -0,0 +1,66 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +/// `ConfigEvent` contains data from an event sent from AWS Config +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ConfigEvent { + /// The ID of the AWS account that owns the rule + #[serde(default)] + pub account_id: Option, + /// The ARN that AWS Config assigned to the rule + /// + /// nolint:stylecheck + #[serde(default)] + pub config_rule_arn: Option, + /// nolint:stylecheck + #[serde(default)] + pub config_rule_id: Option, + /// The name that you assigned to the rule that caused AWS Config to publish the event + #[serde(default)] + pub config_rule_name: Option, + /// A boolean value that indicates whether the AWS resource to be evaluated has been removed from the rule's scope + pub event_left_scope: bool, + /// nolint:stylecheck + #[serde(default)] + pub execution_role_arn: Option, + /// If the event is published in response to a resource configuration change, this value contains a JSON configuration item + #[serde(default)] + pub invoking_event: Option, + /// A token that the function must pass to AWS Config with the PutEvaluations call + #[serde(default)] + pub result_token: Option, + /// Key/value pairs that the function processes as part of its evaluation logic + #[serde(default)] + pub rule_parameters: Option, + #[serde(default)] + pub version: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "config")] + fn example_config_event() { + let data = include_bytes!("../../fixtures/example-config-event.json"); + let parsed: ConfigEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ConfigEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/connect/mod.rs b/lambda-events/src/event/connect/mod.rs new file mode 100644 index 000000000..98925ae48 --- /dev/null +++ b/lambda-events/src/event/connect/mod.rs @@ -0,0 +1,170 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +use std::collections::HashMap; + +use crate::custom_serde::deserialize_nullish; + +/// `ConnectEvent` contains the data structure for a Connect event. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectEvent { + #[serde(rename = "Details")] + pub details: ConnectDetails, + /// The name of the event. + #[serde(default)] + #[serde(rename = "Name")] + pub name: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ConnectDetails` holds the details of a Connect event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectDetails { + #[serde(rename = "ContactData")] + pub contact_data: ConnectContactData, + /// The parameters that have been set in the Connect instance at the time of the Lambda invocation. + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + #[serde(rename = "Parameters")] + pub parameters: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ConnectContactData` holds all of the contact information for the user that invoked the Connect event. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectContactData { + /// The custom attributes from Connect that the Lambda function was invoked with. + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + #[serde(rename = "Attributes")] + pub attributes: HashMap, + #[serde(default)] + #[serde(rename = "Channel")] + pub channel: Option, + #[serde(default)] + #[serde(rename = "ContactId")] + pub contact_id: Option, + #[serde(rename = "CustomerEndpoint")] + pub customer_endpoint: ConnectEndpoint, + #[serde(default)] + #[serde(rename = "InitialContactId")] + pub initial_contact_id: Option, + /// Either: INBOUND/OUTBOUND/TRANSFER/CALLBACK + #[serde(default)] + #[serde(rename = "InitiationMethod")] + pub initiation_method: Option, + #[serde(default)] + #[serde(rename = "PreviousContactId")] + pub previous_contact_id: Option, + #[serde(rename = "Queue", default)] + pub queue: Option, + #[serde(rename = "SystemEndpoint")] + pub system_endpoint: ConnectEndpoint, + #[serde(default)] + #[serde(rename = "InstanceARN")] + pub instance_arn: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ConnectEndpoint` represents routing information. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectEndpoint { + #[serde(default)] + #[serde(rename = "Address")] + pub address: Option, + #[serde(default)] + #[serde(rename = "Type")] + pub type_: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ConnectQueue` represents a queue object. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectQueue { + #[serde(default)] + #[serde(rename = "Name")] + pub name: Option, + #[serde(default)] + #[serde(rename = "ARN")] + pub arn: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +pub type ConnectResponse = HashMap; + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "connect")] + fn example_connect_event() { + let data = include_bytes!("../../fixtures/example-connect-event.json"); + let parsed: ConnectEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ConnectEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "connect")] + fn example_connect_event_without_queue() { + let data = include_bytes!("../../fixtures/example-connect-event-without-queue.json"); + let parsed: ConnectEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ConnectEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/controltower/mod.rs b/lambda-events/src/event/controltower/mod.rs new file mode 100644 index 000000000..8a03c8a46 --- /dev/null +++ b/lambda-events/src/event/controltower/mod.rs @@ -0,0 +1,467 @@ +/// AWS Control Tower lifecycle event definitions. +/// +/// Control Tower lifecycle events are delivered via EventBridge as CloudTrail +/// service events with detail-type `"AWS Service Event via CloudTrail"` and +/// source `"aws.controltower"`. +/// +/// Use with the `CloudWatchEvent` or `EventBridgeEvent` wrapper: +/// ```ignore +/// use aws_lambda_events::cloudwatch_events::CloudWatchEvent; +/// use aws_lambda_events::controltower::ControlTowerLifecycleEvent; +/// +/// let event: CloudWatchEvent = serde_json::from_slice(data)?; +/// ``` +/// +/// See +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +/// The CloudTrail service event delivered as the EventBridge `detail` payload +/// for Control Tower lifecycle events. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ControlTowerLifecycleEvent { + pub event_version: String, + pub user_identity: UserIdentity, + pub event_time: String, + pub event_source: String, + pub event_name: String, + pub aws_region: String, + #[serde(rename = "sourceIPAddress")] + pub source_ip_address: String, + pub user_agent: String, + #[serde(rename = "eventID")] + pub event_id: String, + pub read_only: bool, + pub event_type: String, + pub service_event_details: ServiceEventDetails, + #[serde(default)] + pub management_event: Option, + #[serde(default)] + pub recipient_account_id: Option, + #[serde(default)] + pub request_parameters: Option, + #[serde(default)] + pub response_elements: Option, + #[serde(default)] + pub event_category: Option, +} + +/// User identity information for Control Tower lifecycle events. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UserIdentity { + pub account_id: String, + #[serde(default)] + pub invoked_by: Option, +} + +/// The service event details for Control Tower lifecycle events. +/// +/// Each variant corresponds to a specific lifecycle event type. +#[non_exhaustive] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum ServiceEventDetails { + CreateManagedAccountStatus(ManagedAccountStatus), + UpdateManagedAccountStatus(ManagedAccountStatus), + EnableGuardrailStatus(GuardrailStatus), + DisableGuardrailStatus(GuardrailStatus), + SetupLandingZoneStatus(LandingZoneStatus), + UpdateLandingZoneStatus(LandingZoneStatus), + RegisterOrganizationalUnitStatus(OrganizationalUnitRegistrationStatus), + DeregisterOrganizationalUnitStatus(OrganizationalUnitRegistrationStatus), + PrecheckOrganizationalUnitStatus(PrecheckOrganizationalUnitStatus), + EnableBaselineStatus(BaselineStatus), + ResetEnabledBaselineStatus(BaselineStatus), + UpdateEnabledBaselineStatus(BaselineStatus), + DisableBaselineStatus(BaselineStatus), +} + +/// An organizational unit reference. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OrganizationalUnit { + pub organizational_unit_name: String, + pub organizational_unit_id: String, +} + +/// An account reference. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Account { + pub account_name: String, + pub account_id: String, +} + +/// A guardrail (control) reference. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Guardrail { + pub guardrail_id: String, + pub guardrail_behavior: String, +} + +/// Status for `CreateManagedAccount` and `UpdateManagedAccount` events. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ManagedAccountStatus { + pub organizational_unit: OrganizationalUnit, + pub account: Account, + pub state: String, + pub message: String, + pub requested_timestamp: String, + pub completed_timestamp: String, +} + +/// Status for `EnableGuardrail` and `DisableGuardrail` events. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GuardrailStatus { + pub organizational_units: Vec, + pub guardrails: Vec, + pub state: String, + pub message: String, + pub request_timestamp: String, + pub completed_timestamp: String, +} + +/// Status for `SetupLandingZone` and `UpdateLandingZone` events. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LandingZoneStatus { + pub state: String, + pub message: String, + pub root_organizational_id: String, + pub organizational_units: Vec, + pub accounts: Vec, + pub requested_timestamp: String, + pub completed_timestamp: String, +} + +/// Status for `RegisterOrganizationalUnit` and `DeregisterOrganizationalUnit` events. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OrganizationalUnitRegistrationStatus { + pub state: String, + pub message: String, + pub organizational_unit: OrganizationalUnit, + pub requested_timestamp: String, + pub completed_timestamp: String, +} + +/// An organizational unit with precheck failure information. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PrecheckOrganizationalUnit { + pub organizational_unit_name: String, + pub organizational_unit_id: String, + #[serde(default)] + pub failed_prechecks: Vec, +} + +/// An account with precheck failure information. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PrecheckAccount { + pub account_name: String, + pub account_id: String, + #[serde(default)] + pub failed_prechecks: Vec, +} + +/// Status for `PrecheckOrganizationalUnit` events. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PrecheckOrganizationalUnitStatus { + pub organizational_unit: PrecheckOrganizationalUnit, + pub accounts: Vec, + pub state: String, + pub message: String, + pub requested_timestamp: String, + pub completed_timestamp: String, +} + +/// Status summary for a baseline operation. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BaselineStatusSummary { + pub last_operation_identifier: String, + pub status: String, +} + +/// A parameter value wrapping an untyped object. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BaselineParameterValue { + pub untyped: BaselineUntypedValue, +} + +/// An untyped baseline parameter value. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BaselineUntypedValue { + pub object: String, +} + +/// A baseline parameter key-value pair. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BaselineParameter { + pub key: String, + pub value: BaselineParameterValue, +} + +/// Details about an enabled baseline. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EnabledBaselineDetails { + pub arn: String, + pub parent_identifier: String, + pub target_identifier: String, + pub baseline_identifier: String, + pub baseline_version: String, + pub status_summary: BaselineStatusSummary, + #[serde(default)] + pub parameters: Vec, +} + +/// Status for `EnableBaseline`, `ResetEnabledBaseline`, +/// `UpdateEnabledBaseline`, and `DisableBaseline` events. +/// +/// The `baseline_details` field is only present in `DisableBaseline` events. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BaselineStatus { + pub enabled_baseline_details: EnabledBaselineDetails, + #[serde(default)] + pub baseline_details: Option, + pub requested_timestamp: String, + pub completed_timestamp: String, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn example_controltower_create_managed_account() { + let data = include_bytes!("../../fixtures/example-controltower-create-managed-account.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "CreateManagedAccount"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::CreateManagedAccountStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_enable_guardrail() { + let data = include_bytes!("../../fixtures/example-controltower-enable-guardrail.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "EnableGuardrail"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::EnableGuardrailStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_setup_landing_zone() { + let data = include_bytes!("../../fixtures/example-controltower-setup-landing-zone.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "SetupLandingZone"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::SetupLandingZoneStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_register_organizational_unit() { + let data = include_bytes!("../../fixtures/example-controltower-register-organizational-unit.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "RegisterOrganizationalUnit"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::RegisterOrganizationalUnitStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_precheck_organizational_unit() { + let data = include_bytes!("../../fixtures/example-controltower-precheck-organizational-unit.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "PrecheckOrganizationalUnit"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::PrecheckOrganizationalUnitStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_enable_baseline() { + let data = include_bytes!("../../fixtures/example-controltower-enable-baseline.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "EnableBaseline"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::EnableBaselineStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_disable_baseline() { + let data = include_bytes!("../../fixtures/example-controltower-disable-baseline.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "DisableBaseline"); + if let ServiceEventDetails::DisableBaselineStatus(ref status) = parsed.service_event_details { + assert!(status.baseline_details.is_some()); + } else { + panic!("Expected DisableBaselineStatus"); + } + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_update_managed_account() { + let data = include_bytes!("../../fixtures/example-controltower-update-managed-account.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "UpdateManagedAccount"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::UpdateManagedAccountStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_disable_guardrail() { + let data = include_bytes!("../../fixtures/example-controltower-disable-guardrail.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "DisableGuardrail"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::DisableGuardrailStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_update_landing_zone() { + let data = include_bytes!("../../fixtures/example-controltower-update-landing-zone.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "UpdateLandingZone"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::UpdateLandingZoneStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_deregister_organizational_unit() { + let data = include_bytes!("../../fixtures/example-controltower-deregister-organizational-unit.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "DeregisterOrganizationalUnit"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::DeregisterOrganizationalUnitStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_reset_enabled_baseline() { + let data = include_bytes!("../../fixtures/example-controltower-reset-enabled-baseline.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "ResetEnabledBaseline"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::ResetEnabledBaselineStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_update_enabled_baseline() { + let data = include_bytes!("../../fixtures/example-controltower-update-enabled-baseline.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "UpdateEnabledBaseline"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::UpdateEnabledBaselineStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/documentdb/events/commom_types.rs b/lambda-events/src/event/documentdb/events/commom_types.rs new file mode 100644 index 000000000..16c312137 --- /dev/null +++ b/lambda-events/src/event/documentdb/events/commom_types.rs @@ -0,0 +1,105 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; + +pub type AnyDocument = HashMap; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DatabaseCollection { + db: String, + #[serde(default)] + coll: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct DocumentId { + #[serde(rename = "_data")] + pub data: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct DocumentKeyIdOid { + #[serde(rename = "$oid")] + pub oid: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct DocumentKeyId { + #[serde(rename = "_id")] + pub id: DocumentKeyIdOid, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct InnerTimestamp { + t: usize, + i: usize, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct Timestamp { + #[serde(rename = "$timestamp")] + pub timestamp: InnerTimestamp, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/documentdb/events/delete_event.rs b/lambda-events/src/event/documentdb/events/delete_event.rs new file mode 100644 index 000000000..1bb61dc1f --- /dev/null +++ b/lambda-events/src/event/documentdb/events/delete_event.rs @@ -0,0 +1,34 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +use super::commom_types::{AnyDocument, DatabaseCollection, DocumentId, DocumentKeyId, Timestamp}; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChangeDeleteEvent { + #[serde(rename = "_id")] + id: DocumentId, + #[serde(default)] + cluster_time: Option, + document_key: DocumentKeyId, + #[serde(default)] + #[serde(rename = "lsid")] + ls_id: Option, + ns: DatabaseCollection, + // operation_type: String, + #[serde(default)] + txn_number: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/documentdb/events/drop_database_event.rs b/lambda-events/src/event/documentdb/events/drop_database_event.rs new file mode 100644 index 000000000..d1c12d49d --- /dev/null +++ b/lambda-events/src/event/documentdb/events/drop_database_event.rs @@ -0,0 +1,33 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +use super::commom_types::{AnyDocument, DatabaseCollection, DocumentId, Timestamp}; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChangeDropDatabaseEvent { + #[serde(rename = "_id")] + id: DocumentId, + #[serde(default)] + cluster_time: Option, + #[serde(default)] + #[serde(rename = "lsid")] + ls_id: Option, + ns: DatabaseCollection, + // operation_type: String, + #[serde(default)] + txn_number: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/documentdb/events/drop_event.rs b/lambda-events/src/event/documentdb/events/drop_event.rs new file mode 100644 index 000000000..5b01531f7 --- /dev/null +++ b/lambda-events/src/event/documentdb/events/drop_event.rs @@ -0,0 +1,32 @@ +use super::commom_types::{AnyDocument, DatabaseCollection, DocumentId, Timestamp}; +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChangeDropEvent { + #[serde(rename = "_id")] + id: DocumentId, + #[serde(default)] + cluster_time: Option, + #[serde(default)] + #[serde(rename = "lsid")] + ls_id: Option, + ns: DatabaseCollection, + // operation_type: String, + #[serde(default)] + txn_number: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/documentdb/events/insert_event.rs b/lambda-events/src/event/documentdb/events/insert_event.rs new file mode 100644 index 000000000..0b560232f --- /dev/null +++ b/lambda-events/src/event/documentdb/events/insert_event.rs @@ -0,0 +1,34 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +use super::commom_types::{AnyDocument, DatabaseCollection, DocumentId, DocumentKeyId, Timestamp}; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChangeInsertEvent { + #[serde(rename = "_id")] + id: DocumentId, + #[serde(default)] + cluster_time: Option, + document_key: DocumentKeyId, + #[serde(default)] + #[serde(rename = "lsid")] + ls_id: Option, + ns: DatabaseCollection, + //operation_type: String, + #[serde(default)] + txn_number: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/documentdb/events/invalidate_event.rs b/lambda-events/src/event/documentdb/events/invalidate_event.rs new file mode 100644 index 000000000..9757c2205 --- /dev/null +++ b/lambda-events/src/event/documentdb/events/invalidate_event.rs @@ -0,0 +1,27 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +use super::commom_types::{DocumentId, Timestamp}; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChangeInvalidateEvent { + #[serde(rename = "_id")] + id: DocumentId, + #[serde(default)] + cluster_time: Option, + // operation_type: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/documentdb/events/mod.rs b/lambda-events/src/event/documentdb/events/mod.rs new file mode 100644 index 000000000..c1c41b989 --- /dev/null +++ b/lambda-events/src/event/documentdb/events/mod.rs @@ -0,0 +1,9 @@ +pub mod commom_types; +pub mod delete_event; +pub mod drop_database_event; +pub mod drop_event; +pub mod insert_event; +pub mod invalidate_event; +pub mod rename_event; +pub mod replace_event; +pub mod update_event; diff --git a/lambda-events/src/event/documentdb/events/rename_event.rs b/lambda-events/src/event/documentdb/events/rename_event.rs new file mode 100644 index 000000000..b3b311843 --- /dev/null +++ b/lambda-events/src/event/documentdb/events/rename_event.rs @@ -0,0 +1,35 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +use super::commom_types::{AnyDocument, DatabaseCollection, DocumentId, Timestamp}; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChangeRenameEvent { + #[serde(rename = "_id")] + id: DocumentId, + #[serde(default)] + cluster_time: Option, + + #[serde(default)] + #[serde(rename = "lsid")] + ls_id: Option, + ns: DatabaseCollection, + //operation_type: String, + #[serde(default)] + txn_number: Option, + to: DatabaseCollection, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/documentdb/events/replace_event.rs b/lambda-events/src/event/documentdb/events/replace_event.rs new file mode 100644 index 000000000..75be2bf48 --- /dev/null +++ b/lambda-events/src/event/documentdb/events/replace_event.rs @@ -0,0 +1,34 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +use super::commom_types::{AnyDocument, DatabaseCollection, DocumentId, DocumentKeyId, Timestamp}; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChangeReplaceEvent { + #[serde(rename = "_id")] + id: DocumentId, + #[serde(default)] + cluster_time: Option, + document_key: DocumentKeyId, + #[serde(default)] + #[serde(rename = "lsid")] + ls_id: Option, + ns: DatabaseCollection, + // operation_type: String, + #[serde(default)] + txn_number: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/documentdb/events/update_event.rs b/lambda-events/src/event/documentdb/events/update_event.rs new file mode 100644 index 000000000..1a0de1ed3 --- /dev/null +++ b/lambda-events/src/event/documentdb/events/update_event.rs @@ -0,0 +1,70 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +use super::commom_types::{AnyDocument, DatabaseCollection, DocumentId, DocumentKeyId, Timestamp}; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdateTruncate { + field: String, + new_size: usize, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdateDescription { + removed_fields: Vec, + truncated_arrays: Vec, + updated_fields: AnyDocument, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChangeUpdateEvent { + #[serde(rename = "_id")] + id: DocumentId, + #[serde(default)] + cluster_time: Option, + document_key: DocumentKeyId, + #[serde(default)] + #[serde(rename = "lsid")] + ls_id: Option, + ns: DatabaseCollection, + // operation_type: String, + update_description: UpdateDescription, + #[serde(default)] + txn_number: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/documentdb/mod.rs b/lambda-events/src/event/documentdb/mod.rs new file mode 100644 index 000000000..a960acf72 --- /dev/null +++ b/lambda-events/src/event/documentdb/mod.rs @@ -0,0 +1,121 @@ +pub mod events; + +use self::events::{ + delete_event::ChangeDeleteEvent, drop_database_event::ChangeDropDatabaseEvent, drop_event::ChangeDropEvent, + insert_event::ChangeInsertEvent, invalidate_event::ChangeInvalidateEvent, rename_event::ChangeRenameEvent, + replace_event::ChangeReplaceEvent, update_event::ChangeUpdateEvent, +}; +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +#[non_exhaustive] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(tag = "operationType", rename_all = "camelCase")] +pub enum ChangeEvent { + Insert(ChangeInsertEvent), + Delete(ChangeDeleteEvent), + Drop(ChangeDropEvent), + DropDatabase(ChangeDropDatabaseEvent), + Invalidate(ChangeInvalidateEvent), + Replace(ChangeReplaceEvent), + Update(ChangeUpdateEvent), + Rename(ChangeRenameEvent), +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct DocumentDbInnerEvent { + pub event: ChangeEvent, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DocumentDbEvent { + #[serde(default)] + pub event_source_arn: Option, + pub events: Vec, + #[serde(default)] + pub event_source: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +#[cfg(feature = "documentdb")] +mod test { + use super::*; + + pub type Event = DocumentDbEvent; + + fn test_example(data: &[u8]) { + let parsed: Event = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: Event = serde_json::from_slice(output.as_bytes()).unwrap(); + + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_documentdb_insert_event() { + test_example(include_bytes!("../../fixtures/example-documentdb-insert-event.json")); + } + + #[test] + fn example_documentdb_delete_event() { + test_example(include_bytes!("../../fixtures/example-documentdb-delete-event.json")); + } + + #[test] + fn example_documentdb_drop_event() { + test_example(include_bytes!("../../fixtures/example-documentdb-drop-event.json")); + } + + #[test] + fn example_documentdb_replace_event() { + test_example(include_bytes!("../../fixtures/example-documentdb-replace-event.json")); + } + + #[test] + fn example_documentdb_update_event() { + test_example(include_bytes!("../../fixtures/example-documentdb-update-event.json")); + } + + #[test] + fn example_documentdb_rename_event() { + test_example(include_bytes!("../../fixtures/example-documentdb-rename-event.json")); + } + + #[test] + fn example_documentdb_invalidate_event() { + test_example(include_bytes!( + "../../fixtures/example-documentdb-invalidate-event.json" + )); + } + + #[test] + fn example_documentdb_drop_database_event() { + test_example(include_bytes!( + "../../fixtures/example-documentdb-drop-database-event.json" + )); + } +} diff --git a/lambda-events/src/event/dynamodb/attributes.rs b/lambda-events/src/event/dynamodb/attributes.rs new file mode 100644 index 000000000..ac43adea4 --- /dev/null +++ b/lambda-events/src/event/dynamodb/attributes.rs @@ -0,0 +1,194 @@ +use base64::Engine; +use serde_dynamo::AttributeValue; +use std::collections::HashMap; + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_null_attribute() { + let value = serde_json::json!({ + "NULL": true + }); + + let attr: AttributeValue = serde_json::from_value(value.clone()).unwrap(); + match attr { + AttributeValue::Null(true) => {} + other => panic!("unexpected value {other:?}"), + } + + let reparsed = serde_json::to_value(attr).unwrap(); + assert_eq!(value, reparsed); + } + + #[test] + fn test_string_attribute() { + let value = serde_json::json!({ + "S": "value" + }); + + let attr: AttributeValue = serde_json::from_value(value.clone()).unwrap(); + match attr { + AttributeValue::S(ref s) => assert_eq!("value", s.as_str()), + other => panic!("unexpected value {other:?}"), + } + + let reparsed = serde_json::to_value(attr).unwrap(); + assert_eq!(value, reparsed); + } + + #[test] + fn test_number_attribute() { + let value = serde_json::json!({ + "N": "123.45" + }); + + let attr: AttributeValue = serde_json::from_value(value.clone()).unwrap(); + match attr { + AttributeValue::N(ref n) => assert_eq!("123.45", n.as_str()), + other => panic!("unexpected value {other:?}"), + } + + let reparsed = serde_json::to_value(attr).unwrap(); + assert_eq!(value, reparsed); + } + + #[test] + fn test_binary_attribute() { + let value = serde_json::json!({ + "B": "dGhpcyB0ZXh0IGlzIGJhc2U2NC1lbmNvZGVk" + }); + + let attr: AttributeValue = serde_json::from_value(value.clone()).unwrap(); + match attr { + AttributeValue::B(ref b) => { + let expected = base64::engine::general_purpose::STANDARD + .decode("dGhpcyB0ZXh0IGlzIGJhc2U2NC1lbmNvZGVk") + .unwrap(); + assert_eq!(&expected, b) + } + other => panic!("unexpected value {other:?}"), + } + + let reparsed = serde_json::to_value(attr).unwrap(); + assert_eq!(value, reparsed); + } + + #[test] + fn test_boolean_attribute() { + let value = serde_json::json!({ + "BOOL": true + }); + + let attr: AttributeValue = serde_json::from_value(value.clone()).unwrap(); + match attr { + AttributeValue::Bool(b) => assert!(b), + other => panic!("unexpected value {other:?}"), + } + + let reparsed = serde_json::to_value(attr).unwrap(); + assert_eq!(value, reparsed); + } + + #[test] + fn test_string_set_attribute() { + let value = serde_json::json!({ + "SS": ["Giraffe", "Hippo" ,"Zebra"] + }); + + let attr: AttributeValue = serde_json::from_value(value.clone()).unwrap(); + match attr { + AttributeValue::Ss(ref s) => { + let expected = vec!["Giraffe", "Hippo", "Zebra"]; + assert_eq!(expected, s.iter().collect::>()); + } + other => panic!("unexpected value {other:?}"), + } + + let reparsed = serde_json::to_value(attr).unwrap(); + assert_eq!(value, reparsed); + } + + #[test] + fn test_number_set_attribute() { + let value = serde_json::json!({ + "NS": ["42.2", "-19", "7.5", "3.14"] + }); + + let attr: AttributeValue = serde_json::from_value(value.clone()).unwrap(); + match attr { + AttributeValue::Ns(ref s) => { + let expected = vec!["42.2", "-19", "7.5", "3.14"]; + assert_eq!(expected, s.iter().collect::>()); + } + other => panic!("unexpected value {other:?}"), + } + + let reparsed = serde_json::to_value(attr).unwrap(); + assert_eq!(value, reparsed); + } + + #[test] + fn test_binary_set_attribute() { + let value = serde_json::json!({ + "BS": ["U3Vubnk=", "UmFpbnk=", "U25vd3k="] + }); + + let attr: AttributeValue = serde_json::from_value(value.clone()).unwrap(); + match attr { + AttributeValue::Bs(ref s) => { + let expected = vec!["U3Vubnk=", "UmFpbnk=", "U25vd3k="] + .into_iter() + .flat_map(|s| base64::engine::general_purpose::STANDARD.decode(s)) + .collect::>(); + assert_eq!(&expected, s); + } + other => panic!("unexpected value {other:?}"), + } + + let reparsed = serde_json::to_value(attr).unwrap(); + assert_eq!(value, reparsed); + } + + #[test] + fn test_attribute_list_attribute() { + let value = serde_json::json!({ + "L": [ {"S": "Cookies"} , {"S": "Coffee"}, {"N": "3.14159"}] + }); + + let attr: AttributeValue = serde_json::from_value(value.clone()).unwrap(); + match attr { + AttributeValue::L(ref s) => { + let expected = vec![ + AttributeValue::S("Cookies".into()), + AttributeValue::S("Coffee".into()), + AttributeValue::N("3.14159".into()), + ]; + assert_eq!(&expected, s); + } + other => panic!("unexpected value {other:?}"), + } + + let reparsed = serde_json::to_value(attr).unwrap(); + assert_eq!(value, reparsed); + } + + #[test] + fn test_attribute_map_attribute() { + let value = serde_json::json!({ + "M": {"Name": {"S": "Joe"}, "Age": {"N": "35"}} + }); + + let attr: AttributeValue = serde_json::from_value(value).unwrap(); + match attr { + AttributeValue::M(s) => { + let mut expected = HashMap::new(); + expected.insert("Name".into(), AttributeValue::S("Joe".into())); + expected.insert("Age".into(), AttributeValue::N("35".into())); + assert_eq!(expected, s); + } + other => panic!("unexpected value {other:?}"), + } + } +} diff --git a/lambda-events/src/event/dynamodb/mod.rs b/lambda-events/src/event/dynamodb/mod.rs new file mode 100644 index 000000000..0f67dc0af --- /dev/null +++ b/lambda-events/src/event/dynamodb/mod.rs @@ -0,0 +1,369 @@ +use crate::{ + custom_serde::{deserialize_nullish, float_unix_epoch}, + streams::DynamoDbBatchItemFailure, + time_window::*, +}; +#[cfg(feature = "builders")] +use bon::Builder; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +use std::fmt; + +#[cfg(test)] +mod attributes; + +#[non_exhaustive] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum StreamViewType { + NewImage, + OldImage, + NewAndOldImages, + #[default] + KeysOnly, +} + +impl fmt::Display for StreamViewType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let val = match self { + StreamViewType::NewImage => "NEW_IMAGE", + StreamViewType::OldImage => "OLD_IMAGE", + StreamViewType::NewAndOldImages => "NEW_AND_OLD_IMAGES", + StreamViewType::KeysOnly => "KEYS_ONLY", + }; + write!(f, "{val}") + } +} + +#[non_exhaustive] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum StreamStatus { + Enabling, + Enabled, + Disabling, + #[default] + Disabled, +} + +impl fmt::Display for StreamStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let val = match self { + StreamStatus::Enabling => "ENABLING", + StreamStatus::Enabled => "ENABLED", + StreamStatus::Disabling => "DISABLING", + StreamStatus::Disabled => "DISABLED", + }; + write!(f, "{val}") + } +} + +#[non_exhaustive] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum SharedIteratorType { + TrimHorizon, + #[default] + Latest, + AtSequenceNumber, + AfterSequenceNumber, +} + +impl fmt::Display for SharedIteratorType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let val = match self { + SharedIteratorType::TrimHorizon => "TRIM_HORIZON", + SharedIteratorType::Latest => "LATEST", + SharedIteratorType::AtSequenceNumber => "AT_SEQUENCE_NUMBER", + SharedIteratorType::AfterSequenceNumber => "AFTER_SEQUENCE_NUMBER", + }; + write!(f, "{val}") + } +} + +#[non_exhaustive] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum OperationType { + #[default] + Insert, + Modify, + Remove, +} + +impl fmt::Display for OperationType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let val = match self { + OperationType::Insert => "INSERT", + OperationType::Modify => "MODIFY", + OperationType::Remove => "REMOVE", + }; + write!(f, "{val}") + } +} + +#[non_exhaustive] +#[derive(Clone, Default, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum KeyType { + #[default] + Hash, + Range, +} + +impl fmt::Display for KeyType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let val = match self { + KeyType::Hash => "HASH", + KeyType::Range => "RANGE", + }; + write!(f, "{val}") + } +} + +/// The `Event` stream event handled to Lambda +/// +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +pub struct Event { + #[serde(rename = "Records")] + pub records: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `TimeWindowEvent` represents an Amazon Dynamodb event when using time windows +/// ref. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TimeWindowEvent { + #[serde(rename = "DynamoDBEvent")] + #[serde(flatten)] + pub dynamo_db_event: Event, + #[serde(rename = "TimeWindowProperties")] + #[serde(flatten)] + pub time_window_properties: TimeWindowProperties, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `TimeWindowEventResponse` is the outer structure to report batch item failures for DynamoDBTimeWindowEvent. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TimeWindowEventResponse { + #[serde(rename = "TimeWindowEventResponseProperties")] + #[serde(flatten)] + pub time_window_event_response_properties: TimeWindowEventResponseProperties, + pub batch_item_failures: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// EventRecord stores information about each record of a DynamoDb stream event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EventRecord { + /// The region in which the GetRecords request was received. + pub aws_region: String, + /// The main body of the stream record, containing all of the DynamoDB-specific + /// fields. + #[serde(rename = "dynamodb")] + pub change: StreamRecord, + /// A globally unique identifier for the event that was recorded in this stream + /// record. + #[serde(rename = "eventID")] + pub event_id: String, + /// The type of data modification that was performed on the DynamoDB table: + /// + /// * INSERT - a new item was added to the table. + /// + /// * MODIFY - one or more of an existing item's attributes were modified. + /// + /// * REMOVE - the item was deleted from the table + pub event_name: String, + /// The AWS service from which the stream record originated. For DynamoDB Streams, + /// this is aws:dynamodb. + #[serde(default)] + pub event_source: Option, + /// The version number of the stream record format. This number is updated whenever + /// the structure of Record is modified. + /// + /// Client applications must not assume that eventVersion will remain at a particular + /// value, as this number is subject to change at any time. In general, eventVersion + /// will only increase as the low-level DynamoDB Streams API evolves. + #[serde(default)] + pub event_version: Option, + /// The event source ARN of DynamoDB + #[serde(rename = "eventSourceARN")] + #[serde(default)] + pub event_source_arn: Option, + /// Items that are deleted by the Time to Live process after expiration have + /// the following fields: + /// + /// * Records[].userIdentity.type + /// + /// "Service" + /// + /// * Records[].userIdentity.principalId + /// + /// "dynamodb.amazonaws.com" + #[serde(default)] + pub user_identity: Option, + /// Describes the record format and relevant mapping information that + /// should be applied to schematize the records on the stream. For + /// DynamoDB Streams, this is application/json. + #[serde(default)] + pub record_format: Option, + /// The DynamoDB table that this event was recorded for. + #[serde(default)] + pub table_name: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UserIdentity { + #[serde(default)] + pub type_: String, + #[serde(default)] + pub principal_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `DynamoDbStreamRecord` represents a description of a single data modification that was performed on an item +/// in a DynamoDB table. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StreamRecord { + /// The approximate date and time when the stream record was created, in UNIX + /// epoch time () format. Might not be present in + /// the record: + #[serde(rename = "ApproximateCreationDateTime")] + #[serde(with = "float_unix_epoch")] + #[serde(default)] + pub approximate_creation_date_time: DateTime, + /// The primary key attribute(s) for the DynamoDB item that was modified. + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + #[serde(rename = "Keys")] + pub keys: serde_dynamo::Item, + /// The item in the DynamoDB table as it appeared after it was modified. + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + #[serde(rename = "NewImage")] + pub new_image: serde_dynamo::Item, + /// The item in the DynamoDB table as it appeared before it was modified. + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + #[serde(rename = "OldImage")] + pub old_image: serde_dynamo::Item, + /// The sequence number of the stream record. + #[serde(default)] + #[serde(rename = "SequenceNumber")] + pub sequence_number: Option, + /// The size of the stream record, in bytes. + #[serde(rename = "SizeBytes")] + pub size_bytes: i64, + /// The type of data from the modified DynamoDB item that was captured in this + /// stream record. + #[serde(default)] + #[serde(rename = "StreamViewType")] + pub stream_view_type: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +#[allow(deprecated)] +mod test { + use super::*; + use crate::fixtures::verify_serde_roundtrip; + use chrono::TimeZone; + + #[test] + #[cfg(feature = "dynamodb")] + fn example_dynamodb_event() { + let data = include_bytes!("../../fixtures/example-dynamodb-event.json"); + let mut parsed: Event = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: Event = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + + let event = parsed.records.pop().unwrap(); + let date = Utc.ymd(2016, 12, 2).and_hms(1, 27, 0); + assert_eq!(date, event.change.approximate_creation_date_time); + } + + #[test] + #[cfg(feature = "dynamodb")] + fn example_dynamodb_event_null_items() { + let data = include_bytes!("../../fixtures/example-dynamodb-event-null-items.json"); + let event = verify_serde_roundtrip::(data); + assert_eq!(0, event.records[0].change.keys.len()); + assert_eq!(0, event.records[0].change.new_image.len()); + assert_eq!(0, event.records[0].change.old_image.len()); + } + + #[test] + #[cfg(feature = "dynamodb")] + fn example_dynamodb_event_with_optional_fields() { + let data = include_bytes!("../../fixtures/example-dynamodb-event-record-with-optional-fields.json"); + let parsed: EventRecord = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: EventRecord = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + let date = Utc.timestamp_micros(0).unwrap(); // 1970-01-01T00:00:00Z + assert_eq!(date, reparsed.change.approximate_creation_date_time); + } +} diff --git a/lambda-events/src/event/ecr_scan/mod.rs b/lambda-events/src/event/ecr_scan/mod.rs new file mode 100644 index 000000000..588edc7e4 --- /dev/null +++ b/lambda-events/src/event/ecr_scan/mod.rs @@ -0,0 +1,123 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EcrScanEvent { + #[serde(default)] + pub version: Option, + #[serde(default)] + pub id: Option, + #[serde(default)] + #[serde(rename = "detail-type")] + pub detail_type: Option, + #[serde(default)] + pub source: Option, + #[serde(default)] + pub time: Option, + #[serde(default)] + pub region: Option, + pub resources: Vec, + #[serde(default)] + pub account: Option, + pub detail: EcrScanEventDetailType, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EcrScanEventDetailType { + #[serde(default)] + #[serde(rename = "scan-status")] + pub scan_status: Option, + #[serde(default)] + #[serde(rename = "repository-name")] + pub repository_name: Option, + #[serde(rename = "finding-severity-counts")] + pub finding_severity_counts: EcrScanEventFindingSeverityCounts, + #[serde(default)] + #[serde(rename = "image-digest")] + pub image_digest: Option, + #[serde(rename = "image-tags")] + pub image_tags: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EcrScanEventFindingSeverityCounts { + #[serde(default)] + #[serde(rename = "CRITICAL")] + pub critical: Option, + #[serde(default)] + #[serde(rename = "HIGH")] + pub high: Option, + #[serde(default)] + #[serde(rename = "MEDIUM")] + pub medium: Option, + #[serde(default)] + #[serde(rename = "LOW")] + pub low: Option, + #[serde(default)] + #[serde(rename = "INFORMATIONAL")] + pub informational: Option, + #[serde(default)] + #[serde(rename = "UNDEFINED")] + pub undefined: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "ecr_scan")] + fn example_ecr_image_scan_event() { + let data = include_bytes!("../../fixtures/example-ecr-image-scan-event.json"); + let parsed: EcrScanEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: EcrScanEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "ecr_scan")] + fn example_ecr_image_scan_event_with_missing_severities() { + let data = include_bytes!("../../fixtures/example-ecr-image-scan-event-with-missing-severities.json"); + let parsed: EcrScanEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: EcrScanEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/eventbridge/mod.rs b/lambda-events/src/event/eventbridge/mod.rs new file mode 100644 index 000000000..36dc43b09 --- /dev/null +++ b/lambda-events/src/event/eventbridge/mod.rs @@ -0,0 +1,85 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use chrono::{DateTime, Utc}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::Value; + +/// Parse EventBridge events. +/// Deserialize the event detail into a structure that's `DeserializeOwned`. +/// +/// See for structure details. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(bound(deserialize = "T1: DeserializeOwned"))] +#[serde(rename_all = "kebab-case")] +pub struct EventBridgeEvent +where + T1: Serialize, + T1: DeserializeOwned, +{ + #[serde(default)] + pub version: Option, + #[serde(default)] + pub id: Option, + pub detail_type: String, + pub source: String, + #[serde(default)] + pub account: Option, + #[serde(default)] + pub time: Option>, + #[serde(default)] + pub region: Option, + #[serde(default)] + pub resources: Option>, + #[serde(bound = "")] + pub detail: T1, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +#[cfg(feature = "eventbridge")] +mod test { + use super::*; + + #[test] + fn example_eventbridge_obj_event() { + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] + #[serde(rename_all = "kebab-case")] + struct Ec2StateChange { + instance_id: String, + state: String, + } + + // Example from https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/monitoring-instance-state-changes.html + let data = include_bytes!("../../fixtures/example-eventbridge-event-obj.json"); + let parsed: EventBridgeEvent = serde_json::from_slice(data).unwrap(); + + assert_eq!("i-abcd1111", parsed.detail.instance_id); + assert_eq!("pending", parsed.detail.state); + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: EventBridgeEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_eventbridge_schedule_event() { + let data = include_bytes!("../../fixtures/example-eventbridge-schedule.json"); + let parsed: EventBridgeEvent = serde_json::from_slice(data).unwrap(); + + assert_eq!("aws.events", parsed.source); + assert_eq!("Scheduled Event", parsed.detail_type); + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: EventBridgeEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/firehose/mod.rs b/lambda-events/src/event/firehose/mod.rs new file mode 100644 index 000000000..527c5ba11 --- /dev/null +++ b/lambda-events/src/event/firehose/mod.rs @@ -0,0 +1,152 @@ +use crate::{ + custom_serde::deserialize_nullish, + encodings::{Base64Data, MillisecondTimestamp}, +}; +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +use std::collections::HashMap; + +/// `KinesisFirehoseEvent` represents the input event from Amazon Kinesis Firehose. It is used as the input parameter. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KinesisFirehoseEvent { + #[serde(default)] + pub invocation_id: Option, + /// nolint: stylecheck + #[serde(default)] + pub delivery_stream_arn: Option, + /// nolint: stylecheck + #[serde(default)] + pub source_kinesis_stream_arn: Option, + #[serde(default)] + pub region: Option, + pub records: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KinesisFirehoseEventRecord { + #[serde(default)] + pub record_id: Option, + pub approximate_arrival_timestamp: MillisecondTimestamp, + pub data: Base64Data, + #[serde(rename = "kinesisRecordMetadata")] + pub kinesis_firehose_record_metadata: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KinesisFirehoseResponse { + pub records: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KinesisFirehoseResponseRecord { + #[serde(default)] + pub record_id: Option, + /// The status of the transformation. May be TransformedStateOk, TransformedStateDropped or TransformedStateProcessingFailed + #[serde(default)] + pub result: Option, + pub data: Base64Data, + pub metadata: KinesisFirehoseResponseRecordMetadata, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KinesisFirehoseResponseRecordMetadata { + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub partition_keys: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KinesisFirehoseRecordMetadata { + #[serde(default)] + pub shard_id: Option, + #[serde(default)] + pub partition_key: Option, + #[serde(default)] + pub sequence_number: Option, + pub subsequence_number: i64, + pub approximate_arrival_timestamp: MillisecondTimestamp, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "firehose")] + fn example_firehose_event() { + let data = include_bytes!("../../fixtures/example-firehose-event.json"); + let parsed: KinesisFirehoseEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: KinesisFirehoseEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/iam/mod.rs b/lambda-events/src/event/iam/mod.rs new file mode 100644 index 000000000..9a04a8dae --- /dev/null +++ b/lambda-events/src/event/iam/mod.rs @@ -0,0 +1,220 @@ +#[cfg(feature = "builders")] +use bon::Builder; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +use std::{borrow::Cow, collections::HashMap, fmt}; + +use serde::{ + de::{Error as DeError, MapAccess, Visitor}, + Deserialize, Deserializer, Serialize, +}; + +/// `IamPolicyDocument` represents an IAM policy document. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct IamPolicyDocument { + #[serde(default)] + pub version: Option, + pub statement: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `IamPolicyStatement` represents one statement from IAM policy with action, effect and resource +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct IamPolicyStatement { + #[serde(deserialize_with = "deserialize_string_or_slice")] + pub action: Vec, + #[serde(default = "default_statement_effect")] + pub effect: IamPolicyEffect, + #[serde(deserialize_with = "deserialize_string_or_slice")] + pub resource: Vec, + #[serde(default, deserialize_with = "deserialize_policy_condition")] + #[serde(skip_serializing_if = "Option::is_none")] + pub condition: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +pub type IamPolicyCondition = HashMap>>; + +#[non_exhaustive] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub enum IamPolicyEffect { + #[default] + Allow, + Deny, +} + +fn default_statement_effect() -> IamPolicyEffect { + IamPolicyEffect::Allow +} + +#[derive(serde::Deserialize)] +#[serde(untagged)] +enum StringOrSlice { + String(String), + Slice(Vec), +} + +/// Deserializes `Vec`, from a JSON `string` or `[string]`. +fn deserialize_string_or_slice<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let string_or_slice = StringOrSlice::deserialize(deserializer)?; + + match string_or_slice { + StringOrSlice::Slice(slice) => Ok(slice), + StringOrSlice::String(s) => Ok(vec![s]), + } +} + +fn deserialize_policy_condition<'de, D>(de: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + de.deserialize_option(IamPolicyConditionVisitor) +} + +struct IamPolicyConditionVisitor; + +impl<'de> Visitor<'de> for IamPolicyConditionVisitor { + type Value = Option; + + // Format a message stating what data this Visitor expects to receive. + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("lots of things can go wrong with a IAM Policy Condition") + } + + fn visit_unit(self) -> Result + where + E: DeError, + { + Ok(None) + } + + fn visit_none(self) -> Result + where + E: DeError, + { + Ok(None) + } + + fn visit_some(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_map(self) + } + + fn visit_map(self, mut access: M) -> Result + where + M: MapAccess<'de>, + { + let mut map = HashMap::with_capacity(access.size_hint().unwrap_or(0)); + + while let Some((key, val)) = access.next_entry::, HashMap, StringOrSlice>>()? { + let mut value = HashMap::with_capacity(val.len()); + for (val_key, string_or_slice) in val { + let val = match string_or_slice { + StringOrSlice::Slice(slice) => slice, + StringOrSlice::String(s) => vec![s], + }; + value.insert(val_key.into_owned(), val); + } + + map.insert(key.into_owned(), value); + } + + Ok(Some(map)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_deserialize_string_condition() { + let data = serde_json::json!({ + "condition": { + "StringEquals": { + "iam:RegisterSecurityKey": "Activate", + "iam:FIDO-certification": "L1plus" + } + } + }); + + #[derive(Deserialize)] + struct Test { + #[serde(deserialize_with = "deserialize_policy_condition")] + condition: Option, + } + + let test: Test = serde_json::from_value(data).unwrap(); + let condition = test.condition.unwrap(); + assert_eq!(1, condition.len()); + + assert_eq!(vec!["Activate"], condition["StringEquals"]["iam:RegisterSecurityKey"]); + assert_eq!(vec!["L1plus"], condition["StringEquals"]["iam:FIDO-certification"]); + } + + #[test] + fn test_deserialize_slide_condition() { + let data = serde_json::json!({ + "condition": {"StringLike": {"s3:prefix": ["janedoe/*"]}} + }); + + #[derive(Deserialize)] + struct Test { + #[serde(deserialize_with = "deserialize_policy_condition")] + condition: Option, + } + + let test: Test = serde_json::from_value(data).unwrap(); + let condition = test.condition.unwrap(); + assert_eq!(1, condition.len()); + + assert_eq!(vec!["janedoe/*"], condition["StringLike"]["s3:prefix"]); + } + + #[test] + fn test_serialize_none_condition() { + let policy = IamPolicyStatement { + action: vec!["some:action".into()], + effect: IamPolicyEffect::Allow, + resource: vec!["some:resource".into()], + condition: None, + #[cfg(feature = "catch-all-fields")] + other: Default::default(), + }; + let policy_ser = serde_json::to_value(policy).unwrap(); + + assert_eq!( + policy_ser, + serde_json::json!({ + "Action": ["some:action"], + "Effect": "Allow", + "Resource": ["some:resource"] + }) + ); + } +} diff --git a/lambda-events/src/event/iot/mod.rs b/lambda-events/src/event/iot/mod.rs new file mode 100644 index 000000000..5fe14f565 --- /dev/null +++ b/lambda-events/src/event/iot/mod.rs @@ -0,0 +1,188 @@ +use crate::{custom_serde::serialize_headers, encodings::Base64Data, iam::IamPolicyDocument}; +#[cfg(feature = "builders")] +use bon::Builder; +use http::HeaderMap; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +/// `IoTCoreCustomAuthorizerRequest` represents the request to an IoT Core custom authorizer. +/// See +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct IoTCoreCustomAuthorizerRequest { + #[serde(default)] + pub token: Option, + pub signature_verified: bool, + pub protocols: Vec, + pub protocol_data: Option, + pub connection_metadata: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct IoTCoreProtocolData { + pub tls: Option, + pub http: Option, + pub mqtt: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct IoTCoreTlsContext { + #[serde(default)] + pub server_name: Option, + #[serde(default)] + pub x509_certificate_pem: Option, + #[serde(default)] + pub principal_id: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct IoTCoreHttpContext { + #[serde(deserialize_with = "http_serde::header_map::deserialize", default)] + #[serde(serialize_with = "serialize_headers")] + pub headers: HeaderMap, + #[serde(default)] + pub query_string: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct IoTCoreMqttContext { + #[serde(default)] + pub client_id: Option, + /// X.509 custom authorizer requests don't include a password field. + /// Default to empty `Vec` when absent. + /// Serializing result will be `password: ""` + #[serde(default)] + pub password: Base64Data, + #[serde(default)] + pub username: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct IoTCoreConnectionMetadata { + #[serde(default)] + pub id: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `IoTCoreCustomAuthorizerResponse` represents the response from an IoT Core custom authorizer. +/// See +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct IoTCoreCustomAuthorizerResponse { + pub is_authenticated: bool, + #[serde(default)] + pub principal_id: Option, + pub disconnect_after_in_seconds: u32, + pub refresh_after_in_seconds: u32, + pub policy_documents: Vec>, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "iot")] + fn example_iot_custom_auth_request() { + let data = include_bytes!("../../fixtures/example-iot-custom-auth-request.json"); + let parsed: IoTCoreCustomAuthorizerRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: IoTCoreCustomAuthorizerRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "iot")] + fn example_iot_custom_auth_request_x509() { + let data = include_bytes!("../../fixtures/example-iot-custom-auth-request-x509.json"); + let parsed: IoTCoreCustomAuthorizerRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: IoTCoreCustomAuthorizerRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "iot")] + fn example_iot_custom_auth_response() { + let data = include_bytes!("../../fixtures/example-iot-custom-auth-response.json"); + let parsed: IoTCoreCustomAuthorizerResponse = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: IoTCoreCustomAuthorizerResponse = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/iot_1_click/mod.rs b/lambda-events/src/event/iot_1_click/mod.rs new file mode 100644 index 000000000..58b6e2562 --- /dev/null +++ b/lambda-events/src/event/iot_1_click/mod.rs @@ -0,0 +1,126 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +use std::collections::HashMap; + +use crate::custom_serde::deserialize_nullish; + +/// `IoTOneClickEvent` represents a click event published by clicking button type +/// device. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct IoTOneClickEvent { + pub device_event: IoTOneClickDeviceEvent, + pub device_info: IoTOneClickDeviceInfo, + pub placement_info: IoTOneClickPlacementInfo, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct IoTOneClickDeviceEvent { + pub button_clicked: IoTOneClickButtonClicked, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct IoTOneClickButtonClicked { + #[serde(default)] + pub click_type: Option, + #[serde(default)] + pub reported_time: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct IoTOneClickDeviceInfo { + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub attributes: HashMap, + #[serde(default)] + pub type_: Option, + #[serde(default)] + pub device_id: Option, + pub remaining_life: f64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct IoTOneClickPlacementInfo { + #[serde(default)] + pub project_name: Option, + #[serde(default)] + pub placement_name: Option, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub attributes: HashMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub devices: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "iot_1_click")] + fn example_iot_1_click_event() { + let data = include_bytes!("../../fixtures/example-iot_1_click-event.json"); + let parsed: IoTOneClickEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: IoTOneClickEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/iot_button/mod.rs b/lambda-events/src/event/iot_button/mod.rs new file mode 100644 index 000000000..75ad7ca31 --- /dev/null +++ b/lambda-events/src/event/iot_button/mod.rs @@ -0,0 +1,41 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct IoTButtonEvent { + #[serde(default)] + pub serial_number: Option, + #[serde(default)] + pub click_type: Option, + #[serde(default)] + pub battery_voltage: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "iot_button")] + fn example_iot_button_event() { + let data = include_bytes!("../../fixtures/example-iot_button-event.json"); + let parsed: IoTButtonEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: IoTButtonEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/iot_deprecated/mod.rs b/lambda-events/src/event/iot_deprecated/mod.rs new file mode 100644 index 000000000..e29bfab18 --- /dev/null +++ b/lambda-events/src/event/iot_deprecated/mod.rs @@ -0,0 +1,60 @@ +use crate::iot::*; +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +/// `IoTCustomAuthorizerRequest` contains data coming in to a custom IoT device gateway authorizer function. +/// Deprecated: Use IoTCoreCustomAuthorizerRequest instead. `IoTCustomAuthorizerRequest` does not correctly model the request schema +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct IoTCustomAuthorizerRequest { + pub http_context: Option, + pub mqtt_context: Option, + pub tls_context: Option, + #[serde(default)] + #[serde(rename = "token")] + pub authorization_token: Option, + #[serde(default)] + pub token_signature: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +pub type IoTHttpContext = IoTCoreHttpContext; + +pub type IoTMqttContext = IoTCoreMqttContext; + +pub type IoTTlsContext = IoTCoreTlsContext; + +/// `IoTCustomAuthorizerResponse` represents the expected format of an IoT device gateway authorization response. +/// Deprecated: Use IoTCoreCustomAuthorizerResponse. `IoTCustomAuthorizerResponse` does not correctly model the response schema. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct IoTCustomAuthorizerResponse { + pub is_authenticated: bool, + #[serde(default)] + pub principal_id: Option, + pub disconnect_after_in_seconds: i32, + pub refresh_after_in_seconds: i32, + pub policy_documents: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/kafka/mod.rs b/lambda-events/src/event/kafka/mod.rs new file mode 100644 index 000000000..5ca28b3db --- /dev/null +++ b/lambda-events/src/event/kafka/mod.rs @@ -0,0 +1,71 @@ +use crate::{custom_serde::deserialize_nullish, encodings::MillisecondTimestamp}; +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +use std::collections::HashMap; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KafkaEvent { + #[serde(default)] + pub event_source: Option, + #[serde(default)] + pub event_source_arn: Option, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub records: HashMap>, + #[serde(default)] + pub bootstrap_servers: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KafkaRecord { + #[serde(default)] + pub topic: Option, + pub partition: i64, + pub offset: i64, + pub timestamp: MillisecondTimestamp, + #[serde(default)] + pub timestamp_type: Option, + pub key: Option, + pub value: Option, + pub headers: Vec>>, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "kafka")] + fn example_kafka_event() { + let data = include_bytes!("../../fixtures/example-kafka-event.json"); + let parsed: KafkaEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: KafkaEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/kinesis/analytics.rs b/lambda-events/src/event/kinesis/analytics.rs new file mode 100644 index 000000000..ae4381135 --- /dev/null +++ b/lambda-events/src/event/kinesis/analytics.rs @@ -0,0 +1,80 @@ +use crate::encodings::Base64Data; +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KinesisAnalyticsOutputDeliveryEvent { + #[serde(default)] + pub invocation_id: Option, + #[serde(default)] + pub application_arn: Option, + pub records: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KinesisAnalyticsOutputDeliveryEventRecord { + #[serde(default)] + pub record_id: Option, + pub data: Base64Data, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KinesisAnalyticsOutputDeliveryResponse { + pub records: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KinesisAnalyticsOutputDeliveryResponseRecord { + #[serde(default)] + pub record_id: Option, + /// possible values include Ok and DeliveryFailed + #[serde(default)] + pub result: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/kinesis/event.rs b/lambda-events/src/event/kinesis/event.rs new file mode 100644 index 000000000..09b54eb40 --- /dev/null +++ b/lambda-events/src/event/kinesis/event.rs @@ -0,0 +1,180 @@ +use crate::{ + encodings::{Base64Data, SecondTimestamp}, + time_window::{TimeWindowEventResponseProperties, TimeWindowProperties}, +}; +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Default, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KinesisEvent { + #[serde(rename = "Records")] + pub records: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `KinesisTimeWindowEvent` represents an Amazon Dynamodb event when using time windows +/// ref. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KinesisTimeWindowEvent { + #[serde(rename = "KinesisEvent")] + #[serde(flatten)] + pub kinesis_event: KinesisEvent, + #[serde(rename = "TimeWindowProperties")] + #[serde(flatten)] + pub time_window_properties: TimeWindowProperties, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `KinesisTimeWindowEventResponse` is the outer structure to report batch item failures for KinesisTimeWindowEvent. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KinesisTimeWindowEventResponse { + #[serde(rename = "TimeWindowEventResponseProperties")] + #[serde(flatten)] + pub time_window_event_response_properties: TimeWindowEventResponseProperties, + // pub batch_item_failures: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KinesisEventRecord { + /// nolint: stylecheck + #[serde(default)] + pub aws_region: Option, + #[serde(default)] + #[serde(rename = "eventID")] + pub event_id: Option, + #[serde(default)] + pub event_name: Option, + #[serde(default)] + pub event_source: Option, + /// nolint: stylecheck + #[serde(default)] + #[serde(rename = "eventSourceARN")] + pub event_source_arn: Option, + #[serde(default)] + pub event_version: Option, + /// nolint: stylecheck + #[serde(default)] + pub invoke_identity_arn: Option, + pub kinesis: KinesisRecord, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KinesisRecord { + pub approximate_arrival_timestamp: SecondTimestamp, + pub data: Base64Data, + #[serde(default)] + pub encryption_type: KinesisEncryptionType, + #[serde(default)] + pub partition_key: String, + #[serde(default)] + pub sequence_number: String, + #[serde(default)] + pub kinesis_schema_version: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum KinesisEncryptionType { + #[default] + None, + Kms, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "kinesis")] + fn example_kinesis_event() { + let data = include_bytes!("../../fixtures/example-kinesis-event.json"); + let parsed: KinesisEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(KinesisEncryptionType::None, parsed.records[0].kinesis.encryption_type); + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: KinesisEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "kinesis")] + fn example_kinesis_event_encrypted() { + let data = include_bytes!("../../fixtures/example-kinesis-event-encrypted.json"); + let parsed: KinesisEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(KinesisEncryptionType::Kms, parsed.records[0].kinesis.encryption_type); + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: KinesisEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + /// `cargo lambda init` autogenerates code that relies on `Default` being implemented for event structs. + /// + /// This test validates that `Default` is implemented for each KinesisEvent struct. + #[test] + #[cfg(feature = "kinesis")] + fn test_ensure_default_implemented_for_structs() { + let _kinesis_event = KinesisEvent::default(); + let _kinesis_time_window_event = KinesisTimeWindowEvent::default(); + let _kinesis_event_record = KinesisEventRecord::default(); + let _kinesis_record = KinesisRecord::default(); + let _kinesis_encryption_type = KinesisEncryptionType::default(); + } +} diff --git a/lambda-events/src/event/kinesis/mod.rs b/lambda-events/src/event/kinesis/mod.rs new file mode 100644 index 000000000..079280b97 --- /dev/null +++ b/lambda-events/src/event/kinesis/mod.rs @@ -0,0 +1,3 @@ +pub mod analytics; +mod event; +pub use self::event::*; diff --git a/lambda-events/src/event/lambda_function_urls/mod.rs b/lambda-events/src/event/lambda_function_urls/mod.rs new file mode 100644 index 000000000..b08700a77 --- /dev/null +++ b/lambda-events/src/event/lambda_function_urls/mod.rs @@ -0,0 +1,170 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use http::HeaderMap; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +use std::collections::HashMap; + +use crate::custom_serde::{deserialize_nullish, serialize_headers}; + +/// `LambdaFunctionUrlRequest` contains data coming from the HTTP request to a Lambda Function URL. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LambdaFunctionUrlRequest { + /// Version is expected to be `"2.0"` + #[serde(default)] + pub version: Option, + #[serde(default)] + pub raw_path: Option, + #[serde(default)] + pub raw_query_string: Option, + pub cookies: Option>, + #[serde(deserialize_with = "http_serde::header_map::deserialize", default)] + #[serde(serialize_with = "serialize_headers")] + pub headers: HeaderMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub query_string_parameters: HashMap, + pub request_context: LambdaFunctionUrlRequestContext, + pub body: Option, + pub is_base64_encoded: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `LambdaFunctionUrlRequestContext` contains the information to identify the AWS account and resources invoking the Lambda function. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LambdaFunctionUrlRequestContext { + #[serde(default)] + pub account_id: Option, + #[serde(default)] + pub request_id: Option, + pub authorizer: Option, + /// APIID is the Lambda URL ID + #[serde(default)] + #[serde(rename = "apiId")] + pub apiid: Option, + /// DomainName is of the format `".lambda-url..on.aws"` + #[serde(default)] + pub domain_name: Option, + /// DomainPrefix is the Lambda URL ID + #[serde(default)] + pub domain_prefix: Option, + #[serde(default)] + pub time: Option, + pub time_epoch: i64, + pub http: LambdaFunctionUrlRequestContextHttpDescription, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `LambdaFunctionUrlRequestContextAuthorizerDescription` contains authorizer information for the request context. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LambdaFunctionUrlRequestContextAuthorizerDescription { + pub iam: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `LambdaFunctionUrlRequestContextAuthorizerIamDescription` contains IAM information for the request context. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LambdaFunctionUrlRequestContextAuthorizerIamDescription { + #[serde(default)] + pub access_key: Option, + #[serde(default)] + pub account_id: Option, + #[serde(default)] + pub caller_id: Option, + #[serde(default)] + pub user_arn: Option, + #[serde(default)] + pub user_id: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `LambdaFunctionUrlRequestContextHttpDescription` contains HTTP information for the request context. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LambdaFunctionUrlRequestContextHttpDescription { + #[serde(default)] + pub method: Option, + #[serde(default)] + pub path: Option, + #[serde(default)] + pub protocol: Option, + #[serde(default)] + pub source_ip: Option, + #[serde(default)] + pub user_agent: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `LambdaFunctionUrlResponse` configures the HTTP response to be returned by Lambda Function URL for the request. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LambdaFunctionUrlResponse { + pub status_code: i64, + #[serde(deserialize_with = "http_serde::header_map::deserialize", default)] + #[serde(serialize_with = "serialize_headers")] + pub headers: HeaderMap, + #[serde(default)] + pub body: Option, + pub is_base64_encoded: bool, + pub cookies: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/lex/mod.rs b/lambda-events/src/event/lex/mod.rs new file mode 100644 index 000000000..88c48c58f --- /dev/null +++ b/lambda-events/src/event/lex/mod.rs @@ -0,0 +1,224 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +use std::collections::HashMap; + +use crate::custom_serde::deserialize_nullish; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LexEvent { + pub message_version: Option, + pub invocation_source: Option, + pub user_id: Option, + pub input_transcript: Option, + pub session_attributes: Option, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub request_attributes: HashMap, + pub bot: Option, + pub output_dialog_mode: Option, + pub current_intent: Option, + pub alternative_intents: Option>, + /// Deprecated: the DialogAction field is never populated by Lex events + pub dialog_action: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LexBot { + pub name: Option, + pub alias: Option, + pub version: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LexCurrentIntent { + pub name: Option, + pub nlu_intent_confidence_score: Option, + pub slots: Option, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub slot_details: HashMap, + pub confirmation_status: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LexAlternativeIntents { + pub name: Option, + pub nlu_intent_confidence_score: Option, + pub slots: Option, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub slot_details: HashMap, + pub confirmation_status: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SlotDetail { + pub resolutions: Option>>, + pub original_value: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LexDialogAction { + pub type_: Option, + pub fulfillment_state: Option, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub message: HashMap, + pub intent_name: Option, + pub slots: Option, + pub slot_to_elicit: Option, + pub response_card: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +pub type SessionAttributes = HashMap; + +pub type Slots = HashMap>; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LexResponse { + pub session_attributes: SessionAttributes, + pub dialog_action: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LexResponseCard { + pub version: Option, + pub content_type: Option, + pub generic_attachments: Option>, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Attachment { + pub title: Option, + pub sub_title: Option, + pub image_url: Option, + pub attachment_link_url: Option, + pub buttons: Option>>, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "lex")] + fn example_lex_event() { + let data = include_bytes!("../../fixtures/example-lex-event.json"); + let parsed: LexEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: LexEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "lex")] + fn example_lex_response() { + let data = include_bytes!("../../fixtures/example-lex-response.json"); + let parsed: LexEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: LexEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/mod.rs b/lambda-events/src/event/mod.rs new file mode 100644 index 000000000..cfd4c9e85 --- /dev/null +++ b/lambda-events/src/event/mod.rs @@ -0,0 +1,213 @@ +/// AWS Lambda event definitions for activemq. +#[cfg(feature = "activemq")] +#[cfg_attr(docsrs, doc(cfg(feature = "activemq")))] +pub mod activemq; + +/// AWS Lambda event definitions for alb. +#[cfg(feature = "alb")] +#[cfg_attr(docsrs, doc(cfg(feature = "alb")))] +pub mod alb; +/// AWS Lambda event definitions for apigw. +#[cfg(feature = "apigw")] +#[cfg_attr(docsrs, doc(cfg(feature = "apigw")))] +pub mod apigw; + +/// AWS Lambda event definitions for appsync. +#[cfg(feature = "appsync")] +#[cfg_attr(docsrs, doc(cfg(feature = "appsync")))] +pub mod appsync; + +/// AWS Lambda event definitions for autoscaling. +#[cfg(feature = "autoscaling")] +#[cfg_attr(docsrs, doc(cfg(feature = "autoscaling")))] +pub mod autoscaling; + +/// AWS Lambda event definitions for agent for amazon bedrock +#[cfg(feature = "bedrock_agent_runtime")] +#[cfg_attr(docsrs, doc(cfg(feature = "bedrock_agent_runtime")))] +pub mod bedrock_agent_runtime; + +/// AWS Lambda event definitions for chime_bot. +#[cfg(feature = "chime_bot")] +#[cfg_attr(docsrs, doc(cfg(feature = "chime_bot")))] +pub mod chime_bot; + +/// AWS Lambda event definitions for clientvpn. +#[cfg(feature = "clientvpn")] +#[cfg_attr(docsrs, doc(cfg(feature = "clientvpn")))] +pub mod clientvpn; + +/// AWS Lambda event definitions for cloudformation. +#[cfg(feature = "cloudformation")] +#[cfg_attr(docsrs, doc(cfg(feature = "cloudformation")))] +pub mod cloudformation; + +/// AWS Lambda event definitions for CloudWatch alarms. +#[cfg(feature = "cloudwatch_alarms")] +#[cfg_attr(docsrs, doc(cfg(feature = "cloudwatch_alarms")))] +pub mod cloudwatch_alarms; + +/// AWS Lambda event definitions for CloudWatch events. +#[cfg(feature = "cloudwatch_events")] +#[cfg_attr(docsrs, doc(cfg(feature = "cloudwatch_events")))] +pub mod cloudwatch_events; + +/// AWS Lambda event definitions for cloudwatch_logs. +#[cfg(feature = "cloudwatch_logs")] +#[cfg_attr(docsrs, doc(cfg(feature = "cloudwatch_logs")))] +pub mod cloudwatch_logs; + +/// AWS Lambda event definitions for code_commit. +#[cfg(feature = "code_commit")] +#[cfg_attr(docsrs, doc(cfg(feature = "code_commit")))] +pub mod code_commit; + +/// AWS Lambda event definitions for codebuild. +#[cfg(feature = "codebuild")] +#[cfg_attr(docsrs, doc(cfg(feature = "codebuild")))] +pub mod codebuild; + +/// AWS Lambda event definitions for codedeploy. +#[cfg(feature = "codedeploy")] +#[cfg_attr(docsrs, doc(cfg(feature = "codedeploy")))] +pub mod codedeploy; + +/// AWS Lambda event definitions for codepipeline_cloudwatch. +#[cfg(feature = "codepipeline_cloudwatch")] +#[cfg_attr(docsrs, doc(cfg(feature = "codepipeline_cloudwatch")))] +pub mod codepipeline_cloudwatch; + +/// AWS Lambda event definitions for codepipeline_job. +#[cfg(feature = "codepipeline_job")] +#[cfg_attr(docsrs, doc(cfg(feature = "codepipeline_job")))] +pub mod codepipeline_job; + +/// AWS Lambda event definitions for cognito. +#[cfg(feature = "cognito")] +#[cfg_attr(docsrs, doc(cfg(feature = "cognito")))] +pub mod cognito; + +/// AWS Lambda event definitions for config. +#[cfg(feature = "config")] +#[cfg_attr(docsrs, doc(cfg(feature = "config")))] +pub mod config; + +/// AWS Lambda event definitions for connect. +#[cfg(feature = "connect")] +#[cfg_attr(docsrs, doc(cfg(feature = "connect")))] +pub mod connect; + +/// AWS Lambda event definitions for AWS Control Tower lifecycle events. +#[cfg(feature = "controltower")] +#[cfg_attr(docsrs, doc(cfg(feature = "controltower")))] +pub mod controltower; + +/// AWS Lambda event definitions for dynamodb. +#[cfg(feature = "dynamodb")] +#[cfg_attr(docsrs, doc(cfg(feature = "dynamodb")))] +pub mod dynamodb; + +/// AWS Lambda event definitions for ecr_scan. +#[cfg(feature = "ecr_scan")] +#[cfg_attr(docsrs, doc(cfg(feature = "ecr_scan")))] +pub mod ecr_scan; + +/// AWS Lambda event definitions for firehose. +#[cfg(feature = "firehose")] +#[cfg_attr(docsrs, doc(cfg(feature = "firehose")))] +pub mod firehose; + +/// AWS Lambda event definitions for iam. +#[cfg(feature = "iam")] +#[cfg_attr(docsrs, doc(cfg(feature = "iam")))] +pub mod iam; + +/// AWS Lambda event definitions for iot. +#[cfg(feature = "iot")] +#[cfg_attr(docsrs, doc(cfg(feature = "iot")))] +pub mod iot; + +/// AWS Lambda event definitions for iot_1_click. +#[cfg(feature = "iot_1_click")] +#[cfg_attr(docsrs, doc(cfg(feature = "iot_1_click")))] +pub mod iot_1_click; + +/// AWS Lambda event definitions for iot_button. +#[cfg(feature = "iot_button")] +#[cfg_attr(docsrs, doc(cfg(feature = "iot_button")))] +pub mod iot_button; + +/// AWS Lambda event definitions for iot_deprecated. +#[cfg(feature = "iot_deprecated")] +#[cfg_attr(docsrs, doc(cfg(feature = "iot_deprecated")))] +pub mod iot_deprecated; + +/// AWS Lambda event definitions for kafka. +#[cfg(feature = "kafka")] +#[cfg_attr(docsrs, doc(cfg(feature = "kafka")))] +pub mod kafka; + +/// AWS Lambda event definitions for kinesis. +#[cfg(feature = "kinesis")] +#[cfg_attr(docsrs, doc(cfg(feature = "kinesis")))] +pub mod kinesis; + +/// AWS Lambda event definitions for lambda_function_urls. +#[cfg(feature = "lambda_function_urls")] +#[cfg_attr(docsrs, doc(cfg(feature = "lambda_function_urls")))] +pub mod lambda_function_urls; + +/// AWS Lambda event definitions for lex. +#[cfg(feature = "lex")] +#[cfg_attr(docsrs, doc(cfg(feature = "lex")))] +pub mod lex; + +/// AWS Lambda event definitions for rabbitmq. +#[cfg(feature = "rabbitmq")] +#[cfg_attr(docsrs, doc(cfg(feature = "rabbitmq")))] +pub mod rabbitmq; + +/// AWS Lambda event definitions for s3. +#[cfg(feature = "s3")] +#[cfg_attr(docsrs, doc(cfg(feature = "s3")))] +pub mod s3; + +/// AWS Lambda event definitions for secretsmanager. +#[cfg(feature = "secretsmanager")] +#[cfg_attr(docsrs, doc(cfg(feature = "secretsmanager")))] +pub mod secretsmanager; + +/// AWS Lambda event definitions for ses. +#[cfg(feature = "ses")] +#[cfg_attr(docsrs, doc(cfg(feature = "ses")))] +pub mod ses; + +/// AWS Lambda event definitions for SNS. +#[cfg(feature = "sns")] +#[cfg_attr(docsrs, doc(cfg(feature = "sns")))] +pub mod sns; + +/// AWS Lambda event definitions for SQS. +#[cfg(feature = "sqs")] +#[cfg_attr(docsrs, doc(cfg(feature = "sqs")))] +pub mod sqs; + +/// AWS Lambda event definitions for streams. +#[cfg(feature = "streams")] +#[cfg_attr(docsrs, doc(cfg(feature = "streams")))] +pub mod streams; + +// AWS Lambda event definitions for DocumentDB +#[cfg(feature = "documentdb")] +#[cfg_attr(docsrs, doc(cfg(feature = "documentdb")))] +pub mod documentdb; + +/// AWS Lambda event definitions for EventBridge. +#[cfg(feature = "eventbridge")] +#[cfg_attr(docsrs, doc(cfg(feature = "eventbridge")))] +pub mod eventbridge; + +/// AWS Lambda event definitions for VPC Lattice. +#[cfg(feature = "vpc_lattice")] +#[cfg_attr(docsrs, doc(cfg(feature = "vpc_lattice")))] +pub mod vpc_lattice; diff --git a/lambda-events/src/event/rabbitmq/mod.rs b/lambda-events/src/event/rabbitmq/mod.rs new file mode 100644 index 000000000..383f93477 --- /dev/null +++ b/lambda-events/src/event/rabbitmq/mod.rs @@ -0,0 +1,106 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; + +use crate::custom_serde::deserialize_nullish; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RabbitMqEvent { + #[serde(default)] + pub event_source: Option, + #[serde(default)] + pub event_source_arn: Option, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + #[serde(rename = "rmqMessagesByQueue")] + pub messages_by_queue: HashMap>, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RabbitMqMessage { + pub basic_properties: RabbitMqBasicProperties, + #[serde(default)] + pub data: Option, + pub redelivered: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RabbitMqBasicProperties +where + T1: DeserializeOwned, + T1: Serialize, +{ + #[serde(default)] + pub content_type: Option, + pub content_encoding: Option, + /// Application or header exchange table + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + #[serde(bound = "")] + pub headers: HashMap, + pub delivery_mode: u8, + pub priority: u8, + pub correlation_id: Option, + pub reply_to: Option, + #[serde(default)] + pub expiration: Option, + pub message_id: Option, + #[serde(default)] + pub timestamp: Option, + pub type_: Option, + #[serde(default)] + pub user_id: Option, + pub app_id: Option, + pub cluster_id: Option, + pub body_size: u64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "rabbitmq")] + fn example_rabbitmq_event() { + let data = include_bytes!("../../fixtures/example-rabbitmq-event.json"); + let parsed: RabbitMqEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: RabbitMqEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/s3/batch_job.rs b/lambda-events/src/event/s3/batch_job.rs new file mode 100644 index 000000000..a6d3b12a6 --- /dev/null +++ b/lambda-events/src/event/s3/batch_job.rs @@ -0,0 +1,114 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +/// `S3BatchJobEvent` encapsulates the detail of a s3 batch job +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct S3BatchJobEvent { + #[serde(default)] + pub invocation_schema_version: Option, + #[serde(default)] + pub invocation_id: Option, + pub job: S3BatchJob, + pub tasks: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `S3BatchJob` whichs have the job id +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct S3BatchJob { + #[serde(default)] + pub id: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `S3BatchJobTask` represents one task in the s3 batch job and have all task details +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct S3BatchJobTask { + #[serde(default)] + pub task_id: Option, + #[serde(default)] + pub s3_key: Option, + #[serde(default)] + pub s3_version_id: Option, + #[serde(default)] + pub s3_bucket_arn: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `S3BatchJobResponse` is the response of a iven s3 batch job with the results +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct S3BatchJobResponse { + #[serde(default)] + pub invocation_schema_version: Option, + #[serde(default)] + pub treat_missing_keys_as: Option, + #[serde(default)] + pub invocation_id: Option, + pub results: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `S3BatchJobResult` represents the result of a given task +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct S3BatchJobResult { + #[serde(default)] + pub task_id: Option, + #[serde(default)] + pub result_code: Option, + #[serde(default)] + pub result_string: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} diff --git a/lambda-events/src/event/s3/event.rs b/lambda-events/src/event/s3/event.rs new file mode 100644 index 000000000..f2d69165a --- /dev/null +++ b/lambda-events/src/event/s3/event.rs @@ -0,0 +1,188 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +use std::collections::HashMap; + +use crate::custom_serde::deserialize_nullish; + +/// `S3Event` which wrap an array of `S3Event`Record +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct S3Event { + #[serde(rename = "Records")] + pub records: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `S3EventRecord` which wrap record data +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct S3EventRecord { + #[serde(default)] + pub event_version: Option, + #[serde(default)] + pub event_source: Option, + #[serde(default)] + pub aws_region: Option, + pub event_time: DateTime, + #[serde(default)] + pub event_name: Option, + #[serde(rename = "userIdentity")] + pub principal_id: S3UserIdentity, + pub request_parameters: S3RequestParameters, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub response_elements: HashMap, + pub s3: S3Entity, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct S3UserIdentity { + #[serde(default)] + pub principal_id: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct S3RequestParameters { + #[serde(default)] + #[serde(rename = "sourceIPAddress")] + pub source_ip_address: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct S3Entity { + #[serde(default)] + #[serde(rename = "s3SchemaVersion")] + pub schema_version: Option, + #[serde(default)] + pub configuration_id: Option, + pub bucket: S3Bucket, + pub object: S3Object, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct S3Bucket { + #[serde(default)] + pub name: Option, + #[serde(default)] + pub owner_identity: Option, + #[serde(default)] + pub arn: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct S3Object { + #[serde(default)] + pub key: Option, + pub size: Option, + #[serde(default)] + pub url_decoded_key: Option, + #[serde(default)] + pub version_id: Option, + #[serde(default)] + pub e_tag: Option, + #[serde(default)] + pub sequencer: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "s3")] + fn example_s3_event() { + let data = include_bytes!("../../fixtures/example-s3-event.json"); + let parsed: S3Event = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: S3Event = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "s3")] + fn example_s3_event_with_decoded() { + let data = include_bytes!("../../fixtures/example-s3-event-with-decoded.json"); + let parsed: S3Event = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: S3Event = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/s3/mod.rs b/lambda-events/src/event/s3/mod.rs new file mode 100644 index 000000000..b56645855 --- /dev/null +++ b/lambda-events/src/event/s3/mod.rs @@ -0,0 +1,5 @@ +mod event; +pub use self::event::*; + +pub mod batch_job; +pub mod object_lambda; diff --git a/lambda-events/src/event/s3/object_lambda.rs b/lambda-events/src/event/s3/object_lambda.rs new file mode 100644 index 000000000..8464fcae9 --- /dev/null +++ b/lambda-events/src/event/s3/object_lambda.rs @@ -0,0 +1,272 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use http::HeaderMap; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; + +use crate::custom_serde::{deserialize_headers, serialize_headers}; + +/// `S3ObjectLambdaEvent` contains data coming from S3 object lambdas +/// See: +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct S3ObjectLambdaEvent

+where + P: DeserializeOwned, + P: Serialize, +{ + pub x_amz_request_id: String, + pub get_object_context: Option, + pub head_object_context: Option, + pub list_objects_context: Option, + pub list_objects_v2_context: Option, + #[serde(default, bound = "")] + pub configuration: Configuration

, + pub user_request: UserRequest, + pub user_identity: UserIdentity, + pub protocol_version: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `GetObjectContext` contains the input and output details +/// for connections to Amazon S3 and S3 Object Lambda +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetObjectContext { + pub input_s3_url: String, + pub output_route: String, + pub output_token: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `HeadObjectContext` +/// for connections to Amazon S3 and S3 Object Lambda +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct HeadObjectContext { + pub input_s3_url: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ListObjectsContext` +/// for connections to Amazon S3 and S3 Object Lambda +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ListObjectsContext { + pub input_s3_url: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `ListObjectsV2Context` +/// for connections to Amazon S3 and S3 Object Lambda +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ListObjectsV2Context { + pub input_s3_url: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `Configuration` contains information about the Object Lambda access point +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Configuration

+where + P: DeserializeOwned, + P: Serialize, +{ + pub access_point_arn: String, + pub supporting_access_point_arn: String, + #[serde(default, bound = "")] + pub payload: P, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `UserRequest` contains information about the original call to S3 Object Lambda +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UserRequest { + pub url: String, + #[serde(deserialize_with = "deserialize_headers", default)] + #[serde(serialize_with = "serialize_headers")] + pub headers: HeaderMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `UserIdentity` contains details about the identity that made the call to S3 Object Lambda +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UserIdentity { + pub r#type: String, + pub principal_id: String, + pub arn: String, + pub account_id: String, + pub access_key_id: String, + pub session_context: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionContext { + pub attributes: HashMap, + #[serde(default)] + pub session_issuer: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionIssuer { + pub r#type: String, + pub principal_id: String, + pub arn: String, + pub account_id: String, + pub user_name: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "s3")] + fn example_object_lambda_event_get_object_assumed_role() { + let data = include_bytes!("../../fixtures/example-s3-object-lambda-event-get-object-assumed-role.json"); + let parsed: S3ObjectLambdaEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: S3ObjectLambdaEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "s3")] + fn example_object_lambda_event_get_object_iam() { + let data = include_bytes!("../../fixtures/example-s3-object-lambda-event-get-object-iam.json"); + let parsed: S3ObjectLambdaEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: S3ObjectLambdaEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "s3")] + fn example_object_lambda_event_head_object_iam() { + let data = include_bytes!("../../fixtures/example-s3-object-lambda-event-head-object-iam.json"); + let parsed: S3ObjectLambdaEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: S3ObjectLambdaEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "s3")] + fn example_object_lambda_event_list_objects_iam() { + let data = include_bytes!("../../fixtures/example-s3-object-lambda-event-list-objects-iam.json"); + let parsed: S3ObjectLambdaEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: S3ObjectLambdaEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "s3")] + fn example_object_lambda_event_list_objects_v2_iam() { + let data = include_bytes!("../../fixtures/example-s3-object-lambda-event-list-objects-v2-iam.json"); + let parsed: S3ObjectLambdaEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: S3ObjectLambdaEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/secretsmanager/mod.rs b/lambda-events/src/event/secretsmanager/mod.rs new file mode 100644 index 000000000..0507467f8 --- /dev/null +++ b/lambda-events/src/event/secretsmanager/mod.rs @@ -0,0 +1,38 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct SecretsManagerSecretRotationEvent { + pub step: String, + pub secret_id: String, + pub client_request_token: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "secretsmanager")] + fn example_secretsmanager_secret_rotation_event() { + let data = include_bytes!("../../fixtures/example-secretsmanager-secret-rotation-event.json"); + let parsed: SecretsManagerSecretRotationEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: SecretsManagerSecretRotationEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/ses/mod.rs b/lambda-events/src/event/ses/mod.rs new file mode 100644 index 000000000..0b8bec55e --- /dev/null +++ b/lambda-events/src/event/ses/mod.rs @@ -0,0 +1,269 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +/// `SimpleEmailEvent` is the outer structure of an event sent via SES. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SimpleEmailEvent { + #[serde(rename = "Records")] + pub records: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SimpleEmailRecord { + #[serde(default)] + pub event_version: Option, + #[serde(default)] + pub event_source: Option, + pub ses: SimpleEmailService, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SimpleEmailService { + pub mail: SimpleEmailMessage, + pub receipt: SimpleEmailReceipt, + pub content: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SimpleEmailMessage { + pub common_headers: SimpleEmailCommonHeaders, + #[serde(default)] + pub source: Option, + pub timestamp: DateTime, + pub destination: Vec, + pub headers: Vec, + pub headers_truncated: bool, + #[serde(default)] + pub message_id: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SimpleEmailReceipt { + pub recipients: Vec, + pub timestamp: DateTime, + pub spam_verdict: SimpleEmailVerdict, + pub dkim_verdict: SimpleEmailVerdict, + pub dmarc_verdict: SimpleEmailVerdict, + #[serde(default)] + pub dmarc_policy: Option, + pub spf_verdict: SimpleEmailVerdict, + pub virus_verdict: SimpleEmailVerdict, + pub action: SimpleEmailReceiptAction, + pub processing_time_millis: i64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SimpleEmailHeader { + #[serde(default)] + pub name: Option, + #[serde(default)] + pub value: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SimpleEmailCommonHeaders { + pub from: Vec, + #[serde(default)] + pub to: Vec, + #[serde(default)] + pub return_path: Option, + #[serde(default)] + pub message_id: Option, + #[serde(default)] + pub date: Option, + #[serde(default)] + pub subject: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `SimpleEmailReceiptAction` is a logical union of fields present in all action +/// Types. For example, the FunctionARN and InvocationType fields are only +/// present for the Lambda Type, and the BucketName and ObjectKey fields are only +/// present for the S3 Type. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SimpleEmailReceiptAction { + #[serde(default)] + pub type_: Option, + pub topic_arn: Option, + pub bucket_name: Option, + pub object_key: Option, + pub smtp_reply_code: Option, + pub status_code: Option, + pub message: Option, + pub sender: Option, + pub invocation_type: Option, + pub function_arn: Option, + pub organization_arn: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SimpleEmailVerdict { + #[serde(default)] + pub status: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +pub type SimpleEmailDispositionValue = String; + +/// `SimpleEmailDisposition` disposition return for SES to control rule functions +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SimpleEmailDisposition { + pub disposition: SimpleEmailDispositionValue, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "ses")] + fn example_ses_lambda_event() { + let data = include_bytes!("../../fixtures/example-ses-lambda-event.json"); + let parsed: SimpleEmailEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: SimpleEmailEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "ses")] + fn example_ses_s3_event() { + let data = include_bytes!("../../fixtures/example-ses-s3-event.json"); + let parsed: SimpleEmailEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: SimpleEmailEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "ses")] + fn example_ses_sns_event() { + let data = include_bytes!("../../fixtures/example-ses-sns-event.json"); + let parsed: SimpleEmailEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: SimpleEmailEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "ses")] + fn example_ses_bcc_event() { + let data = include_bytes!("../../fixtures/example-ses-bcc-event.json"); + let parsed: SimpleEmailEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: SimpleEmailEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/sns/mod.rs b/lambda-events/src/event/sns/mod.rs new file mode 100644 index 000000000..5879bc873 --- /dev/null +++ b/lambda-events/src/event/sns/mod.rs @@ -0,0 +1,598 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use chrono::{DateTime, Utc}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +use std::collections::HashMap; + +use crate::custom_serde::deserialize_nullish; + +/// The `Event` notification event handled by Lambda +/// +/// [https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html](https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html) +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct SnsEvent { + pub records: Vec, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// SnsRecord stores information about each record of a SNS event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct SnsRecord { + /// A string containing the event source. + pub event_source: String, + + /// A string containing the event version. + pub event_version: String, + + /// A string containing the event subscription ARN. + pub event_subscription_arn: String, + + /// An SNS object representing the SNS message. + pub sns: SnsMessage, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// SnsMessage stores information about SNS **Notification** type messages only. +/// +/// **Important**: This struct is designed specifically for handling SNS Notification messages +/// (where `Type` field equals "Notification"). For handling SubscriptionConfirmation or +/// UnsubscribeConfirmation messages, use [`SnsSubscriptionMessage`] instead. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct SnsMessage { + /// The type of SNS message. For a lambda event, this should always be **Notification** + #[serde(rename = "Type")] + pub sns_message_type: String, + + /// A Universally Unique Identifier, unique for each message published. For a notification that Amazon SNS resends during a retry, the message ID of the original message is used. + pub message_id: String, + + /// The Amazon Resource Name (ARN) for the topic that this message was published to. + pub topic_arn: String, + + /// The Subject parameter specified when the notification was published to the topic. + /// + /// The SNS Developer Guide states: *This is an optional parameter. If no Subject was specified, then this name-value pair does not appear in this JSON document.* + /// + /// Preliminary tests show this appears in the lambda event JSON as `Subject: null`, marking as Option with need to test additional scenarios + #[serde(default)] + pub subject: Option, + + /// The time (UTC) when the notification was published. + pub timestamp: DateTime, + + /// Version of the Amazon SNS signature used. + pub signature_version: String, + + /// Base64-encoded SHA1withRSA signature of the Message, MessageId, Subject (if present), Type, Timestamp, and TopicArn values. + pub signature: String, + + /// The URL to the certificate that was used to sign the message. + #[serde(alias = "SigningCertURL")] + pub signing_cert_url: String, + + /// A URL that you can use to unsubscribe the endpoint from this topic. If you visit this URL, Amazon SNS unsubscribes the endpoint and stops sending notifications to this endpoint. + #[serde(alias = "UnsubscribeURL")] + pub unsubscribe_url: String, + + /// The Message value specified when the notification was published to the topic. + pub message: String, + + /// This is a HashMap of defined attributes for a message. Additional details can be found in the [SNS Developer Guide](https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html) + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub message_attributes: HashMap, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// SnsSubscriptionMessage stores information about SNS SubscriptionConfirmation and +/// UnsubscribeConfirmation type messages. +/// +/// Use this struct when handling messages where the `Type` field equals "SubscriptionConfirmation" +/// or "UnsubscribeConfirmation". For handling Notification messages, use [`SnsMessage`] instead. +/// +/// # Distinguishing SubscriptionConfirmation from UnsubscribeConfirmation +/// +/// Both message types use this same struct. You can distinguish them by: +/// - Checking the `sns_message_type` field ("SubscriptionConfirmation" or "UnsubscribeConfirmation") +/// - Checking `subscribe_url`: `Some(url)` for SubscriptionConfirmation, `None` for UnsubscribeConfirmation +/// +/// # Example +/// +/// ``` +/// use aws_lambda_events::event::sns::SnsSubscriptionMessage; +/// +/// fn handle_confirmation(msg: SnsSubscriptionMessage) { +/// if let Some(url) = &msg.subscribe_url { +/// // SubscriptionConfirmation - visit URL or use token to confirm +/// println!("Confirm subscription at: {}", url); +/// } else { +/// // UnsubscribeConfirmation +/// println!("Unsubscribe confirmed"); +/// } +/// } +/// ``` +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct SnsSubscriptionMessage { + /// The type of SNS message. Will be "SubscriptionConfirmation" or "UnsubscribeConfirmation". + #[serde(rename = "Type")] + pub sns_message_type: String, + + /// A Universally Unique Identifier, unique for each message published. + pub message_id: String, + + /// The Amazon Resource Name (ARN) for the topic that this message was published to. + pub topic_arn: String, + + /// The Subject parameter specified when the notification was published to the topic. + #[serde(default)] + pub subject: Option, + + /// The time (UTC) when the message was sent. + pub timestamp: DateTime, + + /// Version of the Amazon SNS signature used. + pub signature_version: String, + + /// Base64-encoded SHA1withRSA signature of the Message, MessageId, Subject (if present), Type, Timestamp, and TopicArn values. + pub signature: String, + + /// The URL to the certificate that was used to sign the message. + #[serde(alias = "SigningCertURL")] + pub signing_cert_url: String, + + /// A URL that you can visit to confirm the subscription. Present only for SubscriptionConfirmation messages. + /// + /// For UnsubscribeConfirmation messages, this field will be `None`. + #[serde(alias = "SubscribeURL")] + #[serde(default)] + pub subscribe_url: Option, + + /// A value you can use with the ConfirmSubscription action to confirm the subscription. + /// Alternatively, you can simply visit the `subscribe_url`. + #[serde(rename = "Token")] + pub token: String, + + /// The Message value containing a description of the subscription confirmation. + pub message: String, + + /// This is a HashMap of defined attributes for a message. Additional details can be found in the [SNS Developer Guide](https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html) + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub message_attributes: HashMap, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// An alternate `Event` notification event to use alongside `SnsRecordObj` and `SnsMessageObj` if you want to deserialize an object inside your SNS messages rather than getting an `Option` message +/// +/// [https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html](https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html) +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +#[serde(bound(deserialize = "T: DeserializeOwned"))] +pub struct SnsEventObj { + pub records: Vec>, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// Alternative to `SnsRecord`, used alongside `SnsEventObj` and `SnsMessageObj` when deserializing nested objects from within SNS messages) +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +#[serde(bound(deserialize = "T: DeserializeOwned"))] +pub struct SnsRecordObj { + /// A string containing the event source. + pub event_source: String, + + /// A string containing the event version. + pub event_version: String, + + /// A string containing the event subscription ARN. + pub event_subscription_arn: String, + + /// An SNS object representing the SNS message. + pub sns: SnsMessageObj, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// Alternate version of `SnsMessage` to use in conjunction with `SnsEventObj` and `SnsRecordObj` for deserializing the message into a struct of type `T`. +/// +/// **Important**: This struct is designed specifically for handling SNS Notification messages +/// (where `Type` field equals "Notification"). For handling SubscriptionConfirmation or +/// UnsubscribeConfirmation messages, use [`SnsSubscriptionMessage`] instead. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[serde_with::serde_as] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +#[serde(bound(deserialize = "T: DeserializeOwned"))] +pub struct SnsMessageObj { + /// The type of SNS message. For a lambda event, this should always be **Notification** + #[serde(rename = "Type")] + pub sns_message_type: String, + + /// A Universally Unique Identifier, unique for each message published. For a notification that Amazon SNS resends during a retry, the message ID of the original message is used. + pub message_id: String, + + /// The Amazon Resource Name (ARN) for the topic that this message was published to. + pub topic_arn: String, + + /// The Subject parameter specified when the notification was published to the topic. + /// + /// The SNS Developer Guide states: *This is an optional parameter. If no Subject was specified, then this name-value pair does not appear in this JSON document.* + /// + /// Preliminary tests show this appears in the lambda event JSON as `Subject: null`, marking as Option with need to test additional scenarios + #[serde(default)] + pub subject: Option, + + /// The time (UTC) when the notification was published. + pub timestamp: DateTime, + + /// Version of the Amazon SNS signature used. + pub signature_version: String, + + /// Base64-encoded SHA1withRSA signature of the Message, MessageId, Subject (if present), Type, Timestamp, and TopicArn values. + pub signature: String, + + /// The URL to the certificate that was used to sign the message. + #[serde(alias = "SigningCertURL")] + pub signing_cert_url: String, + + /// A URL that you can use to unsubscribe the endpoint from this topic. If you visit this URL, Amazon SNS unsubscribes the endpoint and stops sending notifications to this endpoint. + #[serde(alias = "UnsubscribeURL")] + pub unsubscribe_url: String, + + /// Deserialized into a `T` from nested JSON inside the SNS message string. `T` must implement the `Deserialize` or `DeserializeOwned` trait. + #[serde_as(as = "serde_with::json::JsonString")] + #[serde(bound(deserialize = "T: DeserializeOwned"))] + pub message: T, + + /// This is a HashMap of defined attributes for a message. Additional details can be found in the [SNS Developer Guide](https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html) + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub message_attributes: HashMap, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// Structured metadata items (such as timestamps, geospatial data, signatures, and identifiers) about the message. +/// +/// Message attributes are optional and separate from—but are sent together with—the message body. The receiver can use this information to decide how to handle the message without having to process the message body first. +/// +/// Additional details can be found in the [SNS Developer Guide](https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html) +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct MessageAttribute { + /// The data type of the attribute. Per the [SNS Developer Guide](https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html), lambda notifications, this will only be **String** or **Binary**. + #[serde(rename = "Type")] + pub data_type: String, + + /// The user-specified message attribute value. + #[serde(rename = "Value")] + pub value: String, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct CloudWatchAlarmPayload { + pub alarm_name: String, + pub alarm_description: String, + #[serde(rename = "AWSAccountId")] + pub aws_account_id: String, + pub new_state_value: String, + pub new_state_reason: String, + pub state_change_time: String, + pub region: String, + pub alarm_arn: String, + pub old_state_value: String, + pub trigger: CloudWatchAlarmTrigger, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct CloudWatchAlarmTrigger { + pub period: i64, + pub evaluation_periods: i64, + pub comparison_operator: String, + pub threshold: f64, + pub treat_missing_data: String, + pub evaluate_low_sample_count_percentile: String, + #[serde(default)] + pub metrics: Vec, + pub metric_name: Option, + pub namespace: Option, + pub statistic_type: Option, + pub statistic: Option, + pub unit: Option, + #[serde(default)] + pub dimensions: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct CloudWatchMetricDataQuery { + pub id: String, + pub expression: Option, + pub label: Option, + pub metric_stat: Option, + pub period: Option, + #[serde(default, deserialize_with = "deserialize_nullish")] + pub return_data: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct CloudWatchMetricStat { + pub metric: CloudWatchMetric, + pub period: i64, + pub stat: String, + pub unit: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct CloudWatchMetric { + #[serde(default)] + pub dimensions: Vec, + pub metric_name: Option, + pub namespace: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct CloudWatchDimension { + pub name: String, + pub value: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "sns")] + fn my_example_sns_event() { + let data = include_bytes!("../../fixtures/example-sns-event.json"); + let parsed: SnsEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: SnsEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "sns")] + fn my_example_sns_event_pascal_case() { + let data = include_bytes!("../../fixtures/example-sns-event-pascal-case.json"); + let parsed: SnsEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: SnsEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "sns")] + fn my_example_sns_event_cloudwatch_single_metric() { + let data = include_bytes!("../../fixtures/example-cloudwatch-alarm-sns-payload-single-metric.json"); + let parsed: SnsEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(1, parsed.records.len()); + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: SnsEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + + let parsed: SnsEventObj = + serde_json::from_slice(data).expect("failed to parse CloudWatch Alarm payload"); + + let record = parsed.records.first().unwrap(); + assert_eq!("EXAMPLE", record.sns.message.alarm_name); + } + + #[test] + #[cfg(feature = "sns")] + fn my_example_sns_event_cloudwatch_multiple_metrics() { + let data = include_bytes!("../../fixtures/example-cloudwatch-alarm-sns-payload-multiple-metrics.json"); + let parsed: SnsEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(2, parsed.records.len()); + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: SnsEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "sns")] + fn my_example_sns_obj_event() { + let data = include_bytes!("../../fixtures/example-sns-event-obj.json"); + + #[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] + struct CustStruct { + foo: String, + bar: i32, + } + + let parsed: SnsEventObj = serde_json::from_slice(data).unwrap(); + println!("{parsed:?}"); + + assert_eq!(parsed.records[0].sns.message.foo, "Hello world!"); + assert_eq!(parsed.records[0].sns.message.bar, 123); + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: SnsEventObj = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "sns")] + fn my_example_sns_subscription_confirmation() { + // Test for issue #966: SnsSubscriptionMessage for SubscriptionConfirmation types + let data = include_bytes!("../../fixtures/example-sns-subscription-confirmation.json"); + let parsed: SnsSubscriptionMessage = serde_json::from_slice(data).unwrap(); + + assert_eq!("SubscriptionConfirmation", parsed.sns_message_type); + assert!(parsed.subscribe_url.is_some()); + assert_eq!( + "https://sns.us-east-1.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-east-1:123456789012:MyTopic&Token=2336412f37fb687f5d51e6e2425dacbbffff", + parsed.subscribe_url.as_ref().unwrap() + ); + assert_eq!("2336412f37fb687f5d51e6e2425dacbbffff", parsed.token); + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: SnsSubscriptionMessage = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "sns")] + fn my_example_sns_unsubscribe_confirmation() { + // Test for UnsubscribeConfirmation messages - subscribe_url should be None + let data = include_bytes!("../../fixtures/example-sns-unsubscribe-confirmation.json"); + let parsed: SnsSubscriptionMessage = serde_json::from_slice(data).unwrap(); + + assert_eq!("UnsubscribeConfirmation", parsed.sns_message_type); + assert!(parsed.subscribe_url.is_none()); + assert_eq!("2336412f37fb687f5d51e6e2425dacbbeeee", parsed.token); + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: SnsSubscriptionMessage = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/sqs/mod.rs b/lambda-events/src/event/sqs/mod.rs new file mode 100644 index 000000000..3209e0f58 --- /dev/null +++ b/lambda-events/src/event/sqs/mod.rs @@ -0,0 +1,503 @@ +use crate::{custom_serde::deserialize_nullish, encodings::Base64Data}; +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +use std::collections::HashMap; + +/// The Event sent to Lambda from SQS. Contains 1 or more individual SQS Messages +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SqsEvent { + #[serde(rename = "Records")] + pub records: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[cfg_attr(feature = "builders", builder(default))] + #[serde(flatten)] + pub other: serde_json::Map, +} + +/// An individual SQS Message, its metadata, and Message Attributes +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SqsMessage { + /// nolint: stylecheck + #[serde(default)] + pub message_id: Option, + #[serde(default)] + pub receipt_handle: Option, + #[serde(default)] + pub body: Option, + #[serde(default)] + pub md5_of_body: Option, + #[serde(default)] + pub md5_of_message_attributes: Option, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub attributes: HashMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub message_attributes: HashMap, + #[serde(default)] + #[serde(rename = "eventSourceARN")] + pub event_source_arn: Option, + #[serde(default)] + pub event_source: Option, + #[serde(default)] + pub aws_region: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// Alternative to `SqsEvent` to be used alongside `SqsMessageObj` when you need to deserialize a nested object into a struct of type `T` within the SQS Message rather than just using the raw SQS Message string +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +#[serde(bound(deserialize = "T: DeserializeOwned"))] +pub struct SqsEventObj { + #[serde(rename = "Records")] + #[serde(bound(deserialize = "T: DeserializeOwned"))] + pub records: Vec>, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// Alternative to `SqsMessage` to be used alongside `SqsEventObj` when you need to deserialize a nested object into a struct of type `T` within the SQS Message rather than just using the raw SQS Message string +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[serde_with::serde_as] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(bound(deserialize = "T: DeserializeOwned"))] +#[serde(rename_all = "camelCase")] +pub struct SqsMessageObj { + /// nolint: stylecheck + #[serde(default)] + pub message_id: Option, + #[serde(default)] + pub receipt_handle: Option, + + /// Deserialized into a `T` from nested JSON inside the SQS body string. `T` must implement the `Deserialize` or `DeserializeOwned` trait. + #[serde_as(as = "serde_with::json::JsonString")] + #[serde(bound(deserialize = "T: DeserializeOwned"))] + pub body: T, + #[serde(default)] + pub md5_of_body: Option, + #[serde(default)] + pub md5_of_message_attributes: Option, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub attributes: HashMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub message_attributes: HashMap, + #[serde(default)] + #[serde(rename = "eventSourceARN")] + pub event_source_arn: Option, + #[serde(default)] + pub event_source: Option, + #[serde(default)] + pub aws_region: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SqsMessageAttribute { + pub string_value: Option, + pub binary_value: Option, + #[serde(default)] + pub string_list_values: Vec, + #[serde(default)] + pub binary_list_values: Vec, + #[serde(default)] + pub data_type: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SqsBatchResponse { + pub batch_item_failures: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +impl SqsBatchResponse { + /// Add a failed message ID to the batch response. + /// + /// When processing SQS messages in batches, you can use this helper method to + /// register individual message failures. Lambda will automatically return failed + /// messages to the queue for reprocessing while successfully processed messages + /// will be deleted. + /// + /// Besides `item_identifiers`, the generated struct will use default field values for [`BatchItemFailure`]. + /// + /// **Important**: This feature requires `FunctionResponseTypes: ReportBatchItemFailures` + /// to be enabled in your Lambda function's SQS event source mapping configuration. + /// Without this setting, Lambda will retry the entire batch on any failure. + /// + /// # Example + /// + /// ```rust + /// use aws_lambda_events::event::sqs::{SqsEvent, SqsBatchResponse}; + /// use lambda_runtime::{service_fn, Error, LambdaEvent}; + /// + /// async fn function_handler( + /// event: LambdaEvent, + /// ) -> Result { + /// // Start from a default response + /// let mut response = SqsBatchResponse::default(); + /// + /// for record in event.payload.records { + /// let message_id = record.message_id.clone().unwrap_or_default(); + /// + /// // Try to process the message + /// if let Err(e) = process_record(&record).await { + /// println!("Failed to process message {}: {}", message_id, e); + /// + /// // Use the helper to register the failure + /// response.add_failure(message_id); + /// } + /// } + /// + /// Ok(response) + /// } + /// + /// async fn process_record(record: &aws_lambda_events::event::sqs::SqsMessage) -> Result<(), Error> { + /// // Your message processing logic here + /// Ok(()) + /// } + /// ``` + pub fn add_failure(&mut self, message_id: impl Into) { + self.batch_item_failures.push(BatchItemFailure { + item_identifier: message_id.into(), + ..Default::default() + }); + } + + /// Set multiple failed message IDs at once. + /// + /// This is a convenience method for setting all batch item failures in one call. + /// It replaces any previously registered failures. + /// + /// Besides `item_identifiers`, the generated struct will use default field values for [`BatchItemFailure`]. + /// + /// **Important**: This feature requires `FunctionResponseTypes: ReportBatchItemFailures` + /// to be enabled in your Lambda function's SQS event source mapping configuration. + /// Without this setting, Lambda will retry the entire batch on any failure. + /// + /// # Example + /// + /// ```rust + /// use aws_lambda_events::event::sqs::{SqsEvent, SqsBatchResponse}; + /// use lambda_runtime::{service_fn, Error, LambdaEvent}; + /// + /// async fn function_handler( + /// event: LambdaEvent, + /// ) -> Result { + /// let mut failed_ids = Vec::new(); + /// + /// for record in event.payload.records { + /// let message_id = record.message_id.clone().unwrap_or_default(); + /// + /// // Try to process the message + /// if let Err(e) = process_record(&record).await { + /// println!("Failed to process message {}: {}", message_id, e); + /// failed_ids.push(message_id); + /// } + /// } + /// + /// // Set all failures at once + /// let mut response = SqsBatchResponse::default(); + /// response.set_failures(failed_ids); + /// + /// Ok(response) + /// } + /// + /// async fn process_record(record: &aws_lambda_events::event::sqs::SqsMessage) -> Result<(), Error> { + /// // Your message processing logic here + /// Ok(()) + /// } + /// ``` + pub fn set_failures(&mut self, message_ids: I) + where + I: IntoIterator, + S: Into, + { + self.batch_item_failures = message_ids + .into_iter() + .map(|id| BatchItemFailure { + item_identifier: id.into(), + ..Default::default() + }) + .collect(); + } +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BatchItemFailure { + pub item_identifier: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// The Event sent to Lambda from the SQS API. Contains 1 or more individual SQS Messages +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +#[serde(bound(deserialize = "T: DeserializeOwned"))] +pub struct SqsApiEventObj { + #[serde(bound(deserialize = "T: DeserializeOwned"))] + pub messages: Vec>, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// The Event sent to Lambda from SQS API. Contains 1 or more individual SQS Messages +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SqsApiEvent { + pub messages: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// Alternative to SqsApiEvent to be used alongside `SqsApiMessageObj` when you need to +/// deserialize a nested object into a struct of type T within the SQS Message rather +/// than just using the raw SQS Message string +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[serde_with::serde_as] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(bound(deserialize = "T: DeserializeOwned"))] +#[serde(rename_all = "PascalCase")] +pub struct SqsApiMessageObj { + /// nolint: stylecheck + #[serde(default)] + pub message_id: Option, + #[serde(default)] + pub receipt_handle: Option, + /// Deserialized into a `T` from nested JSON inside the SQS body string. `T` must implement the `Deserialize` or `DeserializeOwned` trait. + #[serde_as(as = "serde_with::json::JsonString")] + #[serde(bound(deserialize = "T: DeserializeOwned"))] + pub body: T, + #[serde(default)] + pub md5_of_body: Option, + #[serde(default)] + pub md5_of_message_attributes: Option, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub attributes: HashMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub message_attributes: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// An individual SQS API Message, its metadata, and Message Attributes +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct SqsApiMessage { + /// nolint: stylecheck + #[serde(default)] + pub message_id: Option, + #[serde(default)] + pub receipt_handle: Option, + #[serde(default)] + pub body: Option, + #[serde(default)] + pub md5_of_body: Option, + #[serde(default)] + pub md5_of_message_attributes: Option, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub attributes: HashMap, + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub message_attributes: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "sqs")] + fn example_sqs_event() { + let data = include_bytes!("../../fixtures/example-sqs-event.json"); + let parsed: SqsEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: SqsEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "sqs")] + fn example_sqs_obj_event() { + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] + struct CustStruct { + a: String, + b: u32, + } + + let data = include_bytes!("../../fixtures/example-sqs-event-obj.json"); + let parsed: SqsEventObj = serde_json::from_slice(data).unwrap(); + + assert_eq!(parsed.records[0].body.a, "Test"); + assert_eq!(parsed.records[0].body.b, 123); + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: SqsEventObj = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "sqs")] + fn example_sqs_batch_response() { + // Example sqs batch response fetched 2022-05-13, from: + // https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#services-sqs-batchfailurereporting + let data = include_bytes!("../../fixtures/example-sqs-batch-response.json"); + let parsed: SqsBatchResponse = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: SqsBatchResponse = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "sqs")] + fn example_sqs_api_obj_event() { + // Example sqs api receive message response, fetched 2023-10-23, inspired from: + // https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_ReceiveMessage.html#API_ReceiveMessage_ResponseSyntax + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] + struct CustStruct { + city: String, + country: String, + } + + let data = include_bytes!("../../fixtures/example-sqs-api-event-obj.json"); + let parsed: SqsApiEventObj = serde_json::from_slice(data).unwrap(); + + assert_eq!(parsed.messages[0].body.city, "provincetown"); + assert_eq!(parsed.messages[0].body.country, "usa"); + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: SqsApiEventObj = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "sqs")] + fn example_sqs_batch_response_add_failure() { + let mut response = SqsBatchResponse::default(); + response.add_failure("msg-1".to_string()); + response.add_failure("msg-2".to_string()); + + assert_eq!(response.batch_item_failures.len(), 2); + assert_eq!(response.batch_item_failures[0].item_identifier, "msg-1"); + assert_eq!(response.batch_item_failures[1].item_identifier, "msg-2"); + } + + #[test] + #[cfg(feature = "sqs")] + fn example_sqs_batch_response_set_failures() { + let mut response = SqsBatchResponse::default(); + response.set_failures(vec!["msg-1", "msg-2", "msg-3"]); + + assert_eq!(response.batch_item_failures.len(), 3); + assert_eq!(response.batch_item_failures[0].item_identifier, "msg-1"); + assert_eq!(response.batch_item_failures[1].item_identifier, "msg-2"); + assert_eq!(response.batch_item_failures[2].item_identifier, "msg-3"); + + // Test that set_failures replaces existing failures + response.set_failures(vec!["msg-4".to_string()]); + assert_eq!(response.batch_item_failures.len(), 1); + assert_eq!(response.batch_item_failures[0].item_identifier, "msg-4"); + } +} diff --git a/lambda-events/src/event/streams/mod.rs b/lambda-events/src/event/streams/mod.rs new file mode 100644 index 000000000..b6be8b1df --- /dev/null +++ b/lambda-events/src/event/streams/mod.rs @@ -0,0 +1,208 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +/// `KinesisEventResponse` is the outer structure to report batch item failures for KinesisEvent. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KinesisEventResponse { + pub batch_item_failures: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +impl KinesisEventResponse { + /// Add a failed item identifier to the batch response. + /// + /// When processing Kinesis records in batches, you can use this helper method to + /// register individual record failures. Lambda will automatically retry failed + /// records while successfully processed records will be checkpointed. + /// + /// Besides `item_identifiers`, the generated struct will use default field values for [`KinesisBatchItemFailure`]. + /// + /// **Important**: This feature requires `FunctionResponseTypes: ReportBatchItemFailures` + /// to be enabled in your Lambda function's Kinesis event source mapping configuration. + /// Without this setting, Lambda will retry the entire batch on any failure. + pub fn add_failure(&mut self, item_identifier: impl Into) { + self.batch_item_failures.push(KinesisBatchItemFailure { + item_identifier: Some(item_identifier.into()), + #[cfg(feature = "catch-all-fields")] + other: serde_json::Map::new(), + ..Default::default() + }); + } + + /// Set multiple failed item identifiers at once. + /// + /// This is a convenience method for setting all batch item failures in one call. + /// It replaces any previously registered failures. + /// + /// Besides `item_identifiers`, the generated struct will use default field values for [`KinesisBatchItemFailure`]. + /// + /// **Important**: This feature requires `FunctionResponseTypes: ReportBatchItemFailures` + /// to be enabled in your Lambda function's Kinesis event source mapping configuration. + /// Without this setting, Lambda will retry the entire batch on any failure. + pub fn set_failures(&mut self, item_identifiers: I) + where + I: IntoIterator, + S: Into, + { + self.batch_item_failures = item_identifiers + .into_iter() + .map(|id| KinesisBatchItemFailure { + item_identifier: Some(id.into()), + #[cfg(feature = "catch-all-fields")] + other: serde_json::Map::new(), + ..Default::default() + }) + .collect(); + } +} + +/// `KinesisBatchItemFailure` is the individual record which failed processing. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KinesisBatchItemFailure { + #[serde(default)] + pub item_identifier: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `DynamoDbEventResponse` is the outer structure to report batch item failures for DynamoDBEvent. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DynamoDbEventResponse { + pub batch_item_failures: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `DynamoDbBatchItemFailure` is the individual record which failed processing. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DynamoDbBatchItemFailure { + #[serde(default)] + pub item_identifier: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `SqsEventResponse` is the outer structure to report batch item failures for SQSEvent. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SqsEventResponse { + pub batch_item_failures: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `SqsBatchItemFailure` is the individual record which failed processing. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SqsBatchItemFailure { + #[serde(default)] + pub item_identifier: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn kinesis_event_response_add_failure() { + let mut response = KinesisEventResponse::default(); + response.add_failure("seq-1"); + response.add_failure("seq-2".to_string()); + + assert_eq!(response.batch_item_failures.len(), 2); + assert_eq!( + response.batch_item_failures[0].item_identifier, + Some("seq-1".to_string()) + ); + assert_eq!( + response.batch_item_failures[1].item_identifier, + Some("seq-2".to_string()) + ); + } + + #[test] + fn kinesis_event_response_set_failures() { + let mut response = KinesisEventResponse::default(); + response.set_failures(vec!["seq-1", "seq-2", "seq-3"]); + + assert_eq!(response.batch_item_failures.len(), 3); + assert_eq!( + response.batch_item_failures[0].item_identifier, + Some("seq-1".to_string()) + ); + assert_eq!( + response.batch_item_failures[1].item_identifier, + Some("seq-2".to_string()) + ); + assert_eq!( + response.batch_item_failures[2].item_identifier, + Some("seq-3".to_string()) + ); + + // Test that set_failures replaces existing failures + response.set_failures(vec!["seq-4".to_string()]); + assert_eq!(response.batch_item_failures.len(), 1); + assert_eq!( + response.batch_item_failures[0].item_identifier, + Some("seq-4".to_string()) + ); + } +} diff --git a/lambda-events/src/event/vpc_lattice/common.rs b/lambda-events/src/event/vpc_lattice/common.rs new file mode 100644 index 000000000..bdb95aba1 --- /dev/null +++ b/lambda-events/src/event/vpc_lattice/common.rs @@ -0,0 +1,62 @@ +use crate::{ + custom_serde::{deserialize_headers, serialize_headers}, + encodings::Body, +}; +use http::HeaderMap; + +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +/// `VpcLatticeResponse` configures the response to be returned +/// by VPC Lattice (both V1 and V2) for the request +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VpcLatticeResponse { + // https://docs.aws.amazon.com/vpc-lattice/latest/ug/lambda-functions.html#respond-to-service + /// Whether the body is base64 encoded + #[serde(default)] + pub is_base64_encoded: bool, + + /// The HTTP status code for the request + pub status_code: u16, + + /// The Http headers to return + #[serde(deserialize_with = "deserialize_headers")] + #[serde(serialize_with = "serialize_headers")] + #[serde(skip_serializing_if = "HeaderMap::is_empty")] + #[serde(default)] + pub headers: HeaderMap, + + /// The response body + #[serde(default)] + pub body: Option, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_response() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-response.json"); + let parsed: VpcLatticeResponse = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: VpcLatticeResponse = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/vpc_lattice/mod.rs b/lambda-events/src/event/vpc_lattice/mod.rs new file mode 100644 index 000000000..cfefcb75d --- /dev/null +++ b/lambda-events/src/event/vpc_lattice/mod.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; + +mod common; +mod v1; +mod v2; + +// re-export types +pub use self::{common::*, v1::*, v2::*}; + +/// A VPC Lattice request that can be either V1 or V2 format. +/// Deserialization tries V2 first (which has a `version` and `requestContext` field), +/// then falls back to V1. +#[non_exhaustive] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +#[allow(clippy::large_enum_variant)] +pub enum VpcLatticeRequest { + V2(VpcLatticeRequestV2), + V1(VpcLatticeRequestV1), +} diff --git a/lambda-events/src/event/vpc_lattice/v1.rs b/lambda-events/src/event/vpc_lattice/v1.rs new file mode 100644 index 000000000..4ab423ffb --- /dev/null +++ b/lambda-events/src/event/vpc_lattice/v1.rs @@ -0,0 +1,106 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use http::{HeaderMap, Method}; +use query_map::QueryMap; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +use crate::{ + custom_serde::{ + deserialize_comma_separated_headers, deserialize_nullish, http_method, serialize_comma_separated_headers, + }, + encodings::Body, +}; + +/// `VpcLatticeRequestV1` contains data coming from VPC Lattice service (V1 format) +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +// we note that V1 requests are snake cased UNLIKE v2 which are camel cased +#[serde(rename_all = "snake_case")] +pub struct VpcLatticeRequestV1 { + /// The url path for the request. + /// Present only if the protocol is HTTP, HTTPS, or gRPC. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub raw_path: Option, + + /// The HTTP method of the request. + /// Present only if the protocol is HTTP, HTTPS, or gRPC. + #[serde(deserialize_with = "http_method::deserialize_optional")] + #[serde(serialize_with = "http_method::serialize_optional")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub method: Option, + + /// HTTP headers of the request (V1 uses comma-separated strings for multi-values) + #[serde(deserialize_with = "deserialize_comma_separated_headers", default)] + #[serde(serialize_with = "serialize_comma_separated_headers")] + pub headers: HeaderMap, + + /// HTTP query string parameters (V1 uses the last value passed for multi-values + /// so no special serializer is needed) + #[serde(default)] + pub query_string_parameters: QueryMap, + + /// The request body + #[serde(default)] + pub body: Option, + + /// Whether the body is base64 encoded + #[serde(default, deserialize_with = "deserialize_nullish")] + pub is_base64_encoded: bool, + + /// Catchall to catch any additional fields + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v1_deserialize() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v1-request.json"); + let parsed: VpcLatticeRequestV1 = serde_json::from_slice(data).unwrap(); + + assert_eq!(Some("/api/product".to_string()), parsed.raw_path); + assert_eq!(Some(Method::POST), parsed.method); + assert_eq!( + "curl/7.68.0", + parsed.headers.get_all("user-agent").iter().next().unwrap() + ); + assert_eq!("electronics", parsed.query_string_parameters.first("category").unwrap()); + assert_eq!( + Body::Text("{\"id\": 5, \"description\": \"TV\"}".to_string()), + parsed.body.unwrap() + ); + assert!(!parsed.is_base64_encoded); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v1_deserialize_headers_multi_values() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v1-request.json"); + let parsed: VpcLatticeRequestV1 = serde_json::from_slice(data).unwrap(); + + assert_eq!("abcd", parsed.headers.get_all("multi").iter().next().unwrap()); + assert_eq!("DEF", parsed.headers.get_all("multi").iter().nth(1).unwrap()); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v1_deserialize_query_string_map() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v1-request.json"); + let parsed: VpcLatticeRequestV1 = serde_json::from_slice(data).unwrap(); + + assert_eq!("electronics", parsed.query_string_parameters.first("category").unwrap()); + assert_eq!("tv", parsed.query_string_parameters.first("tags").unwrap()); + } +} diff --git a/lambda-events/src/event/vpc_lattice/v2.rs b/lambda-events/src/event/vpc_lattice/v2.rs new file mode 100644 index 000000000..1865a8644 --- /dev/null +++ b/lambda-events/src/event/vpc_lattice/v2.rs @@ -0,0 +1,251 @@ +use crate::custom_serde::{deserialize_headers, deserialize_nullish, http_method, serialize_multi_value_headers}; +#[cfg(feature = "builders")] +use bon::Builder; +use http::{HeaderMap, Method}; +use query_map::QueryMap; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +/// `VpcLatticeRequestV2` contains data coming from VPC Lattice service (V2 format) +/// see: +/// for field definitions. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VpcLatticeRequestV2 { + /// The version of the event structure (always "2.0" for V2) + #[serde(default = "default_version")] + pub version: String, + + /// The url path for the request. + /// Present only if the protocol is HTTP, HTTPS, or gRPC. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, + + /// The HTTP method of the request. + /// Present only if the protocol is HTTP, HTTPS, or gRPC. + #[serde(deserialize_with = "http_method::deserialize_optional")] + #[serde(serialize_with = "http_method::serialize_optional")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub method: Option, + + /// HTTP headers of the request (VPC Lattice V2 uses arrays for multi-values) + #[serde(default, deserialize_with = "deserialize_headers")] + #[serde(serialize_with = "serialize_multi_value_headers")] + pub headers: HeaderMap, + + /// HTTP query string parameters (VPC Lattice V2 uses arrays for multi-values) + #[serde(default)] + pub query_string_parameters: QueryMap, + + /// The request body + #[serde(default)] + pub body: Option, + + /// Whether the body is base64 encoded + #[serde(default, deserialize_with = "deserialize_nullish")] + pub is_base64_encoded: bool, + + /// VPC Lattice specific request context + pub request_context: VpcLatticeRequestV2Context, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// VPC Lattice specific request context +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VpcLatticeRequestV2Context { + /// ARN of the service network that delivers the request + pub service_network_arn: String, + + /// ARN of the service that receives the request + pub service_arn: String, + + /// ARN of the target group that receives the request + pub target_group_arn: String, + + /// Identity information for the request. + /// Present only if authentication is configured. + #[serde(default)] + pub identity: Option, + + /// AWS region where the request is processed + pub region: String, + + /// Time of the request in microseconds since epoch + pub time_epoch: String, + + /// Catchall for additional context fields + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// Identity information in VPC Lattice request context +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VpcLatticeRequestV2Identity { + /// ARN of the VPC where the request originated + #[serde(default)] + pub source_vpc_arn: Option, + + /// Type of authentication (e.g., "AWS_IAM") + #[serde(rename = "type")] + #[serde(default)] + pub identity_type: Option, + + /// The authenticated principal + #[serde(default)] + pub principal: Option, + + /// Organization ID of the authenticated principal + #[serde(rename = "principalOrgID")] + #[serde(default)] + pub principal_org_id: Option, + + /// Name of the authenticated session + #[serde(default)] + pub session_name: Option, + + /// X.509 certificate fields (for Roles Anywhere) + #[serde(rename = "x509IssuerOu")] + #[serde(default)] + pub x509_issuer_ou: Option, + #[serde(rename = "x509SanDns")] + #[serde(default)] + pub x509_san_dns: Option, + #[serde(rename = "x509SanNameCn")] + #[serde(default)] + pub x509_san_name_cn: Option, + #[serde(rename = "x509SanUri")] + #[serde(default)] + pub x509_san_uri: Option, + #[serde(rename = "x509SubjectCn")] + #[serde(default)] + pub x509_subject_cn: Option, + + /// Catchall for additional identity fields + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +fn default_version() -> String { + "2.0".to_string() +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v2_deserialize() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v2-request.json"); + let parsed: VpcLatticeRequestV2 = serde_json::from_slice(data).unwrap(); + + assert_eq!(Some("/health".to_string()), parsed.path); + assert_eq!(Some(Method::GET), parsed.method); + assert_eq!( + "curl/7.68.0", + parsed.headers.get_all("user-agent").iter().next().unwrap() + ); + + // headers including testing multi-values + let header_multi = parsed.headers.get_all("multi"); + assert_eq!("x", header_multi.iter().next().unwrap()); + assert_eq!("y", header_multi.iter().nth(1).unwrap()); + + // query string including testing multi-values + assert_eq!("prod", parsed.query_string_parameters.first("state").unwrap()); + let query_multi = parsed.query_string_parameters.all("multi").unwrap(); + assert_eq!(&"a", query_multi.first().unwrap()); + assert_eq!(&"DEF", query_multi.get(1).unwrap()); + assert_eq!(&"g", query_multi.get(2).unwrap()); + + assert!(parsed.body.is_none()); + assert!(!parsed.is_base64_encoded); + + // nested structure testing + assert_eq!( + "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", + parsed.request_context.service_arn + ); + assert_eq!( + "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + parsed.request_context.service_network_arn + ); + assert_eq!( + "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + parsed.request_context.target_group_arn + ); + assert_eq!("ap-southeast-2", parsed.request_context.region); + assert_eq!("1724875399456789", parsed.request_context.time_epoch); + + let context = parsed.request_context.identity.as_ref().unwrap(); + + // identity + assert_eq!( + "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", + context.clone().source_vpc_arn.unwrap() + ); + assert_eq!("AWS_IAM", context.clone().identity_type.unwrap()); + assert_eq!( + "arn:aws:iam::123456789012:role/service-role/HealthChecker", + context.clone().principal.unwrap() + ); + assert_eq!("o-50dc6c495c0c9188", context.clone().principal_org_id.unwrap()); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v2_serde_round_trip() { + // our basic example has instances of multi-value headers and multi-value parameters + // so this test covers both those serialization edge cases + let data = include_bytes!("../../fixtures/example-vpc-lattice-v2-request.json"); + let parsed: VpcLatticeRequestV2 = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: VpcLatticeRequestV2 = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v2_serde_round_trip_base64_body() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v2-request-base64.json"); + let parsed: VpcLatticeRequestV2 = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: VpcLatticeRequestV2 = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v2_serde_round_trip_role_anywhere() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v2-request-roles-anywhere.json"); + let parsed: VpcLatticeRequestV2 = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: VpcLatticeRequestV2 = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/fixtures/example-activemq-event-null-properties.json b/lambda-events/src/fixtures/example-activemq-event-null-properties.json new file mode 100644 index 000000000..2ac6fd543 --- /dev/null +++ b/lambda-events/src/fixtures/example-activemq-event-null-properties.json @@ -0,0 +1,25 @@ +{ + "eventSource": "aws:mq", + "eventSourceArn": "arn:aws:mq:us-west-2:533019413397:broker:shask-test:b-0f5b7522-2b41-4f85-a615-735a4e6d96b5", + "messages": [ + { + "messageID": "ID:b-0f5b7522-2b41-4f85-a615-735a4e6d96b5-2.mq.us-west-2.amazonaws.com-34859-1598944546501-4:12:1:1:3", + "messageType": "jms/text-message", + "timestamp": 1599863938941, + "deliveryMode": 1, + "correlationID": "", + "replyTo": "null", + "destination": { + "physicalName": "testQueue" + }, + "redelivered": false, + "type": "", + "expiration": 0, + "priority": 0, + "data": "RW50ZXIgc29tZSB0ZXh0IGhlcmUgZm9yIHRoZSBtZXNzYWdlIGJvZHkuLi4=", + "brokerInTime": 1599863938943, + "brokerOutTime": 1599863938944, + "properties": null + } + ] +} diff --git a/lambda-events/src/fixtures/example-activemq-event.json b/lambda-events/src/fixtures/example-activemq-event.json new file mode 100644 index 000000000..48ae90cd7 --- /dev/null +++ b/lambda-events/src/fixtures/example-activemq-event.json @@ -0,0 +1,25 @@ +{ + "eventSource": "aws:mq", + "eventSourceArn": "arn:aws:mq:us-west-2:533019413397:broker:shask-test:b-0f5b7522-2b41-4f85-a615-735a4e6d96b5", + "messages": [ + { + "messageID": "ID:b-0f5b7522-2b41-4f85-a615-735a4e6d96b5-2.mq.us-west-2.amazonaws.com-34859-1598944546501-4:12:1:1:3", + "messageType": "jms/text-message", + "timestamp": 1599863938941, + "deliveryMode": 1, + "correlationID": "", + "replyTo": "null", + "destination": { + "physicalName": "testQueue" + }, + "redelivered": false, + "type": "", + "expiration": 0, + "priority": 0, + "data": "RW50ZXIgc29tZSB0ZXh0IGhlcmUgZm9yIHRoZSBtZXNzYWdlIGJvZHkuLi4=", + "brokerInTime": 1599863938943, + "brokerOutTime": 1599863938944, + "properties": {"testKey": "testValue"} + } + ] +} diff --git a/lambda-events/src/fixtures/example-alb-lambda-target-request-headers-only.json b/lambda-events/src/fixtures/example-alb-lambda-target-request-headers-only.json new file mode 100644 index 000000000..8fc2ec511 --- /dev/null +++ b/lambda-events/src/fixtures/example-alb-lambda-target-request-headers-only.json @@ -0,0 +1,26 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/lambda-target/abcdefg" + } + }, + "httpMethod": "GET", + "path": "/", + "queryStringParameters": { + "key": "hello" + }, + "headers": { + "accept": "*/*", + "connection": "keep-alive", + "host": "lambda-test-alb-1334523864.us-east-1.elb.amazonaws.com", + "user-agent": "curl/7.54.0", + "x-amzn-trace-id": "Root=1-5c34e93e-4dea0086f9763ac0667b115a", + "x-forwarded-for": "25.12.198.67", + "x-forwarded-port": "80", + "x-forwarded-proto": "http", + "x-imforwards": "20", + "x-myheader": "123" + }, + "isBase64Encoded": false +} + diff --git a/lambda-events/src/fixtures/example-alb-lambda-target-request-multivalue-headers.json b/lambda-events/src/fixtures/example-alb-lambda-target-request-multivalue-headers.json new file mode 100644 index 000000000..7d54621e5 --- /dev/null +++ b/lambda-events/src/fixtures/example-alb-lambda-target-request-multivalue-headers.json @@ -0,0 +1,49 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/lambda-target/abcdefgh" + } + }, + "httpMethod": "GET", + "path": "/", + "multiValueQueryStringParameters": { + "key": [ + "hello" + ] + }, + "multiValueHeaders": { + "accept": [ + "*/*" + ], + "connection": [ + "keep-alive" + ], + "host": [ + "lambda-test-alb-1234567.us-east-1.elb.amazonaws.com" + ], + "user-agent": [ + "curl/7.54.0" + ], + "x-amzn-trace-id": [ + "Root=1-5c34e7d4-00ca239424b68028d4c56d68" + ], + "x-forwarded-for": [ + "72.21.198.67" + ], + "x-forwarded-port": [ + "80" + ], + "x-forwarded-proto": [ + "http" + ], + "x-imforwards": [ + "20" + ], + "x-myheader": [ + "123" + ] + }, + "body": "Some text", + "isBase64Encoded": false +} + diff --git a/lambda-events/src/fixtures/example-alb-lambda-target-response.json b/lambda-events/src/fixtures/example-alb-lambda-target-response.json new file mode 100644 index 000000000..1411bf345 --- /dev/null +++ b/lambda-events/src/fixtures/example-alb-lambda-target-response.json @@ -0,0 +1,15 @@ +{ + "isBase64Encoded": false, + "statusCode": 200, + "statusDescription": "200 OK", + "headers": { + "Set-cookie": "cookies", + "Content-Type": "application/json" + }, + "multiValueHeaders": { + "Set-cookie": ["cookie-name=cookie-value;Domain=myweb.com;Secure;HttpOnly","cookie-name=cookie-value;Expires=May 8, 2019"], + "Content-Type": ["application/json"] + }, + "body": "Hello from Lambda" +} + diff --git a/lambda-events/src/fixtures/example-apigw-console-request.json b/lambda-events/src/fixtures/example-apigw-console-request.json new file mode 100644 index 000000000..e56f4cd72 --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-console-request.json @@ -0,0 +1,57 @@ +{ + "body": "{\"test\":\"body\"}", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.{dns_suffix}", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "apiKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890" + } +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-apigw-console-test-request.json b/lambda-events/src/fixtures/example-apigw-console-test-request.json new file mode 100644 index 000000000..ba119d859 --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-console-test-request.json @@ -0,0 +1,45 @@ +{ + "body": "{\r\n\t\"a\": 1\r\n}", + "headers":null, + "httpMethod":"POST", + "isBase64Encoded":false, + "multiValueHeaders":null, + "multiValueQueryStringParameters":null, + "path":"/myPath", + "pathParameters":null, + "queryStringParameters":null, + "requestContext":{ + "accountId":"xxxxx", + "apiId":"xxxxx", + "domainName":"testPrefix.testDomainName", + "domainPrefix":"testPrefix", + "extendedRequestId":"NvWWKEZbliAFliA=", + "httpMethod":"POST", + "identity":{ + "accessKey":"xxxxx", + "accountId":"xxxxx", + "apiKey":"test-invoke-api-key", + "apiKeyId":"test-invoke-api-key-id", + "caller":"xxxxx:xxxxx", + "cognitoAuthenticationProvider":null, + "cognitoAuthenticationType":null, + "cognitoIdentityId":null, + "cognitoIdentityPoolId":null, + "principalOrgId":null, + "sourceIp":"test-invoke-source-ip", + "user":"xxxxx:xxxxx", + "userAgent":"aws-internal/3 aws-sdk-java/1.12.154 Linux/5.4.156-94.273.amzn2int.x86_64 OpenJDK_64-Bit_Server_VM/25.322-b06 java/1.8.0_322 vendor/Oracle_Corporation cfg/retry-mode/standard", + "userArn":"arn:aws:sts::xxxxx:assumed-role/xxxxx/xxxxx" + }, + "path":"/myPath", + "protocol":"HTTP/1.1", + "requestId":"e5488776-afe4-4e5e-92b1-37bd23f234d6", + "requestTime":"18/Feb/2022:13:23:12 +0000", + "requestTimeEpoch":1645190592806, + "resourceId":"ddw8yd", + "resourcePath":"/myPath", + "stage":"test-invoke-stage" + }, + "resource":"/myPath", + "stageVariables":null +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-apigw-custom-auth-request-type-request.json b/lambda-events/src/fixtures/example-apigw-custom-auth-request-type-request.json new file mode 100644 index 000000000..101c63a70 --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-custom-auth-request-type-request.json @@ -0,0 +1,88 @@ +{ + "type": "REQUEST", + "methodArn": "arn:aws:execute-api:us-east-1:123456789012:s4x3opwd6i/test/GET/request", + "resource": "/request", + "path": "/request", + "httpMethod": "GET", + "headers": { + "X-AMZ-Date": "20170718T062915Z", + "Accept": "*/*", + "HeaderAuth1": "headerValue1", + "CloudFront-Viewer-Country": "US", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Is-Mobile-Viewer": "false", + "User-Agent": "...", + "X-Forwarded-Proto": "https", + "CloudFront-Is-SmartTV-Viewer": "false", + "Host": "....execute-api.us-east-1.amazonaws.com", + "Accept-Encoding": "gzip, deflate", + "X-Forwarded-Port": "443", + "X-Amzn-Trace-Id": "...", + "Via": "...cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "...", + "X-Forwarded-For": "..., ...", + "Postman-Token": "...", + "cache-control": "no-cache", + "CloudFront-Is-Desktop-Viewer": "true", + "Content-Type": "application/x-www-form-urlencoded" + }, + "multiValueHeaders": { + "X-AMZ-Date": ["20170718T062915Z"], + "Accept": ["*/*"], + "HeaderAuth1": ["headerValue1"], + "CloudFront-Viewer-Country": ["US"], + "CloudFront-Forwarded-Proto": ["https"], + "CloudFront-Is-Tablet-Viewer": ["false"], + "CloudFront-Is-Mobile-Viewer": ["false"], + "User-Agent": ["..."], + "X-Forwarded-Proto": ["https"], + "CloudFront-Is-SmartTV-Viewer": ["false"], + "Host": ["....execute-api.us-east-1.amazonaws.com"], + "Accept-Encoding": ["gzip, deflate"], + "X-Forwarded-Port": ["443"], + "X-Amzn-Trace-Id": ["..."], + "Via": ["...cloudfront.net (CloudFront)"], + "X-Amz-Cf-Id": ["..."], + "X-Forwarded-For": ["..., ..."], + "Postman-Token": ["..."], + "cache-control": ["no-cache"], + "CloudFront-Is-Desktop-Viewer": ["true"], + "Content-Type": ["application/x-www-form-urlencoded"] + }, + "queryStringParameters": { + "QueryString1": "queryValue1" + }, + "multiValueQueryStringParameters": { + "QueryString1": ["queryValue1"] + }, + "pathParameters": {}, + "stageVariables": { + "StageVar1": "stageValue1" + }, + "requestContext": { + "path": "/request", + "accountId": "123456789012", + "resourceId": "05c7jb", + "stage": "test", + "requestId": "...", + "identity": { + "apiKey": "...", + "sourceIp": "...", + "clientCert": { + "clientCertPem": "-----BEGIN CERTIFICATE-----\nMIIEZTCCAk0CAQEwDQ...", + "issuerDN": "C=US,ST=Washington,L=Seattle,O=Amazon Web Services,OU=Security,CN=My Private CA", + "serialNumber": "1", + "subjectDN": "C=US,ST=Washington,L=Seattle,O=Amazon Web Services,OU=Security,CN=My Client", + "validity": { + "notAfter": "Aug 5 00:28:21 2120 GMT", + "notBefore": "Aug 29 00:28:21 2020 GMT" + } + } + }, + "resourcePath": "/request", + "httpMethod": "GET", + "apiId": "s4x3opwd6i" + } +} + diff --git a/lambda-events/src/fixtures/example-apigw-custom-auth-request.json b/lambda-events/src/fixtures/example-apigw-custom-auth-request.json new file mode 100644 index 000000000..9ff8134e4 --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-custom-auth-request.json @@ -0,0 +1,5 @@ +{ + "type":"TOKEN", + "authorizationToken":"allow", + "methodArn":"arn:aws:execute-api:us-west-2:123456789012:ymy8tbxw7b/*/GET/" +} diff --git a/lambda-events/src/fixtures/example-apigw-custom-auth-response-with-condition.json b/lambda-events/src/fixtures/example-apigw-custom-auth-response-with-condition.json new file mode 100644 index 000000000..53a09b391 --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-custom-auth-response-with-condition.json @@ -0,0 +1,30 @@ +{ + "principalId": "yyyyyyyy", + "policyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "execute-api:Invoke" + ], + "Effect": "Deny", + "Resource": [ + "arn:aws:execute-api:{regionId}:{accountId}:{appId}/{stage}/{httpVerb}/[{resource}/[child-resources]]" + ], + "Condition": { + "StringEquals": { + "aws:SourceIp": [ + "xxx" + ] + } + } + } + ] + }, + "context": { + "stringKey": "value", + "numberKey": "1", + "booleanKey": "true" + }, + "usageIdentifierKey": "{api-key}" +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-apigw-custom-auth-response-with-single-value-action.json b/lambda-events/src/fixtures/example-apigw-custom-auth-response-with-single-value-action.json new file mode 100644 index 000000000..e656caaa4 --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-custom-auth-response-with-single-value-action.json @@ -0,0 +1,19 @@ +{ + "principalId": "yyyyyyyy", + "policyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "execute-api:Invoke", + "Effect": "Allow", + "Resource": ["arn:aws:execute-api:{regionId}:{accountId}:{appId}/{stage}/{httpVerb}/[{resource}/[child-resources]]"] + } + ] + }, + "context": { + "stringKey": "value", + "numberKey": "1", + "booleanKey": "true" + }, + "usageIdentifierKey": "{api-key}" +} diff --git a/lambda-events/src/fixtures/example-apigw-custom-auth-response-with-single-value-resource.json b/lambda-events/src/fixtures/example-apigw-custom-auth-response-with-single-value-resource.json new file mode 100644 index 000000000..af96bb172 --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-custom-auth-response-with-single-value-resource.json @@ -0,0 +1,19 @@ +{ + "principalId": "yyyyyyyy", + "policyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": ["execute-api:Invoke"], + "Effect": "Allow", + "Resource": "arn:aws:execute-api:{regionId}:{accountId}:{appId}/{stage}/{httpVerb}/[{resource}/[child-resources]]" + } + ] + }, + "context": { + "stringKey": "value", + "numberKey": "1", + "booleanKey": "true" + }, + "usageIdentifierKey": "{api-key}" +} diff --git a/lambda-events/src/fixtures/example-apigw-custom-auth-response.json b/lambda-events/src/fixtures/example-apigw-custom-auth-response.json new file mode 100644 index 000000000..b1502cded --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-custom-auth-response.json @@ -0,0 +1,23 @@ +{ + "principalId": "yyyyyyyy", + "policyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "execute-api:Invoke" + ], + "Effect": "Deny", + "Resource": [ + "arn:aws:execute-api:{regionId}:{accountId}:{appId}/{stage}/{httpVerb}/[{resource}/[child-resources]]" + ] + } + ] + }, + "context": { + "stringKey": "value", + "numberKey": "1", + "booleanKey": "true" + }, + "usageIdentifierKey": "{api-key}" +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-apigw-request-catch-all.json b/lambda-events/src/fixtures/example-apigw-request-catch-all.json new file mode 100644 index 000000000..fe1955f46 --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-request-catch-all.json @@ -0,0 +1,137 @@ +{ + "resource": "/{proxy+}", + "path": "/hello/world", + "httpMethod": "POST", + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "cache-control": "no-cache", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Content-Type": "application/json", + "headerName": "headerValue", + "Host": "gy415nuibc.execute-api.us-east-1.amazonaws.com", + "Postman-Token": "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f", + "User-Agent": "PostmanRuntime/2.4.5", + "Via": "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==", + "X-Forwarded-For": "54.240.196.186, 54.182.214.83", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "cache-control": [ + "no-cache" + ], + "CloudFront-Forwarded-Proto": [ + "https" + ], + "CloudFront-Is-Desktop-Viewer": [ + "true" + ], + "CloudFront-Is-Mobile-Viewer": [ + "false" + ], + "CloudFront-Is-SmartTV-Viewer": [ + "false" + ], + "CloudFront-Is-Tablet-Viewer": [ + "false" + ], + "CloudFront-Viewer-Country": [ + "US" + ], + "Content-Type": [ + "application/json" + ], + "headerName": [ + "headerValue" + ], + "Host": [ + "gy415nuibc.execute-api.us-east-1.amazonaws.com" + ], + "Postman-Token": [ + "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f" + ], + "User-Agent": [ + "PostmanRuntime/2.4.5" + ], + "Via": [ + "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)" + ], + "X-Amz-Cf-Id": [ + "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==" + ], + "X-Forwarded-For": [ + "54.240.196.186, 54.182.214.83" + ], + "X-Forwarded-Port": [ + "443" + ], + "X-Forwarded-Proto": [ + "https" + ] + }, + "queryStringParameters": { + "name": "me" + }, + "multiValueQueryStringParameters": { + "name": [ + "me" + ] + }, + "pathParameters": { + "proxy": "hello/world" + }, + "stageVariables": { + "stageVariableName": "stageVariableValue" + }, + "requestContext": { + "accountId": "12345678912", + "resourceId": "roq9wj", + "path": "/hello/world", + "stage": "testStage", + "domainName": "gy415nuibc.execute-api.us-east-2.amazonaws.com", + "domainPrefix": "y0ne18dixk", + "requestId": "deef4878-7910-11e6-8f14-25afc3e9ae33", + "protocol": "HTTP/1.1", + "identity": { + "cognitoIdentityPoolId": "theCognitoIdentityPoolId", + "accountId": "theAccountId", + "cognitoIdentityId": "theCognitoIdentityId", + "caller": "theCaller", + "apiKey": "theApiKey", + "apiKeyId": "theApiKeyId", + "accessKey": "ANEXAMPLEOFACCESSKEY", + "sourceIp": "192.168.196.186", + "cognitoAuthenticationType": "theCognitoAuthenticationType", + "cognitoAuthenticationProvider": "theCognitoAuthenticationProvider", + "userArn": "theUserArn", + "userAgent": "PostmanRuntime/2.4.5", + "user": "theUser", + "otherField": 2345 + }, + "authorizer": { + "principalId": "admin", + "clientId": 1, + "clientName": "Exata" + }, + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "requestTime": "15/May/2020:06:01:09 +0000", + "requestTimeEpoch": 1589522469693, + "apiId": "gy415nuibc" + }, + "body": "{\r\n\t\"a\": 1\r\n}", + "otherField": "foobar" +} diff --git a/lambda-events/src/fixtures/example-apigw-request-multi-value-parameters.json b/lambda-events/src/fixtures/example-apigw-request-multi-value-parameters.json new file mode 100644 index 000000000..340b1df12 --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-request-multi-value-parameters.json @@ -0,0 +1,136 @@ +{ + "resource": "/{proxy+}", + "path": "/hello/world", + "httpMethod": "POST", + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "cache-control": "no-cache", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Content-Type": "application/json", + "headerName": "headerValue2", + "Host": "gy415nuibc.execute-api.us-east-1.amazonaws.com", + "Postman-Token": "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f", + "User-Agent": "PostmanRuntime/2.4.5", + "Via": "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==", + "X-Forwarded-For": "54.240.196.186, 54.182.214.83", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "cache-control": [ + "no-cache" + ], + "CloudFront-Forwarded-Proto": [ + "https" + ], + "CloudFront-Is-Desktop-Viewer": [ + "true" + ], + "CloudFront-Is-Mobile-Viewer": [ + "false" + ], + "CloudFront-Is-SmartTV-Viewer": [ + "false" + ], + "CloudFront-Is-Tablet-Viewer": [ + "false" + ], + "CloudFront-Viewer-Country": [ + "US" + ], + "Content-Type": [ + "application/json" + ], + "headerName": [ + "headerValue", + "headerValue2" + ], + "Host": [ + "gy415nuibc.execute-api.us-east-1.amazonaws.com" + ], + "Postman-Token": [ + "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f" + ], + "User-Agent": [ + "PostmanRuntime/2.4.5" + ], + "Via": [ + "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)" + ], + "X-Amz-Cf-Id": [ + "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==" + ], + "X-Forwarded-For": [ + "54.240.196.186, 54.182.214.83" + ], + "X-Forwarded-Port": [ + "443" + ], + "X-Forwarded-Proto": [ + "https" + ] + }, + "queryStringParameters": { + "name": "me" + }, + "multiValueQueryStringParameters": { + "name": [ + "me", "me2" + ] + }, + "pathParameters": { + "proxy": "hello/world" + }, + "stageVariables": { + "stageVariableName": "stageVariableValue" + }, + "requestContext": { + "accountId": "12345678912", + "resourceId": "roq9wj", + "path": "/hello/world", + "stage": "testStage", + "domainName": "gy415nuibc.execute-api.us-east-2.amazonaws.com", + "domainPrefix": "y0ne18dixk", + "requestId": "deef4878-7910-11e6-8f14-25afc3e9ae33", + "protocol": "HTTP/1.1", + "identity": { + "cognitoIdentityPoolId": "theCognitoIdentityPoolId", + "accountId": "theAccountId", + "cognitoIdentityId": "theCognitoIdentityId", + "caller": "theCaller", + "apiKey": "theApiKey", + "apiKeyId": "theApiKeyId", + "accessKey": "ANEXAMPLEOFACCESSKEY", + "sourceIp": "192.168.196.186", + "cognitoAuthenticationType": "theCognitoAuthenticationType", + "cognitoAuthenticationProvider": "theCognitoAuthenticationProvider", + "userArn": "theUserArn", + "userAgent": "PostmanRuntime/2.4.5", + "user": "theUser" + }, + "authorizer": { + "principalId": "admin", + "clientId": 1, + "clientName": "Exata" + }, + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "requestTime": "15/May/2020:06:01:09 +0000", + "requestTimeEpoch": 1589522469693, + "apiId": "gy415nuibc" + }, + "body": "{\r\n\t\"a\": 1\r\n}" +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-apigw-request.json b/lambda-events/src/fixtures/example-apigw-request.json new file mode 100644 index 000000000..d91e9609a --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-request.json @@ -0,0 +1,135 @@ +{ + "resource": "/{proxy+}", + "path": "/hello/world", + "httpMethod": "POST", + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "cache-control": "no-cache", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Content-Type": "application/json", + "headerName": "headerValue", + "Host": "gy415nuibc.execute-api.us-east-1.amazonaws.com", + "Postman-Token": "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f", + "User-Agent": "PostmanRuntime/2.4.5", + "Via": "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==", + "X-Forwarded-For": "54.240.196.186, 54.182.214.83", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "cache-control": [ + "no-cache" + ], + "CloudFront-Forwarded-Proto": [ + "https" + ], + "CloudFront-Is-Desktop-Viewer": [ + "true" + ], + "CloudFront-Is-Mobile-Viewer": [ + "false" + ], + "CloudFront-Is-SmartTV-Viewer": [ + "false" + ], + "CloudFront-Is-Tablet-Viewer": [ + "false" + ], + "CloudFront-Viewer-Country": [ + "US" + ], + "Content-Type": [ + "application/json" + ], + "headerName": [ + "headerValue" + ], + "Host": [ + "gy415nuibc.execute-api.us-east-1.amazonaws.com" + ], + "Postman-Token": [ + "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f" + ], + "User-Agent": [ + "PostmanRuntime/2.4.5" + ], + "Via": [ + "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)" + ], + "X-Amz-Cf-Id": [ + "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==" + ], + "X-Forwarded-For": [ + "54.240.196.186, 54.182.214.83" + ], + "X-Forwarded-Port": [ + "443" + ], + "X-Forwarded-Proto": [ + "https" + ] + }, + "queryStringParameters": { + "name": "me" + }, + "multiValueQueryStringParameters": { + "name": [ + "me" + ] + }, + "pathParameters": { + "proxy": "hello/world" + }, + "stageVariables": { + "stageVariableName": "stageVariableValue" + }, + "requestContext": { + "accountId": "12345678912", + "resourceId": "roq9wj", + "path": "/hello/world", + "stage": "testStage", + "domainName": "gy415nuibc.execute-api.us-east-2.amazonaws.com", + "domainPrefix": "y0ne18dixk", + "requestId": "deef4878-7910-11e6-8f14-25afc3e9ae33", + "protocol": "HTTP/1.1", + "identity": { + "cognitoIdentityPoolId": "theCognitoIdentityPoolId", + "accountId": "theAccountId", + "cognitoIdentityId": "theCognitoIdentityId", + "caller": "theCaller", + "apiKey": "theApiKey", + "apiKeyId": "theApiKeyId", + "accessKey": "ANEXAMPLEOFACCESSKEY", + "sourceIp": "192.168.196.186", + "cognitoAuthenticationType": "theCognitoAuthenticationType", + "cognitoAuthenticationProvider": "theCognitoAuthenticationProvider", + "userArn": "theUserArn", + "userAgent": "PostmanRuntime/2.4.5", + "user": "theUser" + }, + "authorizer": { + "principalId": "admin", + "clientId": 1, + "clientName": "Exata" + }, + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "requestTime": "15/May/2020:06:01:09 +0000", + "requestTimeEpoch": 1589522469693, + "apiId": "gy415nuibc" + }, + "body": "{\r\n\t\"a\": 1\r\n}" +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-apigw-response.json b/lambda-events/src/fixtures/example-apigw-response.json new file mode 100644 index 000000000..ba87222e2 --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-response.json @@ -0,0 +1,42 @@ +{ + "statusCode": 200, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, lzma, sdch, br", + "Accept-Language": "en-US,en;q=0.8", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "wt6mne2s9k.execute-api.us-west-2.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48", + "Via": "1.1 fb7cca60f0ecd82ce07790c9c5eef16c.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "nBsWBOrSHMgnaROZJK1wGCZ9PcRcSpq_oSXZNQwQ10OTZL4cimZo3g==", + "X-Forwarded-For": "192.168.100.1, 192.168.1.1", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"], + "Accept-Encoding": ["gzip, deflate, lzma, sdch, br"], + "Accept-Language": ["en-US,en;q=0.8"], + "CloudFront-Forwarded-Proto": ["https"], + "CloudFront-Is-Desktop-Viewer": ["true"], + "CloudFront-Is-Mobile-Viewer": ["false"], + "CloudFront-Is-SmartTV-Viewer": ["false"], + "CloudFront-Is-Tablet-Viewer": ["false"], + "CloudFront-Viewer-Country": ["US"], + "Host": ["wt6mne2s9k.execute-api.us-west-2.amazonaws.com"], + "Upgrade-Insecure-Requests": ["1"], + "User-Agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48"], + "Via": ["1.1 fb7cca60f0ecd82ce07790c9c5eef16c.cloudfront.net (CloudFront)"], + "X-Amz-Cf-Id": ["nBsWBOrSHMgnaROZJK1wGCZ9PcRcSpq_oSXZNQwQ10OTZL4cimZo3g=="], + "X-Forwarded-For": ["192.168.100.1, 192.168.1.1"], + "X-Forwarded-Port": ["443"], + "X-Forwarded-Proto": ["https"] + }, + "body": "Hello World" + } diff --git a/lambda-events/src/fixtures/example-apigw-restapi-openapi-request.json b/lambda-events/src/fixtures/example-apigw-restapi-openapi-request.json new file mode 100644 index 000000000..906d0eb57 --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-restapi-openapi-request.json @@ -0,0 +1,97 @@ +{ + "resource": "/{proxy+}", + "path": "/hello/world", + "httpMethod": "POST", + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "cache-control": "no-cache", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Content-Type": "application/json", + "headerName": "headerValue", + "Host": "gy415nuibc.execute-api.us-east-1.amazonaws.com", + "Postman-Token": "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f", + "User-Agent": "PostmanRuntime/2.4.5", + "Via": "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==", + "X-Forwarded-For": "54.240.196.186, 54.182.214.83", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Accept": ["*/*"], + "Accept-Encoding": ["gzip, deflate"], + "cache-control": ["no-cache"], + "CloudFront-Forwarded-Proto": ["https"], + "CloudFront-Is-Desktop-Viewer": ["true"], + "CloudFront-Is-Mobile-Viewer": ["false"], + "CloudFront-Is-SmartTV-Viewer": ["false"], + "CloudFront-Is-Tablet-Viewer": ["false"], + "CloudFront-Viewer-Country": ["US"], + "Content-Type": ["application/json"], + "headerName": ["headerValue"], + "Host": ["gy415nuibc.execute-api.us-east-1.amazonaws.com"], + "Postman-Token": ["9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f"], + "User-Agent": ["PostmanRuntime/2.4.5"], + "Via": ["1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)"], + "X-Amz-Cf-Id": ["pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A=="], + "X-Forwarded-For": ["54.240.196.186, 54.182.214.83"], + "X-Forwarded-Port": ["443"], + "X-Forwarded-Proto": ["https"] + }, + "queryStringParameters": { + "name": "me" + }, + "multiValueQueryStringParameters": { + "name": ["me"] + }, + "pathParameters": { + "proxy": "hello/world" + }, + "stageVariables": { + "stageVariableName": "stageVariableValue" + }, + "requestContext": { + "accountId": "12345678912", + "resourceId": "roq9wj", + "path": "/hello/world", + "operationName": "HelloWorld", + "stage": "testStage", + "domainName": "gy415nuibc.execute-api.us-east-2.amazonaws.com", + "domainPrefix": "y0ne18dixk", + "requestId": "deef4878-7910-11e6-8f14-25afc3e9ae33", + "protocol": "HTTP/1.1", + "identity": { + "cognitoIdentityPoolId": "theCognitoIdentityPoolId", + "accountId": "theAccountId", + "cognitoIdentityId": "theCognitoIdentityId", + "caller": "theCaller", + "apiKey": "theApiKey", + "apiKeyId": "theApiKeyId", + "accessKey": "ANEXAMPLEOFACCESSKEY", + "sourceIp": "192.168.196.186", + "cognitoAuthenticationType": "theCognitoAuthenticationType", + "cognitoAuthenticationProvider": "theCognitoAuthenticationProvider", + "userArn": "theUserArn", + "userAgent": "PostmanRuntime/2.4.5", + "user": "theUser" + }, + "authorizer": { + "principalId": "admin", + "clientId": 1, + "clientName": "Exata" + }, + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "requestTime": "15/May/2020:06:01:09 +0000", + "requestTimeEpoch": 1589522469693, + "apiId": "gy415nuibc" + }, + "body": "{\r\n\t\"a\": 1\r\n}" +} + diff --git a/lambda-events/src/fixtures/example-apigw-sam-http-request.json b/lambda-events/src/fixtures/example-apigw-sam-http-request.json new file mode 100644 index 000000000..c60b05a9f --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-sam-http-request.json @@ -0,0 +1,52 @@ +{ + "body": "", + "cookies": [], + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "en-GB,en-US;q=0.9,en;q=0.8", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "Host": "127.0.0.1:3000", + "Pragma": "no-cache", + "Sec-Ch-Ua": "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Google Chrome\";v=\"120\"", + "Sec-Ch-Ua-Mobile": "?0", + "Sec-Ch-Ua-Platform": "\"macOS\"", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Site": "none", + "Sec-Fetch-User": "?1", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "X-Forwarded-Port": "3000", + "X-Forwarded-Proto": "http" + }, + "isBase64Encoded": false, + "pathParameters": {}, + "queryStringParameters": { + "foo": "bar" + }, + "rawPath": "/dump", + "rawQueryString": "foo=bar", + "requestContext": { + "accountId": "123456789012", + "apiId": "1234567890", + "domainName": "localhost", + "domainPrefix": "localhost", + "http": { + "method": "GET", + "path": "/dump", + "protocol": "HTTP/1.1", + "sourceIp": "127.0.0.1", + "userAgent": "Custom User Agent String" + }, + "requestId": "b3ea2f1a-9f2a-48a4-8791-0db24e5d40e5", + "routeKey": "GET /dump", + "stage": "$default", + "time": "28/Jan/2024:12:09:08 +0000", + "timeEpoch": 1706443748 + }, + "routeKey": "GET /dump", + "stageVariables": null, + "version": "2.0" +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-apigw-sam-rest-request.json b/lambda-events/src/fixtures/example-apigw-sam-rest-request.json new file mode 100644 index 000000000..d125a7ab0 --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-sam-rest-request.json @@ -0,0 +1,111 @@ +{ + "body": null, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "en-GB,en-US;q=0.9,en;q=0.8", + "Cache-Control": "max-age=0", + "Connection": "keep-alive", + "Host": "127.0.0.1:3000", + "Sec-Ch-Ua": "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Google Chrome\";v=\"120\"", + "Sec-Ch-Ua-Mobile": "?0", + "Sec-Ch-Ua-Platform": "\"macOS\"", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Site": "none", + "Sec-Fetch-User": "?1", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "X-Forwarded-Port": "3000", + "X-Forwarded-Proto": "http" + }, + "httpMethod": "GET", + "isBase64Encoded": false, + "multiValueHeaders": { + "Accept": [ + "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" + ], + "Accept-Encoding": [ + "gzip, deflate, br" + ], + "Accept-Language": [ + "en-GB,en-US;q=0.9,en;q=0.8" + ], + "Cache-Control": [ + "max-age=0" + ], + "Connection": [ + "keep-alive" + ], + "Host": [ + "127.0.0.1:3000" + ], + "Sec-Ch-Ua": [ + "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Google Chrome\";v=\"120\"" + ], + "Sec-Ch-Ua-Mobile": [ + "?0" + ], + "Sec-Ch-Ua-Platform": [ + "\"macOS\"" + ], + "Sec-Fetch-Dest": [ + "document" + ], + "Sec-Fetch-Mode": [ + "navigate" + ], + "Sec-Fetch-Site": [ + "none" + ], + "Sec-Fetch-User": [ + "?1" + ], + "Upgrade-Insecure-Requests": [ + "1" + ], + "User-Agent": [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + ], + "X-Forwarded-Port": [ + "3000" + ], + "X-Forwarded-Proto": [ + "http" + ] + }, + "multiValueQueryStringParameters": null, + "path": "/test", + "pathParameters": null, + "queryStringParameters": null, + "requestContext": { + "accountId": "123456789012", + "apiId": "1234567890", + "domainName": "127.0.0.1:3000", + "extendedRequestId": null, + "httpMethod": "GET", + "identity": { + "accountId": null, + "apiKey": null, + "caller": null, + "cognitoAuthenticationProvider": null, + "cognitoAuthenticationType": null, + "cognitoIdentityPoolId": null, + "sourceIp": "127.0.0.1", + "user": null, + "userAgent": "Custom User Agent String", + "userArn": null + }, + "path": "/test", + "protocol": "HTTP/1.1", + "requestId": "ea86b9eb-c688-4bbb-8309-a1671442bea9", + "requestTime": "28/Jan/2024:11:05:46 +0000", + "requestTimeEpoch": 1706439946, + "resourceId": "123456", + "resourcePath": "/test", + "stage": "Prod" + }, + "resource": "/test", + "stageVariables": null, + "version": "1.0" +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-apigw-v2-custom-authorizer-v1-request.json b/lambda-events/src/fixtures/example-apigw-v2-custom-authorizer-v1-request.json new file mode 100644 index 000000000..c220f37ba --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-v2-custom-authorizer-v1-request.json @@ -0,0 +1,61 @@ +{ + "version": "1.0", + "type": "REQUEST", + "methodArn": "arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/request", + "identitySource": "user1,123", + "authorizationToken": "user1,123", + "resource": "/request", + "path": "/request", + "httpMethod": "GET", + "headers": { + "X-AMZ-Date": "20170718T062915Z", + "Accept": "*/*", + "HeaderAuth1": "headerValue1", + "CloudFront-Viewer-Country": "US", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Is-Mobile-Viewer": "false", + "User-Agent": "..." + }, + "queryStringParameters": { + "QueryString1": "queryValue1" + }, + "pathParameters": {}, + "stageVariables": { + "StageVar1": "stageValue1" + }, + "requestContext": { + "path": "/request", + "accountId": "123456789012", + "resourceId": "05c7jb", + "stage": "test", + "requestId": "...", + "identity": { + "apiKey": "...", + "sourceIp": "...", + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "http": { + "method": "POST", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "IP", + "userAgent": "agent" + }, + "resourcePath": "/request", + "httpMethod": "GET", + "apiId": "abcdef123", + "routeKey": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + } +} diff --git a/lambda-events/src/fixtures/example-apigw-v2-custom-authorizer-v2-request-null-maps.json b/lambda-events/src/fixtures/example-apigw-v2-custom-authorizer-v2-request-null-maps.json new file mode 100644 index 000000000..5702b7fd9 --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-v2-custom-authorizer-v2-request-null-maps.json @@ -0,0 +1,48 @@ +{ + "version": "2.0", + "type": "REQUEST", + "routeArn": "arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/request", + "identitySource": ["user1", "123"], + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": ["cookie1", "cookie2"], + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "queryStringParameters": null, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "authentication": { + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "IP", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "pathParameters": null, + "stageVariables": null +} + diff --git a/lambda-events/src/fixtures/example-apigw-v2-custom-authorizer-v2-request-without-cookies.json b/lambda-events/src/fixtures/example-apigw-v2-custom-authorizer-v2-request-without-cookies.json new file mode 100644 index 000000000..a70cac91f --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-v2-custom-authorizer-v2-request-without-cookies.json @@ -0,0 +1,49 @@ +{ + "version": "2.0", + "type": "REQUEST", + "routeArn": "arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/request", + "identitySource": ["user1", "123"], + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "authentication": { + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "IP", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "pathParameters": { "parameter1": "value1" }, + "stageVariables": { "stageVariable1": "value1", "stageVariable2": "value2" } +} diff --git a/lambda-events/src/fixtures/example-apigw-v2-custom-authorizer-v2-request-without-identity-source.json b/lambda-events/src/fixtures/example-apigw-v2-custom-authorizer-v2-request-without-identity-source.json new file mode 100644 index 000000000..cc79dda33 --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-v2-custom-authorizer-v2-request-without-identity-source.json @@ -0,0 +1,50 @@ +{ + "version": "2.0", + "type": "REQUEST", + "routeArn": "arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/request", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": ["cookie1", "cookie2"], + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "authentication": { + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "IP", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "pathParameters": { "parameter1": "value1" }, + "stageVariables": { "stageVariable1": "value1", "stageVariable2": "value2" } +} + diff --git a/lambda-events/src/fixtures/example-apigw-v2-custom-authorizer-v2-request.json b/lambda-events/src/fixtures/example-apigw-v2-custom-authorizer-v2-request.json new file mode 100644 index 000000000..59166c8c0 --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-v2-custom-authorizer-v2-request.json @@ -0,0 +1,51 @@ +{ + "version": "2.0", + "type": "REQUEST", + "routeArn": "arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/request", + "identitySource": ["user1", "123"], + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": ["cookie1", "cookie2"], + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "authentication": { + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "IP", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "pathParameters": { "parameter1": "value1" }, + "stageVariables": { "stageVariable1": "value1", "stageVariable2": "value2" } +} + diff --git a/lambda-events/src/fixtures/example-apigw-v2-custom-authorizer-websocket-request.json b/lambda-events/src/fixtures/example-apigw-v2-custom-authorizer-websocket-request.json new file mode 100644 index 000000000..1e741abf0 --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-v2-custom-authorizer-websocket-request.json @@ -0,0 +1,51 @@ +{ + "version": "$LATEST", + "type": "REQUEST", + "methodArn": "arn:aws:execute-api:eu-west-1:123456789012:abcdef123/test/$connect", + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "multiValueHeaders": { + "Header1": ["value1"], + "Header2": ["value2"] + }, + "multiValueQueryStringParameters": {}, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "apiId": "api-id", + "connectedAt": 1655103417248, + "connectionId": "connection-id", + "domainName": "example.com", + "eventType": "CONNECT", + "extendedRequestId": "extended-req-id", + "identity": { + "sourceIp": "1.2.3.4", + "userAgent": "user-agent" + }, + "messageDirection": "IN", + "requestId": "req-id", + "requestTime": "13/Jun/2022:06:56:57 +0000", + "requestTimeEpoch": 1655103417249, + "routeKey": "$connect", + "stage": "test" + }, + "stageVariables": {}, + "context": { + "request_id": "5req-id" + }, + "deadline": 1655103420483, + "invoked_function_arn": "arn:aws:lambda:eu-west-1:123456789012:function:a-lambda", + "xray_trace_id": "xray-trace-id", + "client_context": null, + "identity": null, + "env_config": { + "function_name": "a-lambda" + }, + "memory": 128, + "log_stream": "log-stream-id", + "log_group": "log-group-id" +} diff --git a/lambda-events/src/fixtures/example-apigw-v2-request-iam.json b/lambda-events/src/fixtures/example-apigw-v2-request-iam.json new file mode 100644 index 000000000..b78edb3d1 --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-v2-request-iam.json @@ -0,0 +1,73 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "pathParameters": { + "proxy": "hello/world" + }, + "requestContext": { + "routeKey": "$default", + "accountId": "123456789012", + "stage": "$default", + "requestId": "id", + "authorizer": { + "iam": { + "accessKey": "ARIA2ZJZYVUEREEIHAKY", + "accountId": "1234567890", + "callerId": "AROA7ZJZYVRE7C3DUXHH6:CognitoIdentityCredentials", + "cognitoIdentity": { + "amr" : ["foo"], + "identityId": "us-east-1:3f291106-8703-466b-8f2b-3ecee1ca56ce", + "identityPoolId": "us-east-1:4f291106-8703-466b-8f2b-3ecee1ca56ce" + }, + "principalOrgId": "AwsOrgId", + "userArn": "arn:aws:iam::1234567890:user/Admin", + "userId": "AROA2ZJZYVRE7Y3TUXHH6" + } + }, + "apiId": "api-id", + "authentication": { + "clientCert": { + "clientCertPem": "-----BEGIN CERTIFICATE-----\nMIIEZTCCAk0CAQEwDQ...", + "issuerDN": "C=US,ST=Washington,L=Seattle,O=Amazon Web Services,OU=Security,CN=My Private CA", + "serialNumber": "1", + "subjectDN": "C=US,ST=Washington,L=Seattle,O=Amazon Web Services,OU=Security,CN=My Client", + "validity": { + "notAfter": "Aug 5 00:28:21 2120 GMT", + "notBefore": "Aug 29 00:28:21 2020 GMT" + } + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "time": "12/Mar/2020:19:03:58+0000", + "timeEpoch": 1583348638390, + "http": { + "method": "GET", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "IP", + "userAgent": "agent" + } + }, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + }, + "body": "{\r\n\t\"a\": 1\r\n}", + "isBase64Encoded": false +} + diff --git a/lambda-events/src/fixtures/example-apigw-v2-request-jwt-authorizer.json b/lambda-events/src/fixtures/example-apigw-v2-request-jwt-authorizer.json new file mode 100644 index 000000000..e3422a9a2 --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-v2-request-jwt-authorizer.json @@ -0,0 +1,70 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "pathParameters": { + "proxy": "hello/world" + }, + "requestContext": { + "routeKey": "$default", + "accountId": "123456789012", + "stage": "$default", + "requestId": "id", + "authorizer": { + "jwt": { + "claims": { + "claim1": "value1", + "claim2": "value2" + }, + "scopes": [ + "scope1", + "scope2" + ] + } + }, + "apiId": "api-id", + "authentication": { + "clientCert": { + "clientCertPem": "-----BEGIN CERTIFICATE-----\nMIIEZTCCAk0CAQEwDQ...", + "issuerDN": "C=US,ST=Washington,L=Seattle,O=Amazon Web Services,OU=Security,CN=My Private CA", + "serialNumber": "1", + "subjectDN": "C=US,ST=Washington,L=Seattle,O=Amazon Web Services,OU=Security,CN=My Client", + "validity": { + "notAfter": "Aug 5 00:28:21 2120 GMT", + "notBefore": "Aug 29 00:28:21 2020 GMT" + } + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "time": "12/Mar/2020:19:03:58+0000", + "timeEpoch": 1583348638390, + "http": { + "method": "GET", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "IP", + "userAgent": "agent" + } + }, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + }, + "body": "{\r\n\t\"a\": 1\r\n}", + "isBase64Encoded": false +} + diff --git a/lambda-events/src/fixtures/example-apigw-v2-request-lambda-authorizer.json b/lambda-events/src/fixtures/example-apigw-v2-request-lambda-authorizer.json new file mode 100644 index 000000000..60263e99d --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-v2-request-lambda-authorizer.json @@ -0,0 +1,63 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "pathParameters": { + "proxy": "hello/world" + }, + "requestContext": { + "routeKey": "$default", + "accountId": "123456789012", + "stage": "$default", + "requestId": "id", + "authorizer": { + "lambda": { + "key": "value" + } + }, + "apiId": "api-id", + "authentication": { + "clientCert": { + "clientCertPem": "-----BEGIN CERTIFICATE-----\nMIIEZTCCAk0CAQEwDQ...", + "issuerDN": "C=US,ST=Washington,L=Seattle,O=Amazon Web Services,OU=Security,CN=My Private CA", + "serialNumber": "1", + "subjectDN": "C=US,ST=Washington,L=Seattle,O=Amazon Web Services,OU=Security,CN=My Client", + "validity": { + "notAfter": "Aug 5 00:28:21 2120 GMT", + "notBefore": "Aug 29 00:28:21 2020 GMT" + } + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "time": "12/Mar/2020:19:03:58+0000", + "timeEpoch": 1583348638390, + "http": { + "method": "GET", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "IP", + "userAgent": "agent" + } + }, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + }, + "body": "{\r\n\t\"a\": 1\r\n}", + "isBase64Encoded": false +} + diff --git a/lambda-events/src/fixtures/example-apigw-v2-request-multi-value-parameters.json b/lambda-events/src/fixtures/example-apigw-v2-request-multi-value-parameters.json new file mode 100644 index 000000000..5094d6c2e --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-v2-request-multi-value-parameters.json @@ -0,0 +1,61 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "Parameter1=value1&Parameter1=value2", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "Header2": "value1,value2" + }, + "queryStringParameters": { + "Parameter1": "value1,value2" + }, + "pathParameters": { + "proxy": "hello/world" + }, + "requestContext": { + "routeKey": "$default", + "accountId": "123456789012", + "stage": "$default", + "requestId": "id", + "authorizer": { + "lambda": { + "key": "value" + } + }, + "apiId": "api-id", + "authentication": { + "clientCert": { + "clientCertPem": "-----BEGIN CERTIFICATE-----\nMIIEZTCCAk0CAQEwDQ...", + "issuerDN": "C=US,ST=Washington,L=Seattle,O=Amazon Web Services,OU=Security,CN=My Private CA", + "serialNumber": "1", + "subjectDN": "C=US,ST=Washington,L=Seattle,O=Amazon Web Services,OU=Security,CN=My Client", + "validity": { + "notAfter": "Aug 5 00:28:21 2120 GMT", + "notBefore": "Aug 29 00:28:21 2020 GMT" + } + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "time": "12/Mar/2020:19:03:58+0000", + "timeEpoch": 1583348638390, + "http": { + "method": "GET", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "IP", + "userAgent": "agent" + } + }, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + }, + "body": "{\r\n\t\"a\": 1\r\n}", + "isBase64Encoded": false +} + diff --git a/lambda-events/src/fixtures/example-apigw-v2-request-no-authorizer.json b/lambda-events/src/fixtures/example-apigw-v2-request-no-authorizer.json new file mode 100644 index 000000000..9f7a78385 --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-v2-request-no-authorizer.json @@ -0,0 +1,48 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/", + "rawQueryString": "", + "headers": { + "accept": "*/*", + "content-length": "0", + "host": "aaaaaaaaaa.execute-api.us-west-2.amazonaws.com", + "user-agent": "curl/7.58.0", + "x-amzn-trace-id": "Root=1-5e9f0c65-1de4d666d4dd26aced652b6c", + "x-forwarded-for": "1.2.3.4", + "x-forwarded-port": "443", + "x-forwarded-proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "aaaaaaaaaa", + "authentication": { + "clientCert": { + "clientCertPem": "-----BEGIN CERTIFICATE-----\nMIIEZTCCAk0CAQEwDQ...", + "issuerDN": "C=US,ST=Washington,L=Seattle,O=Amazon Web Services,OU=Security,CN=My Private CA", + "serialNumber": "1", + "subjectDN": "C=US,ST=Washington,L=Seattle,O=Amazon Web Services,OU=Security,CN=My Client", + "validity": { + "notAfter": "Aug 5 00:28:21 2120 GMT", + "notBefore": "Aug 29 00:28:21 2020 GMT" + } + } + }, + "domainName": "aaaaaaaaaa.execute-api.us-west-2.amazonaws.com", + "domainPrefix": "aaaaaaaaaa", + "http": { + "method": "GET", + "path": "/", + "protocol": "HTTP/1.1", + "sourceIp": "1.2.3.4", + "userAgent": "curl/7.58.0" + }, + "requestId": "LV7fzho-PHcEJPw=", + "routeKey": "$default", + "stage": "$default", + "time": "21/Apr/2020:15:08:21 +0000", + "timeEpoch": 1587481701067 + }, + "isBase64Encoded": false +} + diff --git a/lambda-events/src/fixtures/example-apigw-websocket-request-disconnect-route.json b/lambda-events/src/fixtures/example-apigw-websocket-request-disconnect-route.json new file mode 100644 index 000000000..3a6f0cee7 --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-websocket-request-disconnect-route.json @@ -0,0 +1,33 @@ +{ + "headers": { + "Host": "abcd1234.execute-api.us-east-1.amazonaws.com", + "x-api-key": "", + "X-Forwarded-For": "", + "x-restapi": "" + }, + "multiValueHeaders": { + "Host": [ "abcd1234.execute-api.us-east-1.amazonaws.com" ], + "x-api-key": [ "" ], + "X-Forwarded-For": [ "" ], + "x-restapi": [ "" ] + }, + "requestContext": { + "routeKey": "$disconnect", + "disconnectStatusCode": 1005, + "eventType": "DISCONNECT", + "extendedRequestId": "ABCD1234=", + "requestTime": "09/Feb/2024:18:23:28 +0000", + "messageDirection": "IN", + "disconnectReason": "Client-side close frame status not set", + "stage": "prod", + "connectedAt": 1707503007396, + "requestTimeEpoch": 1707503008941, + "identity": { "sourceIp": "192.0.2.1" }, + "requestId": "ABCD1234=", + "domainName": "abcd1234.execute-api.us-east-1.amazonaws.com", + "connectionId": "AAAA1234=", + "apiId": "abcd1234" + }, + "isBase64Encoded": false +} + \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-apigw-websocket-request-without-method.json b/lambda-events/src/fixtures/example-apigw-websocket-request-without-method.json new file mode 100644 index 000000000..27ec34793 --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-websocket-request-without-method.json @@ -0,0 +1,58 @@ +{ + "headers": { + "Host": "asdfasdf.execute-api.us-west-2.amazonaws.com", + "Sec-WebSocket-Key": "asdfasdf==", + "Sec-WebSocket-Version": "13", + "X-Amzn-Trace-Id": "Root=1-asdf-asdfasdf", + "x-api-key": "asdfasdf", + "X-Forwarded-For": "10.0.0.13", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Host": [ + "asdfasdf.execute-api.us-west-2.amazonaws.com" + ], + "Sec-WebSocket-Key": [ + "asdfasdf==" + ], + "Sec-WebSocket-Version": [ + "13" + ], + "X-Amzn-Trace-Id": [ + "Root=1-asdf-asdfasdf" + ], + "x-api-key": [ + "asdfasdf" + ], + "X-Forwarded-For": [ + "10.0.0.13" + ], + "X-Forwarded-Port": [ + "443" + ], + "X-Forwarded-Proto": [ + "https" + ] + }, + "requestContext": { + "routeKey": "$connect", + "eventType": "CONNECT", + "extendedRequestId": "asdfasdf=", + "requestTime": "22/Feb/2022:19:07:37 +0000", + "messageDirection": "IN", + "stage": "dev", + "connectedAt": 1645556857902, + "requestTimeEpoch": 1645556857902, + "identity": { + "apiKey": "asdfasdf", + "apiKeyId": "asdf", + "sourceIp": "10.0.0.13" + }, + "requestId": "asdf=", + "domainName": "asdfasdf.execute-api.us-west-2.amazonaws.com", + "connectionId": "asdfasdf=", + "apiId": "asdfasdf" + }, + "isBase64Encoded": false +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-apigw-websocket-request.json b/lambda-events/src/fixtures/example-apigw-websocket-request.json new file mode 100644 index 000000000..2628ae26a --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-websocket-request.json @@ -0,0 +1,108 @@ +{ + "resource": "/{proxy+}", + "path": "/hello/world", + "httpMethod": "POST", + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "cache-control": "no-cache", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Content-Type": "application/json", + "headerName": "headerValue", + "Host": "gy415nuibc.execute-api.us-east-1.amazonaws.com", + "Postman-Token": "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f", + "User-Agent": "PostmanRuntime/2.4.5", + "Via": "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==", + "X-Forwarded-For": "54.240.196.186, 54.182.214.83", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Host": [ + "*.execute-api.eu-central-1.amazonaws.com" + ], + "Sec-WebSocket-Extensions": [ + "permessage-deflate; client_max_window_bits" + ], + "Sec-WebSocket-Key": [ + "*" + ], + "Sec-WebSocket-Version": [ + "13" + ], + "X-Amzn-Trace-Id": [ + "Root=*" + ], + "X-Forwarded-For": [ + "*.*.*.*" + ], + "X-Forwarded-Port": [ + "443" + ], + "X-Forwarded-Proto": [ + "https" + ] + }, + "queryStringParameters": { + "name": "me" + }, + "multiValueQueryStringParameters": { + "name": ["me"] + }, + "pathParameters": { + "proxy": "hello/world" + }, + "stageVariables": { + "stageVariableName": "stageVariableValue" + }, + "requestContext": { + "accountId": "12345678912", + "resourceId": "roq9wj", + "stage": "testStage", + "requestId": "deef4878-7910-11e6-8f14-25afc3e9ae33", + "identity": { + "cognitoIdentityPoolId": "theCognitoIdentityPoolId", + "accountId": "theAccountId", + "cognitoIdentityId": "theCognitoIdentityId", + "caller": "theCaller", + "apiKey": "theApiKey", + "apiKeyId": "theApiKeyId", + "accessKey": "ANEXAMPLEOFACCESSKEY", + "sourceIp": "192.168.196.186", + "cognitoAuthenticationType": "theCognitoAuthenticationType", + "cognitoAuthenticationProvider": "theCognitoAuthenticationProvider", + "userArn": "theUserArn", + "userAgent": "PostmanRuntime/2.4.5", + "user": "theUser" + }, + "resourcePath": "/{proxy+}", + "authorizer": { + "principalId": "admin", + "clientId": 1, + "clientName": "Exata" + }, + "httpMethod": "POST", + "apiId": "gy415nuibc", + "connectedAt": 1547230720092, + "connectionId": "TWegAcC4EowCHnA=", + "domainName": "*.execute-api.eu-central-1.amazonaws.com", + "error": "*", + "eventType": "CONNECT", + "extendedRequestId": "TWegAcC4EowCHnA=", + "integrationLatency": "123", + "messageDirection": "IN", + "messageId": null, + "requestTime": "07/Jan/2019:09:20:57 +0000", + "requestTimeEpoch": 0, + "routeKey": "$connect", + "status": "*" + }, + "body": "{\r\n\t\"a\": 1\r\n}" +} + diff --git a/lambda-events/src/fixtures/example-appsync-batchinvoke.json b/lambda-events/src/fixtures/example-appsync-batchinvoke.json new file mode 100644 index 000000000..cd6a323ad --- /dev/null +++ b/lambda-events/src/fixtures/example-appsync-batchinvoke.json @@ -0,0 +1,28 @@ +{ + "version": "2017-02-28", + "operation": "BatchInvoke", + "payload": [{ + "arguments": { + "id": "postId1", + "count": 1, + "float": 1.2, + "flag": true + } + }, + { + "arguments": { + "id": "postId2", + "count": 2, + "float": 1.2 + } + }, + { + "arguments": { + "id": "postId3", + "count": 3, + "flag": false + } + } + ] +} + diff --git a/lambda-events/src/fixtures/example-appsync-direct-resolver.json b/lambda-events/src/fixtures/example-appsync-direct-resolver.json new file mode 100644 index 000000000..9a8048765 --- /dev/null +++ b/lambda-events/src/fixtures/example-appsync-direct-resolver.json @@ -0,0 +1,64 @@ +{ + "arguments": { + "input": "foo" + }, + "identity": { + "sourceIp": [ + "x.x.x.x" + ], + "userArn": "arn:aws:iam::123456789012:user/appsync", + "accountId": "666666666666", + "user": "AIDAAAAAAAAAAAAAAAAAA" + }, + "info": { + "fieldName": "greet", + "parentTypeName": "Query", + "selectionSetGraphQL": "", + "selectionSetList": [], + "variables": { + "inputVar": "foo" + } + }, + "prev": null, + "request": { + "domainName": null, + "headers": { + "accept": "application/json, text/plain, */*", + "accept-encoding": "gzip, deflate, br, zstd", + "accept-language": "en-US,en;q=0.9,ja;q=0.8,en-GB;q=0.7", + "cloudfront-forwarded-proto": "https", + "cloudfront-is-desktop-viewer": "true", + "cloudfront-is-mobile-viewer": "false", + "cloudfront-is-smarttv-viewer": "false", + "cloudfront-is-tablet-viewer": "false", + "cloudfront-viewer-asn": "17676", + "cloudfront-viewer-country": "JP", + "content-length": "40", + "content-type": "application/json", + "host": "2ojpkjk2ejb57l7stgad5o4qiq.appsync-api.ap-northeast-1.amazonaws.com", + "origin": "https://ap-northeast-1.console.aws.amazon.com", + "priority": "u=1, i", + "referer": "https://ap-northeast-1.console.aws.amazon.com/", + "sec-ch-ua": "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\", \"Microsoft Edge\";v=\"138\"", + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": "\"Windows\"", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "cross-site", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0", + "via": "2.0 ee337d4db5c7ebfdc8ec0798a1ede776.cloudfront.net (CloudFront)", + "x-amz-cf-id": "O3ZflUCq6_TzxjouyYB3zg7-kl7Ze-gXbniM2jJ3hAOfDFpPMGRu3Q==", + "x-amz-user-agent": "AWS-Console-AppSync/", + "x-amzn-appsync-is-vpce-request": "false", + "x-amzn-remote-ip": "x.x.x.x", + "x-amzn-requestid": "7ada8740-bbf4-49e8-bf45-f10b3d67159b", + "x-amzn-trace-id": "Root=1-68713e21-7a03739120ad60703e794b22", + "x-api-key": "***", + "x-forwarded-for": "***", + "x-forwarded-port": "443", + "x-forwarded-proto": "https" + } + }, + "source": null, + "stash": {} +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-appsync-identity-cognito.json b/lambda-events/src/fixtures/example-appsync-identity-cognito.json new file mode 100644 index 000000000..266774b64 --- /dev/null +++ b/lambda-events/src/fixtures/example-appsync-identity-cognito.json @@ -0,0 +1,19 @@ +{ + "sub": "123-456", + "issuer": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_abc", + "username": "user1", + "claims": { + "sub": "123-456", + "aud": "abcdefg", + "event_id": "123-123-123", + "token_use": "id", + "auth_time": 1551226125, + "iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_abc", + "cognito:username": "user1", + "exp": 1551228178628, + "iat": 1551228178629 + }, + "sourceIp": ["192.168.196.186", "193.168.196.186"], + "defaultAuthStrategy": "ALLOW" +} + diff --git a/lambda-events/src/fixtures/example-appsync-identity-iam.json b/lambda-events/src/fixtures/example-appsync-identity-iam.json new file mode 100644 index 000000000..ffeaa707c --- /dev/null +++ b/lambda-events/src/fixtures/example-appsync-identity-iam.json @@ -0,0 +1,11 @@ +{ + "accountId": "accountid123", + "cognitoIdentityPoolId": "identitypoolid123", + "cognitoIdentityId": "identityid123", + "cognitoIdentityAuthType": "authenticated", + "cognitoIdentityAuthProvider": "providerABC", + "sourceIp": ["192.168.196.186", "193.168.196.186"], + "username": "user1", + "userArn": "arn:aws:iam::123456789012:user/appsync" +} + diff --git a/lambda-events/src/fixtures/example-appsync-invoke.json b/lambda-events/src/fixtures/example-appsync-invoke.json new file mode 100644 index 000000000..7943e0825 --- /dev/null +++ b/lambda-events/src/fixtures/example-appsync-invoke.json @@ -0,0 +1,14 @@ +{ + "version": "2017-02-28", + "operation": "Invoke", + "payload": { + "field": "getPost", + "arguments": { + "id": "postId1", + "count": 1, + "float": 1.2, + "flag": true + } + } +} + diff --git a/lambda-events/src/fixtures/example-appsync-lambda-auth-request.json b/lambda-events/src/fixtures/example-appsync-lambda-auth-request.json new file mode 100644 index 000000000..bcc28672b --- /dev/null +++ b/lambda-events/src/fixtures/example-appsync-lambda-auth-request.json @@ -0,0 +1,12 @@ +{ + "authorizationToken": "ExampleAUTHtoken123123123", + "requestContext": { + "apiId": "aaaaaa123123123example123", + "accountId": "111122223333", + "requestId": "f4081827-1111-4444-5555-5cf4695f339f", + "queryString": "mutation CreateEvent {...}\n\nquery MyQuery {...}\n", + "operationName": "MyQuery", + "variables": {} + } +} + diff --git a/lambda-events/src/fixtures/example-appsync-lambda-auth-response.json b/lambda-events/src/fixtures/example-appsync-lambda-auth-response.json new file mode 100644 index 000000000..461c4a4d9 --- /dev/null +++ b/lambda-events/src/fixtures/example-appsync-lambda-auth-response.json @@ -0,0 +1,8 @@ +{ + "isAuthorized": true, + "resolverContext": { + "banana": "very yellow", + "apple": "very green" + } +} + diff --git a/lambda-events/src/fixtures/example-autoscaling-event-launch-successful.json b/lambda-events/src/fixtures/example-autoscaling-event-launch-successful.json new file mode 100644 index 000000000..d144c10b7 --- /dev/null +++ b/lambda-events/src/fixtures/example-autoscaling-event-launch-successful.json @@ -0,0 +1,29 @@ +{ + "version": "0", + "id": "12345678-1234-1234-1234-123456789012", + "detail-type": "EC2 Instance Launch Successful", + "source": "aws.autoscaling", + "account": "123456789012", + "time": "1970-01-01T00:00:00Z", + "region": "us-west-2", + "resources": [ + "auto-scaling-group-arn", + "instance-arn" + ], + "detail": { + "StatusCode": "InProgress", + "Description": "Launching a new EC2 instance: i-12345678", + "AutoScalingGroupName": "my-auto-scaling-group", + "ActivityId": "87654321-4321-4321-4321-210987654321", + "Details": { + "Availability Zone": "us-west-2b", + "Subnet ID": "subnet-12345678" + }, + "RequestId": "12345678-1234-1234-1234-123456789012", + "StatusMessage": "", + "EndTime": "1970-01-01T00:00:00Z", + "EC2InstanceId": "i-1234567890abcdef0", + "StartTime": "1970-01-01T00:00:00Z", + "Cause": "description-text" + } + } diff --git a/lambda-events/src/fixtures/example-autoscaling-event-launch-unsuccessful.json b/lambda-events/src/fixtures/example-autoscaling-event-launch-unsuccessful.json new file mode 100644 index 000000000..e446d90d7 --- /dev/null +++ b/lambda-events/src/fixtures/example-autoscaling-event-launch-unsuccessful.json @@ -0,0 +1,28 @@ +{ + "version": "0", + "id": "12345678-1234-1234-1234-123456789012", + "detail-type": "EC2 Instance Launch Unsuccessful", + "source": "aws.autoscaling", + "account": "123456789012", + "time": "1970-01-01T00:00:00Z", + "region": "us-west-2", + "resources": [ + "auto-scaling-group-arn", + "instance-arn" + ], + "detail": { + "StatusCode": "Failed", + "AutoScalingGroupName": "my-auto-scaling-group", + "ActivityId": "87654321-4321-4321-4321-210987654321", + "Details": { + "Availability Zone": "us-west-2b", + "Subnet ID": "subnet-12345678" + }, + "RequestId": "12345678-1234-1234-1234-123456789012", + "StatusMessage": "message-text", + "EndTime": "1970-01-01T00:00:00Z", + "EC2InstanceId": "i-1234567890abcdef0", + "StartTime": "1970-01-01T00:00:00Z", + "Cause": "description-text" + } + } diff --git a/lambda-events/src/fixtures/example-autoscaling-event-lifecycle-action.json b/lambda-events/src/fixtures/example-autoscaling-event-lifecycle-action.json new file mode 100644 index 000000000..cd19446aa --- /dev/null +++ b/lambda-events/src/fixtures/example-autoscaling-event-lifecycle-action.json @@ -0,0 +1,21 @@ +{ + "version": "0", + "id": "12345678-1234-1234-1234-123456789012", + "detail-type": "EC2 Instance-launch Lifecycle Action", + "source": "aws.autoscaling", + "account": "123456789012", + "time": "1970-01-01T00:00:00Z", + "region": "us-west-2", + "resources": [ + "auto-scaling-group-arn" + ], + "detail": { + "LifecycleActionToken": "87654321-4321-4321-4321-210987654321", + "AutoScalingGroupName": "my-asg", + "LifecycleHookName": "my-lifecycle-hook", + "EC2InstanceId": "i-1234567890abcdef0", + "LifecycleTransition": "autoscaling:EC2_INSTANCE_LAUNCHING", + "NotificationMetadata": "additional-info" + } + } + diff --git a/lambda-events/src/fixtures/example-autoscaling-event-terminate-action.json b/lambda-events/src/fixtures/example-autoscaling-event-terminate-action.json new file mode 100644 index 000000000..7afd13c70 --- /dev/null +++ b/lambda-events/src/fixtures/example-autoscaling-event-terminate-action.json @@ -0,0 +1,19 @@ +{ + "version": "0", + "id": "12345678-1234-1234-1234-123456789012", + "detail-type": "EC2 Instance-terminate Lifecycle Action", + "source": "aws.autoscaling", + "account": "123456789012", + "time": "1970-01-01T00:00:00Z", + "region": "us-west-2", + "resources": [ + "auto-scaling-group-arn" + ], + "detail": { + "LifecycleActionToken":"87654321-4321-4321-4321-210987654321", + "AutoScalingGroupName":"my-asg", + "LifecycleHookName":"my-lifecycle-hook", + "EC2InstanceId":"i-1234567890abcdef0", + "LifecycleTransition":"autoscaling:EC2_INSTANCE_TERMINATING" + } + } diff --git a/lambda-events/src/fixtures/example-autoscaling-event-terminate-successful.json b/lambda-events/src/fixtures/example-autoscaling-event-terminate-successful.json new file mode 100644 index 000000000..cf6d1b0a0 --- /dev/null +++ b/lambda-events/src/fixtures/example-autoscaling-event-terminate-successful.json @@ -0,0 +1,29 @@ +{ + "version": "0", + "id": "12345678-1234-1234-1234-123456789012", + "detail-type": "EC2 Instance Terminate Successful", + "source": "aws.autoscaling", + "account": "123456789012", + "time": "1970-01-01T00:00:00Z", + "region": "us-west-2", + "resources": [ + "auto-scaling-group-arn", + "instance-arn" + ], + "detail": { + "StatusCode": "InProgress", + "Description": "Terminating EC2 instance: i-12345678", + "AutoScalingGroupName": "my-auto-scaling-group", + "ActivityId": "87654321-4321-4321-4321-210987654321", + "Details": { + "Availability Zone": "us-west-2b", + "Subnet ID": "subnet-12345678" + }, + "RequestId": "12345678-1234-1234-1234-123456789012", + "StatusMessage": "", + "EndTime": "1970-01-01T00:00:00Z", + "EC2InstanceId": "i-1234567890abcdef0", + "StartTime": "1970-01-01T00:00:00Z", + "Cause": "description-text" + } + } diff --git a/lambda-events/src/fixtures/example-autoscaling-event-terminate-unsuccessful.json b/lambda-events/src/fixtures/example-autoscaling-event-terminate-unsuccessful.json new file mode 100644 index 000000000..ff973d97c --- /dev/null +++ b/lambda-events/src/fixtures/example-autoscaling-event-terminate-unsuccessful.json @@ -0,0 +1,28 @@ +{ + "version": "0", + "id": "12345678-1234-1234-1234-123456789012", + "detail-type": "EC2 Instance Terminate Unsuccessful", + "source": "aws.autoscaling", + "account": "123456789012", + "time": "1970-01-01T00:00:00Z", + "region": "us-west-2", + "resources": [ + "auto-scaling-group-arn", + "instance-arn" + ], + "detail": { + "StatusCode": "Failed", + "AutoScalingGroupName": "my-auto-scaling-group", + "ActivityId": "87654321-4321-4321-4321-210987654321", + "Details": { + "Availability Zone": "us-west-2b", + "Subnet ID": "subnet-12345678" + }, + "RequestId": "12345678-1234-1234-1234-123456789012", + "StatusMessage": "message-text", + "EndTime": "1970-01-01T00:00:00Z", + "EC2InstanceId": "i-1234567890abcdef0", + "StartTime": "1970-01-01T00:00:00Z", + "Cause": "description-text" + } + } diff --git a/lambda-events/src/fixtures/example-bedrock-agent-runtime-event-without-parameters.json b/lambda-events/src/fixtures/example-bedrock-agent-runtime-event-without-parameters.json new file mode 100644 index 000000000..d41d9ee1c --- /dev/null +++ b/lambda-events/src/fixtures/example-bedrock-agent-runtime-event-without-parameters.json @@ -0,0 +1,33 @@ +{ + "messageVersion": "1.0", + "agent": { + "name": "AgentName", + "id": "AgentID", + "alias": "AgentAlias", + "version": "AgentVersion" + }, + "inputText": "InputText", + "sessionId": "SessionID", + "actionGroup": "ActionGroup", + "apiPath": "/api/path", + "httpMethod": "POST", + "requestBody": { + "content": { + "application/json": { + "properties": [ + { + "name": "prop1", + "type": "string", + "value": "value1" + } + ] + } + } + }, + "sessionAttributes": { + "attr1": "value1" + }, + "promptSessionAttributes": { + "promptAttr1": "value1" + } +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-bedrock-agent-runtime-event-without-request-body.json b/lambda-events/src/fixtures/example-bedrock-agent-runtime-event-without-request-body.json new file mode 100644 index 000000000..2ca5aa9a9 --- /dev/null +++ b/lambda-events/src/fixtures/example-bedrock-agent-runtime-event-without-request-body.json @@ -0,0 +1,27 @@ +{ + "messageVersion": "1.0", + "agent": { + "name": "AgentName", + "id": "AgentID", + "alias": "AgentAlias", + "version": "AgentVersion" + }, + "inputText": "InputText", + "sessionId": "SessionID", + "actionGroup": "ActionGroup", + "apiPath": "/api/path", + "httpMethod": "POST", + "parameters": [ + { + "name": "param1", + "type": "string", + "value": "value1" + } + ], + "sessionAttributes": { + "attr1": "value1" + }, + "promptSessionAttributes": { + "promptAttr1": "value1" + } +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-bedrock-agent-runtime-event.json b/lambda-events/src/fixtures/example-bedrock-agent-runtime-event.json new file mode 100644 index 000000000..b5244515d --- /dev/null +++ b/lambda-events/src/fixtures/example-bedrock-agent-runtime-event.json @@ -0,0 +1,40 @@ +{ + "messageVersion": "1.0", + "agent": { + "name": "AgentName", + "id": "AgentID", + "alias": "AgentAlias", + "version": "AgentVersion" + }, + "inputText": "InputText", + "sessionId": "SessionID", + "actionGroup": "ActionGroup", + "apiPath": "/api/path", + "httpMethod": "POST", + "parameters": [ + { + "name": "param1", + "type": "string", + "value": "value1" + } + ], + "requestBody": { + "content": { + "application/json": { + "properties": [ + { + "name": "prop1", + "type": "string", + "value": "value1" + } + ] + } + } + }, + "sessionAttributes": { + "attr1": "value1" + }, + "promptSessionAttributes": { + "promptAttr1": "value1" + } +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-clientvpn-connectionhandler-request.json b/lambda-events/src/fixtures/example-clientvpn-connectionhandler-request.json new file mode 100644 index 000000000..92d59ee3b --- /dev/null +++ b/lambda-events/src/fixtures/example-clientvpn-connectionhandler-request.json @@ -0,0 +1,12 @@ +{ + "connection-id": "cvpn-connection-04e7e1b2f0daf9460", + "endpoint-id": "cvpn-endpoint-0f13eab7f860433cc", + "common-name": "", + "username": "username", + "platform": "", + "platform-version": "", + "public-ip": "10.11.12.13", + "client-openvpn-version": "", + "schema-version": "v1" +} + diff --git a/lambda-events/src/fixtures/example-cloudformation-custom-resource-create-request.json b/lambda-events/src/fixtures/example-cloudformation-custom-resource-create-request.json new file mode 100644 index 000000000..d35dd6f7e --- /dev/null +++ b/lambda-events/src/fixtures/example-cloudformation-custom-resource-create-request.json @@ -0,0 +1,14 @@ +{ + "ServiceToken": "arn:aws:lambda:eu-west-2:123456789012:function:custom-resource-handler", + "RequestType" : "Create", + "RequestId" : "82304eb2-bdda-469f-a33b-a3f1406d0a52", + "ResponseURL": "https://cr-response-bucket.s3.us-east-1.amazonaws.com/cr-response-key?sig-params=sig-values", + "StackId" : "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-name/16580499-7622-4a9c-b32f-4eba35da93da", + "ResourceType" : "Custom::MyCustomResourceType", + "LogicalResourceId" : "CustomResource", + "ResourceProperties" : { + "Key1" : "string", + "Key2" : [ "list" ], + "Key3" : { "Key4" : "map" } + } +} diff --git a/lambda-events/src/fixtures/example-cloudformation-custom-resource-delete-request.json b/lambda-events/src/fixtures/example-cloudformation-custom-resource-delete-request.json new file mode 100644 index 000000000..bd788c99e --- /dev/null +++ b/lambda-events/src/fixtures/example-cloudformation-custom-resource-delete-request.json @@ -0,0 +1,15 @@ +{ + "ServiceToken": "arn:aws:lambda:eu-west-2:123456789012:function:custom-resource-handler", + "RequestType" : "Delete", + "RequestId" : "ef70561d-d4ba-42a4-801b-33ad88dafc37", + "ResponseURL": "https://cr-response-bucket.s3.us-east-1.amazonaws.com/cr-response-key?sig-params=sig-values", + "StackId" : "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-name/16580499-7622-4a9c-b32f-4eba35da93da", + "ResourceType" : "Custom::MyCustomResourceType", + "LogicalResourceId" : "CustomResource", + "PhysicalResourceId" : "custom-resource-f4bd5382-3de3-4caf-b7ad-1be06b899647", + "ResourceProperties" : { + "Key1" : "string", + "Key2" : [ "list" ], + "Key3" : { "Key4" : "map" } + } +} diff --git a/lambda-events/src/fixtures/example-cloudformation-custom-resource-provider-create-request.json b/lambda-events/src/fixtures/example-cloudformation-custom-resource-provider-create-request.json new file mode 100644 index 000000000..074a3712f --- /dev/null +++ b/lambda-events/src/fixtures/example-cloudformation-custom-resource-provider-create-request.json @@ -0,0 +1,12 @@ +{ + "RequestType" : "Create", + "RequestId" : "82304eb2-bdda-469f-a33b-a3f1406d0a52", + "StackId" : "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-name/16580499-7622-4a9c-b32f-4eba35da93da", + "ResourceType" : "Custom::MyCustomResourceType", + "LogicalResourceId" : "CustomResource", + "ResourceProperties" : { + "Key1" : "string", + "Key2" : [ "list" ], + "Key3" : { "Key4" : "map" } + } +} diff --git a/lambda-events/src/fixtures/example-cloudformation-custom-resource-provider-delete-request.json b/lambda-events/src/fixtures/example-cloudformation-custom-resource-provider-delete-request.json new file mode 100644 index 000000000..3fb58391b --- /dev/null +++ b/lambda-events/src/fixtures/example-cloudformation-custom-resource-provider-delete-request.json @@ -0,0 +1,13 @@ +{ + "RequestType" : "Delete", + "RequestId" : "ef70561d-d4ba-42a4-801b-33ad88dafc37", + "StackId" : "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-name/16580499-7622-4a9c-b32f-4eba35da93da", + "ResourceType" : "Custom::MyCustomResourceType", + "LogicalResourceId" : "CustomResource", + "PhysicalResourceId" : "custom-resource-f4bd5382-3de3-4caf-b7ad-1be06b899647", + "ResourceProperties" : { + "Key1" : "string", + "Key2" : [ "list" ], + "Key3" : { "Key4" : "map" } + } +} diff --git a/lambda-events/src/fixtures/example-cloudformation-custom-resource-provider-response.json b/lambda-events/src/fixtures/example-cloudformation-custom-resource-provider-response.json new file mode 100644 index 000000000..cff06191f --- /dev/null +++ b/lambda-events/src/fixtures/example-cloudformation-custom-resource-provider-response.json @@ -0,0 +1,9 @@ +{ + "PhysicalResourceId": "custom-resource-f4bd5382-3de3-4caf-b7ad-1be06b899647", + "NoEcho": false, + "Data": { + "Key1": "a", + "Key2": "b", + "Key3": "c" + } +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-cloudformation-custom-resource-provider-update-request.json b/lambda-events/src/fixtures/example-cloudformation-custom-resource-provider-update-request.json new file mode 100644 index 000000000..90280f72e --- /dev/null +++ b/lambda-events/src/fixtures/example-cloudformation-custom-resource-provider-update-request.json @@ -0,0 +1,18 @@ +{ + "RequestType" : "Update", + "RequestId" : "49347ca5-c603-44e5-a34b-10cf1854a887", + "StackId" : "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-name/16580499-7622-4a9c-b32f-4eba35da93da", + "ResourceType" : "Custom::MyCustomResourceType", + "LogicalResourceId" : "CustomResource", + "PhysicalResourceId" : "custom-resource-f4bd5382-3de3-4caf-b7ad-1be06b899647", + "ResourceProperties" : { + "Key1" : "new-string", + "Key2" : [ "new-list" ], + "Key3" : { "Key4" : "new-map" } + }, + "OldResourceProperties" : { + "Key1" : "string", + "Key2" : [ "list" ], + "Key3" : { "Key4" : "map" } + } +} diff --git a/lambda-events/src/fixtures/example-cloudformation-custom-resource-response.json b/lambda-events/src/fixtures/example-cloudformation-custom-resource-response.json new file mode 100644 index 000000000..86d018ad9 --- /dev/null +++ b/lambda-events/src/fixtures/example-cloudformation-custom-resource-response.json @@ -0,0 +1,14 @@ +{ + "Status": "FAILED", + "Reason": "This is a test failure.", + "PhysicalResourceId": "custom-resource-f4bd5382-3de3-4caf-b7ad-1be06b899647", + "StackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-name/16580499-7622-4a9c-b32f-4eba35da93da", + "RequestId": "49347ca5-c603-44e5-a34b-10cf1854a887", + "LogicalResourceId": "CustomResource", + "NoEcho": false, + "Data": { + "Key1": "a", + "Key2": "b", + "Key3": "c" + } +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-cloudformation-custom-resource-update-request.json b/lambda-events/src/fixtures/example-cloudformation-custom-resource-update-request.json new file mode 100644 index 000000000..4fc4378a3 --- /dev/null +++ b/lambda-events/src/fixtures/example-cloudformation-custom-resource-update-request.json @@ -0,0 +1,20 @@ +{ + "ServiceToken": "arn:aws:lambda:eu-west-2:123456789012:function:custom-resource-handler", + "RequestType" : "Update", + "RequestId" : "49347ca5-c603-44e5-a34b-10cf1854a887", + "ResponseURL": "https://cr-response-bucket.s3.us-east-1.amazonaws.com/cr-response-key?sig-params=sig-values", + "StackId" : "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-name/16580499-7622-4a9c-b32f-4eba35da93da", + "ResourceType" : "Custom::MyCustomResourceType", + "LogicalResourceId" : "CustomResource", + "PhysicalResourceId" : "custom-resource-f4bd5382-3de3-4caf-b7ad-1be06b899647", + "ResourceProperties" : { + "Key1" : "new-string", + "Key2" : [ "new-list" ], + "Key3" : { "Key4" : "new-map" } + }, + "OldResourceProperties" : { + "Key1" : "string", + "Key2" : [ "list" ], + "Key3" : { "Key4" : "map" } + } +} diff --git a/lambda-events/src/fixtures/example-cloudwatch-alarm-composite-with-suppressor-alarm.json b/lambda-events/src/fixtures/example-cloudwatch-alarm-composite-with-suppressor-alarm.json new file mode 100644 index 000000000..26ce1c9bc --- /dev/null +++ b/lambda-events/src/fixtures/example-cloudwatch-alarm-composite-with-suppressor-alarm.json @@ -0,0 +1,35 @@ +{ + "version": "0", + "id": "d3dfc86d-384d-24c8-0345-9f7986db0b80", + "detail-type": "CloudWatch Alarm State Change", + "source": "aws.cloudwatch", + "account": "123456789012", + "time": "2022-07-22T15:57:45Z", + "region": "us-east-1", + "resources": [ + "arn:aws:cloudwatch:us-east-1:123456789012:alarm:ServiceAggregatedAlarm" + ], + "alarmData": { + "alarmName": "ServiceAggregatedAlarm", + "state": { + "actionsSuppressedBy": "WaitPeriod", + "actionsSuppressedReason": "Actions suppressed by WaitPeriod", + "value": "ALARM", + "reason": "arn:aws:cloudwatch:us-east-1:123456789012:alarm:SuppressionDemo.EventBridge.FirstChild transitioned to ALARM at Friday 22 July, 2022 15:57:45 UTC", + "reasonData": "{\"triggeringAlarms\":[{\"arn\":\"arn:aws:cloudwatch:us-east-1:123456789012:alarm:ServerCpuTooHigh\",\"state\":{\"value\":\"ALARM\",\"timestamp\":\"2022-07-22T15:57:45.394+0000\"}}]}", + "timestamp": "2022-07-22T15:57:45.394+0000" + }, + "previousState": { + "value": "OK", + "reason": "arn:aws:cloudwatch:us-east-1:123456789012:alarm:SuppressionDemo.EventBridge.Main was created and its alarm rule evaluates to OK", + "reasonData": "{\"triggeringAlarms\":[{\"arn\":\"arn:aws:cloudwatch:us-east-1:123456789012:alarm:TotalNetworkTrafficTooHigh\",\"state\":{\"value\":\"OK\",\"timestamp\":\"2022-07-14T16:28:57.770+0000\"}},{\"arn\":\"arn:aws:cloudwatch:us-east-1:123456789012:alarm:ServerCpuTooHigh\",\"state\":{\"value\":\"OK\",\"timestamp\":\"2022-07-14T16:28:54.191+0000\"}}]}", + "timestamp": "2022-07-22T15:56:14.552+0000" + }, + "configuration": { + "alarmRule": "ALARM(ServerCpuTooHigh) OR ALARM(TotalNetworkTrafficTooHigh)", + "actionsSuppressor": "ServiceMaintenanceAlarm", + "actionsSuppressorWaitPeriod": 120, + "actionsSuppressorExtensionPeriod": 180 + } + } +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-cloudwatch-alarm-composite.json b/lambda-events/src/fixtures/example-cloudwatch-alarm-composite.json new file mode 100644 index 000000000..f74b8f08a --- /dev/null +++ b/lambda-events/src/fixtures/example-cloudwatch-alarm-composite.json @@ -0,0 +1,30 @@ +{ + "source": "aws.cloudwatch", + "alarmArn": "arn:aws:cloudwatch:us-east-1: 111122223333:alarm:SuppressionDemo.Main", + "accountId": "111122223333", + "time": "2023-08-04T12: 56: 46.138+0000", + "region": "us-east-1", + "alarmData": { + "alarmName": "CompositeDemo.Main", + "state": { + "value": "ALARM", + "reason": "arn:aws:cloudwatch:us-east-1: 111122223333:alarm:CompositeDemo.FirstChild transitioned to ALARM at Friday 04 August, 2023 12: 54: 46 UTC", + "reasonData": "{\"triggeringAlarms\": [{\"arn\": \"arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild\",\"state\": {\"value\": \"ALARM\",\"timestamp\": \"2023-08-04T12:54:46.138+0000\"}}]}", + "timestamp": "2023-08-04T12: 56: 46.138+0000" + }, + "previousState": { + "value": "ALARM", + "reason": "arn:aws:cloudwatch:us-east-1: 111122223333:alarm:CompositeDemo.FirstChild transitioned to ALARM at Friday 04 August,2023 12: 54: 46 UTC", + "reasonData": "{\"triggeringAlarms\": [{\"arn\": \"arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild\",\"state\": {\"value\": \"ALARM\",\"timestamp\": \"2023-08-04T12:54:46.138+0000\"}}]}", + "timestamp": "2023-08-04T12: 54: 46.138+0000", + "actionsSuppressedBy": "WaitPeriod", + "actionsSuppressedReason": "Actions suppressed by WaitPeriod" + }, + "configuration": { + "alarmRule": "ALARM(CompositeDemo.FirstChild) OR ALARM(CompositeDemo.SecondChild)", + "actionsSuppressor": "CompositeDemo.ActionsSuppressor", + "actionsSuppressorWaitPeriod": 120, + "actionsSuppressorExtensionPeriod": 180 + } + } +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-cloudwatch-alarm-metric.json b/lambda-events/src/fixtures/example-cloudwatch-alarm-metric.json new file mode 100644 index 000000000..04a17649e --- /dev/null +++ b/lambda-events/src/fixtures/example-cloudwatch-alarm-metric.json @@ -0,0 +1,42 @@ +{ + "source": "aws.cloudwatch", + "alarmArn": "arn:aws:cloudwatch:us-east-1: 444455556666:alarm:lambda-demo-metric-alarm", + "accountId": "444455556666", + "time": "2023-08-04T12: 36: 15.490+0000", + "region": "us-east-1", + "alarmData": { + "alarmName": "lambda-demo-metric-alarm", + "state": { + "value": "ALARM", + "reason": "test", + "timestamp": "2023-08-04T12: 36: 15.490+0000" + }, + "previousState": { + "value": "INSUFFICIENT_DATA", + "reason": "Insufficient Data: 5 datapoints were unknown.", + "reasonData": "{\"version\": \"1.0\", \"queryDate\": \"2023-08-04T12: 31: 29.591+0000\", \"statistic\": \"Average\", \"period\": 60, \"recentDatapoints\": [], \"threshold\": 5.0, \"evaluatedDatapoints\": [{\"timestamp\": \"2023-08-04T12: 30: 00.000+0000\"},{\"timestamp\": \"2023-08-04T12: 29: 00.000+0000\"},{\"timestamp\": \"2023-08-04T12: 28: 00.000+0000\"},{\"timestamp\": \"2023-08-04T12: 27: 00.000+0000\"},{\"timestamp\": \"2023-08-04T12: 26: 00.000+0000\"}]}", + "timestamp": "2023-08-04T12: 31: 29.595+0000" + }, + "configuration": { + "description": "Metric Alarm to test Lambda actions", + "metrics": [ + { + "id": "1234e046-06f0-a3da-9534-EXAMPLEe4c", + "metricStat": { + "metric": { + "namespace": "AWS/Logs", + "name": "CallCount", + "dimensions": { + "InstanceId": "i-12345678" + } + }, + "period": 60, + "stat": "Average", + "unit": "Percent" + }, + "returnData": true + } + ] + } + } +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-cloudwatch-alarm-sns-payload-multiple-metrics.json b/lambda-events/src/fixtures/example-cloudwatch-alarm-sns-payload-multiple-metrics.json new file mode 100644 index 000000000..94a10f963 --- /dev/null +++ b/lambda-events/src/fixtures/example-cloudwatch-alarm-sns-payload-multiple-metrics.json @@ -0,0 +1,40 @@ +{ + "Records": [ + { + "EventSource": "aws:sns", + "EventVersion": "1.0", + "EventSubscriptionArn": "arn:aws:sns:EXAMPLE", + "Sns": { + "Type": "Notification", + "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e", + "TopicArn": "arn:aws:sns:EXAMPLE", + "Subject": "TestInvoke", + "Message": "{\"AlarmName\":\"EXAMPLE\",\"AlarmDescription\":\"EXAMPLE\",\"AWSAccountId\":\"EXAMPLE\",\"NewStateValue\":\"ALARM\",\"NewStateReason\":\"Threshold Crossed: 1 out of the last 1 datapoints [1234.0 (06/03/15 17:43:27)] was greater than the threshold (0.0) (minimum 1 datapoint for OK -> ALARM transition).\",\"StateChangeTime\":\"2015-06-03T17:43:27.123+0000\",\"Region\":\"EXAMPLE\",\"AlarmArn\":\"arn:aws:cloudwatch:REGION:ACCOUNT_NUMBER:alarm:EXAMPLE\",\"OldStateValue\":\"INSUFFICIENT_DATA\",\"Trigger\":{\"Period\":60,\"EvaluationPeriods\":1,\"ComparisonOperator\":\"GreaterThanThreshold\",\"Threshold\":0.0,\"TreatMissingData\":\"- TreatMissingData: missing\",\"EvaluateLowSampleCountPercentile\":\"\",\"Metrics\":[{\"Expression\":\"m1*1\",\"Id\":\"e1\",\"Label\":\"Expression1\",\"ReturnData\":true},{\"Id\":\"m1\",\"MetricStat\":{\"Metric\":{\"Dimensions\":[{\"value\":\"TestInstance\",\"name\":\"InstanceId\"}],\"MetricName\":\"NetworkOut\",\"Namespace\":\"AWS/EC2\"},\"Period\":60,\"Stat\":\"Average\"},\"ReturnData\":false}]}}", + "Timestamp": "2015-06-03T17:43:27.123Z", + "SignatureVersion": "1", + "Signature": "EXAMPLE", + "SigningCertURL": "EXAMPLE", + "UnsubscribeURL": "EXAMPLE", + "MessageAttributes": {} + } + }, + { + "EventSource": "aws:sns", + "EventVersion": "1.0", + "EventSubscriptionArn": "arn:aws:sns:EXAMPLE", + "Sns": { + "Type": "Notification", + "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e", + "TopicArn": "arn:aws:sns:EXAMPLE", + "Subject": "TestInvoke", + "Message": "{\"AlarmName\":\"EXAMPLE\",\"AlarmDescription\":\"EXAMPLE\",\"AWSAccountId\":\"EXAMPLE\",\"NewStateValue\":\"ALARM\",\"NewStateReason\":\"Threshold Crossed: 1 out of the last 1 datapoints [1234.0 (06/03/15 17:43:27)] was greater than the threshold (0.0) (minimum 1 datapoint for OK -> ALARM transition).\",\"StateChangeTime\":\"2015-06-03T17:43:27.123+0000\",\"Region\":\"EXAMPLE\",\"AlarmArn\":\"arn:aws:cloudwatch:REGION:ACCOUNT_NUMBER:alarm:EXAMPLE\",\"OldStateValue\":\"INSUFFICIENT_DATA\",\"Trigger\":{\"Period\":60,\"EvaluationPeriods\":1,\"ComparisonOperator\":\"GreaterThanThreshold\",\"Threshold\":0.0,\"TreatMissingData\":\"- TreatMissingData: missing\",\"EvaluateLowSampleCountPercentile\":\"\",\"Metrics\":[{\"Expression\":\"m1*1\",\"Id\":\"e1\",\"Label\":\"Expression1\",\"ReturnData\":true},{\"Id\":\"m1\",\"MetricStat\":{\"Metric\":{\"Dimensions\":[{\"value\":\"TestInstance\",\"name\":\"InstanceId\"}],\"MetricName\":\"NetworkOut\",\"Namespace\":\"AWS/EC2\"},\"Period\":60,\"Stat\":\"Average\"},\"ReturnData\":false}]}}", + "Timestamp": "2015-06-03T17:43:27.123Z", + "SignatureVersion": "1", + "Signature": "EXAMPLE", + "SigningCertURL": "EXAMPLE", + "UnsubscribeURL": "EXAMPLE", + "MessageAttributes": {} + } + } + ] +} diff --git a/lambda-events/src/fixtures/example-cloudwatch-alarm-sns-payload-single-metric.json b/lambda-events/src/fixtures/example-cloudwatch-alarm-sns-payload-single-metric.json new file mode 100644 index 000000000..8f58b7973 --- /dev/null +++ b/lambda-events/src/fixtures/example-cloudwatch-alarm-sns-payload-single-metric.json @@ -0,0 +1,22 @@ +{ + "Records": [ + { + "EventSource": "aws:sns", + "EventVersion": "1.0", + "EventSubscriptionArn": "arn:aws:sns:EXAMPLE", + "Sns": { + "Type": "Notification", + "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e", + "TopicArn": "arn:aws:sns:EXAMPLE", + "Subject": "TestInvoke", + "Message": "{\"AlarmName\": \"EXAMPLE\",\"AlarmDescription\": \"EXAMPLE\",\"AWSAccountId\": \"123456789012\",\"NewStateValue\": \"ALARM\",\"NewStateReason\": \"Threshold Crossed: 1 out of the last 1 datapoints [1234.0 (06/03/15 17:43:27)] was greater than the threshold (0.0) (minimum 1 datapoint for OK -> ALARM transition).\",\"StateChangeTime\": \"2015-06-03T17:43:27.123+0000\",\"Region\": \"EXAMPLE\",\"AlarmArn\": \"arn:aws:cloudwatch:REGION:ACCOUNT_NUMBER:alarm:EXAMPLE\",\"OldStateValue\": \"INSUFFICIENT_DATA\",\"Trigger\": {\"MetricName\": \"NetworkOut\",\"Namespace\": \"AWS/EC2\",\"StatisticType\": \"Statistic\",\"Statistic\": \"AVERAGE\",\"Unit\": \"Bytes\",\"Dimensions\": [{\"value\": \"TestInstance\",\"name\": \"InstanceId\"}],\"Period\": 60,\"EvaluationPeriods\": 1,\"ComparisonOperator\": \"GreaterThanThreshold\",\"Threshold\": 0.0,\"TreatMissingData\": \"- TreatMissingData: missing\",\"EvaluateLowSampleCountPercentile\": \"\"}}", + "Timestamp": "2015-06-03T17:43:27.123Z", + "SignatureVersion": "1", + "Signature": "EXAMPLE", + "SigningCertURL": "EXAMPLE", + "UnsubscribeURL": "EXAMPLE", + "MessageAttributes": {} + } + } + ] +} diff --git a/lambda-events/src/fixtures/example-cloudwatch-cloudtrail-assumed-role.json b/lambda-events/src/fixtures/example-cloudwatch-cloudtrail-assumed-role.json new file mode 100644 index 000000000..6e8946e9e --- /dev/null +++ b/lambda-events/src/fixtures/example-cloudwatch-cloudtrail-assumed-role.json @@ -0,0 +1,52 @@ +{ + "eventVersion": "1.08", + "userIdentity": { + "type": "AssumedRole", + "principalId": "ZZZZZZZZZZZZZZZZZZZZZ:me@dev.com", + "arn": "arn:aws:sts::123456789000:assumed-role/AWSReservedSSO_AWSAdministratorAccess_abcdef1234567890/me@dev.com", + "accountId": "123456789000", + "accessKeyId": "ABCDEFGHI12345678890", + "sessionContext": { + "sessionIssuer": { + "type": "Role", + "principalId": "ZZZZZZZZZZZZZZZZZZZZZ", + "arn": "arn:aws:iam::123456789000:role/aws-reserved/sso.amazonaws.com/eu-west-1/AWSReservedSSO_AWSAdministratorAccess_abcdef1234567890", + "accountId": "123456789000", + "userName": "AWSReservedSSO_AWSAdministratorAccess_abcdef1234567890" + }, + "webIdFederationData": {}, + "attributes": { + "creationDate": "2024-07-10T16:03:25Z", + "mfaAuthenticated": "false" + } + }, + "invokedBy": "servicecatalog.amazonaws.com" + }, + "eventTime": "2024-07-10T16:48:26Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "CreateManagedAccount", + "awsRegion": "eu-west-1", + "sourceIPAddress": "servicecatalog.amazonaws.com", + "userAgent": "servicecatalog.amazonaws.com", + "requestParameters": { + "accountEmail": "HIDDEN_DUE_TO_SECURITY_REASONS", + "accountName": "Account Name", + "parentOrganizationalUnitName": "Organizational Unit (ou-a1b2-abcdef12)", + "sSOUserEmail": "HIDDEN_DUE_TO_SECURITY_REASONS", + "sSOUserFirstName": "HIDDEN_DUE_TO_SECURITY_REASONS", + "sSOUserLastName": "HIDDEN_DUE_TO_SECURITY_REASONS", + "provisionedProductId": "pp-abcdefg123456", + "idempotencyToken": "abcdef12345-abcdef12345" + }, + "responseElements": { + "createManagedAccountExecutionId": "123456789000-abcdef12345-abcdef12345" + }, + "requestID": "00000000-0000-0000-0000-000000000000", + "eventID": "00000000-0000-0000-0000-000000000000", + "readOnly": false, + "eventType": "AwsApiCall", + "managementEvent": true, + "recipientAccountId": "123456789000", + "eventCategory": "Management", + "sessionCredentialFromConsole": "true" +} diff --git a/lambda-events/src/fixtures/example-cloudwatch-cloudtrail-unknown-federate.json b/lambda-events/src/fixtures/example-cloudwatch-cloudtrail-unknown-federate.json new file mode 100644 index 000000000..336edc1fc --- /dev/null +++ b/lambda-events/src/fixtures/example-cloudwatch-cloudtrail-unknown-federate.json @@ -0,0 +1,27 @@ +{ + "eventVersion": "1.08", + "userIdentity": { + "type": "Unknown", + "principalId": "00000000-0000-0000-0000-000000000000", + "accountId": "123456789000", + "userName": "me@dev.com" + }, + "eventTime": "2024-07-10T18:41:56Z", + "eventSource": "sso.amazonaws.com", + "eventName": "Federate", + "awsRegion": "eu-west-1", + "sourceIPAddress": "1.1.1.1", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36", + "requestParameters": null, + "responseElements": null, + "requestID": "00000000-0000-0000-0000-000000000000", + "eventID": "00000000-0000-0000-0000-000000000000", + "readOnly": false, + "eventType": "AwsServiceEvent", + "managementEvent": true, + "recipientAccountId": "123456789000", + "serviceEventDetails": { + "relayId": "00000000-0000-0000-0000-000000000000_00000000-0000-0000-0000-000000000000" + }, + "eventCategory": "Management" +} diff --git a/lambda-events/src/fixtures/example-cloudwatch-cloudtrail-unknown-user-auth.json b/lambda-events/src/fixtures/example-cloudwatch-cloudtrail-unknown-user-auth.json new file mode 100644 index 000000000..96fed1761 --- /dev/null +++ b/lambda-events/src/fixtures/example-cloudwatch-cloudtrail-unknown-user-auth.json @@ -0,0 +1,33 @@ +{ + "eventVersion": "1.08", + "userIdentity": { + "type": "Unknown", + "principalId": "123456789000", + "arn": "", + "accountId": "123456789000", + "accessKeyId": "" + }, + "eventTime": "2024-07-10T16:05:11Z", + "eventSource": "signin.amazonaws.com", + "eventName": "UserAuthentication", + "awsRegion": "eu-west-1", + "sourceIPAddress": "1.1.1.1", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36", + "requestParameters": null, + "responseElements": null, + "additionalEventData": { + "AuthWorkflowID": "00000000-0000-0000-0000-000000000000", + "LoginTo": "https://tenant.awsapps.com/start/", + "CredentialType": "EXTERNAL_IDP" + }, + "requestID": "00000000-0000-0000-0000-000000000000", + "eventID": "00000000-0000-0000-0000-000000000000", + "readOnly": false, + "eventType": "AwsServiceEvent", + "managementEvent": true, + "recipientAccountId": "123456789000", + "serviceEventDetails": { + "UserAuthentication": "Success" + }, + "eventCategory": "Management" +} diff --git a/lambda-events/src/fixtures/example-cloudwatch_logs-event.json b/lambda-events/src/fixtures/example-cloudwatch_logs-event.json new file mode 100644 index 000000000..e6aae4d32 --- /dev/null +++ b/lambda-events/src/fixtures/example-cloudwatch_logs-event.json @@ -0,0 +1,6 @@ +{ + "awslogs": { + "data": "H4sIAAAAAAAAAHWPwQqCQBCGX0Xm7EFtK+smZBEUgXoLCdMhFtKV3akI8d0bLYmibvPPN3wz00CJxmQnTO41whwWQRIctmEcB6sQbFC3CjW3XW8kxpOpP+OC22d1Wml1qZkQGtoMsScxaczKN3plG8zlaHIta5KqWsozoTYw3/djzwhpLwivWFGHGpAFe7DL68JlBUk+l7KSN7tCOEJ4M3/qOI49vMHj+zCKdlFqLaU2ZHV2a4Ct/an0/ivdX8oYc1UVX860fQDQiMdxRQEAAA==" + } +} + diff --git a/lambda-events/src/fixtures/example-code_commit-event.json b/lambda-events/src/fixtures/example-code_commit-event.json new file mode 100644 index 000000000..751e1afa9 --- /dev/null +++ b/lambda-events/src/fixtures/example-code_commit-event.json @@ -0,0 +1,27 @@ +{ + "Records": [ + { + "eventId": "5a824061-17ca-46a9-bbf9-114edeadbeef", + "eventVersion": "1.0", + "eventTime": "2018-01-26T15:58:33.475+0000", + "eventTriggerName": "my-trigger", + "eventPartNumber": 1, + "codecommit": { + "references": [ + { + "commit": "5c4ef1049f1d27deadbeeff313e0730018be182b", + "ref": "refs/heads/master" + } + ] + }, + "eventName": "TriggerEventTest", + "eventTriggerConfigId": "5a824061-17ca-46a9-bbf9-114edeadbeef", + "eventSourceARN": "arn:aws:codecommit:us-east-1:123456789012:my-repo", + "userIdentityARN": "arn:aws:iam::123456789012:root", + "eventSource": "aws:codecommit", + "awsRegion": "us-east-1", + "eventTotalParts": 1 + } + ] +} + diff --git a/lambda-events/src/fixtures/example-codebuild-phase-change.json b/lambda-events/src/fixtures/example-codebuild-phase-change.json new file mode 100644 index 000000000..5f2f5f5de --- /dev/null +++ b/lambda-events/src/fixtures/example-codebuild-phase-change.json @@ -0,0 +1,138 @@ +{ + "version": "0", + "id": "43ddc2bd-af76-9ca5-2dc7-b695e15adeEX", + "detail-type": "CodeBuild Build Phase Change", + "source": "aws.codebuild", + "account": "123456789012", + "time": "2017-09-01T16:14:21Z", + "region": "us-west-2", + "resources":[ + "arn:aws:codebuild:us-west-2:123456789012:build/my-sample-project:8745a7a9-c340-456a-9166-edf953571bEX" + ], + "detail":{ + "completed-phase": "COMPLETED", + "project-name": "my-sample-project", + "build-id": "arn:aws:codebuild:us-west-2:123456789012:build/my-sample-project:8745a7a9-c340-456a-9166-edf953571bEX", + "completed-phase-context": "[]", + "additional-information": { + "artifact": { + "md5sum": "da9c44c8a9a3cd4b443126e823168fEX", + "sha256sum": "6ccc2ae1df9d155ba83c597051611c42d60e09c6329dcb14a312cecc0a8e39EX", + "location": "arn:aws:s3:::codebuild-123456789012-output-bucket/my-output-artifact.zip" + }, + "environment": { + "image": "aws/codebuild/standard:2.0", + "privileged-mode": false, + "compute-type": "BUILD_GENERAL1_SMALL", + "type": "LINUX_CONTAINER", + "environment-variables": [] + }, + "timeout-in-minutes": 60.0, + "build-complete": true, + "build-number": 55.0, + "initiator": "MyCodeBuildDemoUser", + "build-start-time": "Sep 1, 2017 4:12:29 PM", + "source": { + "location": "codebuild-123456789012-input-bucket/my-input-artifact.zip", + "type": "S3" + }, + "logs": { + "group-name": "/aws/codebuild/my-sample-project", + "stream-name": "8745a7a9-c340-456a-9166-edf953571bEX", + "deep-link": "https://console.aws.amazon.com/cloudwatch/home?region=us-west-2#logEvent:group=/aws/codebuild/my-sample-project;stream=8745a7a9-c340-456a-9166-edf953571bEX" + }, + "phases": [ + { + "phase-context": [], + "start-time": "Sep 1, 2017 4:12:29 PM", + "end-time": "Sep 1, 2017 4:12:29 PM", + "duration-in-seconds": 0.0, + "phase-type": "SUBMITTED", + "phase-status": "SUCCEEDED" + }, + { + "phase-context": [], + "start-time": "Sep 1, 2017 4:12:29 PM", + "end-time": "Sep 13, 2019 4:12:29 AM", + "duration-in-seconds": 0.0, + "phase-type": "QUEUED", + "phase-status": "SUCCEEDED" + }, + { + "phase-context": [], + "start-time": "Sep 1, 2017 4:12:29 PM", + "end-time": "Sep 1, 2017 4:13:05 PM", + "duration-in-seconds": 36, + "phase-type": "PROVISIONING", + "phase-status": "SUCCEEDED" + }, + { + "phase-context": [], + "start-time": "Sep 1, 2017 4:13:05 PM", + "end-time": "Sep 1, 2017 4:13:10 PM", + "duration-in-seconds": 4.0, + "phase-type": "DOWNLOAD_SOURCE", + "phase-status": "SUCCEEDED" + }, + { + "phase-context": [], + "start-time": "Sep 1, 2017 4:13:10 PM", + "end-time": "Sep 1, 2017 4:13:10 PM", + "duration-in-seconds": 0, + "phase-type": "INSTALL", + "phase-status": "SUCCEEDED" + }, + { + "phase-context": [], + "start-time": "Sep 1, 2017 4:13:10 PM", + "end-time": "Sep 1, 2017 4:13:10 PM", + "duration-in-seconds": 0.0, + "phase-type": "PRE_BUILD", + "phase-status": "SUCCEEDED" + }, + { + "phase-context": [], + "start-time": "Sep 1, 2017 4:13:10 PM", + "end-time": "Sep 1, 2017 4:14:21 PM", + "duration-in-seconds": 70, + "phase-type": "BUILD", + "phase-status": "SUCCEEDED" + }, + { + "phase-context": [], + "start-time": "Sep 1, 2017 4:14:21 PM", + "end-time": "Sep 1, 2017 4:14:21 PM", + "duration-in-seconds": 0.0, + "phase-type": "POST_BUILD", + "phase-status": "SUCCEEDED" + }, + { + "phase-context": [], + "start-time": "Sep 1, 2017 4:14:21 PM", + "end-time": "Sep 1, 2017 4:14:21 PM", + "duration-in-seconds": 0, + "phase-type": "UPLOAD_ARTIFACTS", + "phase-status": "SUCCEEDED" + }, + { + "phase-context": [], + "start-time": "Sep 1, 2017 4:14:21 PM", + "end-time": "Sep 1, 2017 4:14:26 PM", + "duration-in-seconds": 4.0, + "phase-type": "FINALIZING", + "phase-status": "SUCCEEDED" + }, + { + "start-time": "Sep 1, 2017 4:14:26 PM", + "phase-type": "COMPLETED" + } + ] + }, + "completed-phase-status": "SUCCEEDED", + "completed-phase-duration-seconds": 4.0, + "version": "1", + "completed-phase-start": "Sep 1, 2017 4:14:21 PM", + "completed-phase-end": "Sep 1, 2017 4:14:26 PM" + } +} + diff --git a/lambda-events/src/fixtures/example-codebuild-state-change.json b/lambda-events/src/fixtures/example-codebuild-state-change.json new file mode 100644 index 000000000..4bf82f20d --- /dev/null +++ b/lambda-events/src/fixtures/example-codebuild-state-change.json @@ -0,0 +1,142 @@ +{ + "version": "0", + "id": "c030038d-8c4d-6141-9545-00ff7b7153EX", + "detail-type": "CodeBuild Build State Change", + "source": "aws.codebuild", + "account": "123456789012", + "time": "2017-09-01T16:14:28Z", + "region": "us-west-2", + "resources":[ + "arn:aws:codebuild:us-west-2:123456789012:build/my-sample-project:8745a7a9-c340-456a-9166-edf953571bEX" + ], + "detail":{ + "build-status": "SUCCEEDED", + "project-name": "my-sample-project", + "build-id": "arn:aws:codebuild:us-west-2:123456789012:build/my-sample-project:8745a7a9-c340-456a-9166-edf953571bEX", + "additional-information": { + "artifact": { + "md5sum": "da9c44c8a9a3cd4b443126e823168fEX", + "sha256sum": "6ccc2ae1df9d155ba83c597051611c42d60e09c6329dcb14a312cecc0a8e39EX", + "location": "arn:aws:s3:::codebuild-123456789012-output-bucket/my-output-artifact.zip" + }, + "environment": { + "image": "aws/codebuild/standard:2.0", + "privileged-mode": false, + "compute-type": "BUILD_GENERAL1_SMALL", + "type": "LINUX_CONTAINER", + "environment-variables": [ + { + "name": "TEST", + "type": "PLAINTEXT", + "value": "TEST" + } + ] + }, + "timeout-in-minutes": 60.0, + "build-complete": true, + "build-number": 55.0, + "initiator": "MyCodeBuildDemoUser", + "build-start-time": "Sep 1, 2017 4:12:29 PM", + "source": { + "location": "codebuild-123456789012-input-bucket/my-input-artifact.zip", + "type": "S3" + }, + "source-version": "my-source-version", + "logs": { + "group-name": "/aws/codebuild/my-sample-project", + "stream-name": "8745a7a9-c340-456a-9166-edf953571bEX", + "deep-link": "https://console.aws.amazon.com/cloudwatch/home?region=us-west-2#logEvent:group=/aws/codebuild/my-sample-project;stream=8745a7a9-c340-456a-9166-edf953571bEX" + }, + "phases": [ + { + "phase-context": [], + "start-time": "Sep 1, 2017 4:12:29 PM", + "end-time": "Sep 1, 2017 4:12:29 PM", + "duration-in-seconds": 0, + "phase-type": "SUBMITTED", + "phase-status": "SUCCEEDED" + }, + { + "phase-context": [], + "start-time": "Sep 1, 2017 4:12:29 PM", + "end-time": "Sep 13, 2019 4:12:29 AM", + "duration-in-seconds": 0.0, + "phase-type": "QUEUED", + "phase-status": "SUCCEEDED" + }, + { + "phase-context": [], + "start-time": "Sep 1, 2017 4:12:29 PM", + "end-time": "Sep 1, 2017 4:13:05 PM", + "duration-in-seconds": 36.0, + "phase-type": "PROVISIONING", + "phase-status": "SUCCEEDED" + }, + { + "phase-context": [], + "start-time": "Sep 1, 2017 4:13:05 PM", + "end-time": "Sep 1, 2017 4:13:10 PM", + "duration-in-seconds": 4, + "phase-type": "DOWNLOAD_SOURCE", + "phase-status": "SUCCEEDED" + }, + { + "phase-context": [], + "start-time": "Sep 1, 2017 4:13:10 PM", + "end-time": "Sep 1, 2017 4:13:10 PM", + "duration-in-seconds": 0.0, + "phase-type": "INSTALL", + "phase-status": "SUCCEEDED" + }, + { + "phase-context": [], + "start-time": "Sep 1, 2017 4:13:10 PM", + "end-time": "Sep 1, 2017 4:13:10 PM", + "duration-in-seconds": 0, + "phase-type": "PRE_BUILD", + "phase-status": "SUCCEEDED" + }, + { + "phase-context": [], + "start-time": "Sep 1, 2017 4:13:10 PM", + "end-time": "Sep 1, 2017 4:14:21 PM", + "duration-in-seconds": 70.0, + "phase-type": "BUILD", + "phase-status": "SUCCEEDED" + }, + { + "phase-context": [], + "start-time": "Sep 1, 2017 4:14:21 PM", + "end-time": "Sep 1, 2017 4:14:21 PM", + "duration-in-seconds": 0, + "phase-type": "POST_BUILD", + "phase-status": "SUCCEEDED" + }, + { + "phase-context": [], + "start-time": "Sep 1, 2017 4:14:21 PM", + "end-time": "Sep 1, 2017 4:14:21 PM", + "duration-in-seconds": 0.0, + "phase-type": "UPLOAD_ARTIFACTS", + "phase-status": "SUCCEEDED" + }, + { + "phase-context": [], + "start-time": "Sep 1, 2017 4:14:21 PM", + "end-time": "Sep 1, 2017 4:14:26 PM", + "duration-in-seconds": 4, + "phase-type": "FINALIZING", + "phase-status": "SUCCEEDED" + }, + { + "start-time": "Sep 1, 2017 4:14:26 PM", + "phase-type": "COMPLETED" + } + ] + }, + "current-phase": "COMPLETED", + "current-phase-context": "[]", + "version": "1" + } +} + diff --git a/lambda-events/src/fixtures/example-codedeploy-deployment-event.json b/lambda-events/src/fixtures/example-codedeploy-deployment-event.json new file mode 100644 index 000000000..dc44df099 --- /dev/null +++ b/lambda-events/src/fixtures/example-codedeploy-deployment-event.json @@ -0,0 +1,22 @@ +{ + "account": "123456789012", + "region": "us-east-1", + "detail-type": "CodeDeploy Deployment State-change Notification", + "source": "aws.codedeploy", + "version": "0", + "time": "2016-06-30T22:06:31Z", + "id": "c071bfbf-83c4-49ca-a6ff-3df053957145", + "resources": [ + "arn:aws:codedeploy:us-east-1:123456789012:application:myApplication", + "arn:aws:codedeploy:us-east-1:123456789012:deploymentgroup:myApplication/myDeploymentGroup" + ], + "detail": { + "instanceGroupId": "9fd2fbef-2157-40d8-91e7-6845af69e2d2", + "region": "us-east-1", + "application": "myApplication", + "deploymentId": "d-123456789", + "state": "SUCCESS", + "deploymentGroup": "myDeploymentGroup" + } +} + diff --git a/lambda-events/src/fixtures/example-codedeploy-instance-event.json b/lambda-events/src/fixtures/example-codedeploy-instance-event.json new file mode 100644 index 000000000..001e1d39e --- /dev/null +++ b/lambda-events/src/fixtures/example-codedeploy-instance-event.json @@ -0,0 +1,23 @@ +{ + "account": "123456789012", + "region": "us-east-1", + "detail-type": "CodeDeploy Instance State-change Notification", + "source": "aws.codedeploy", + "version": "0", + "time": "2016-06-30T23:18:50Z", + "id": "fb1d3015-c091-4bf9-95e2-d98521ab2ecb", + "resources": [ + "arn:aws:ec2:us-east-1:123456789012:instance/i-0000000aaaaaaaaaa", + "arn:aws:codedeploy:us-east-1:123456789012:deploymentgroup:myApplication/myDeploymentGroup", + "arn:aws:codedeploy:us-east-1:123456789012:application:myApplication" + ], + "detail": { + "instanceId": "i-0000000aaaaaaaaaa", + "region": "us-east-1", + "state": "SUCCESS", + "application": "myApplication", + "deploymentId": "d-123456789", + "instanceGroupId": "8cd3bfa8-9e72-4cbe-a1e5-da4efc7efd49", + "deploymentGroup": "myDeploymentGroup" + } +} diff --git a/lambda-events/src/fixtures/example-codedeploy-lifecycle-event.json b/lambda-events/src/fixtures/example-codedeploy-lifecycle-event.json new file mode 100644 index 000000000..41baa76cf --- /dev/null +++ b/lambda-events/src/fixtures/example-codedeploy-lifecycle-event.json @@ -0,0 +1,5 @@ +{ + "DeploymentId": "d-deploymentId", + "LifecycleEventHookExecutionId": "eyJlbmNyeXB0ZWREYXRhIjoiY3VHU2NjdkJXUTJQUENVd2dkYUNGRVg0dWlpME9UWVdHTVhZcDRXVW5LYUVKc21EaUFPMkNLNXMwMWFrNDlYVStlbXdRb29xS3NJTUNVQ3RYRGFZSXc1VTFwUllvMDhmMzdlbDZFeDVVdjZrNFc0eU5waGh6YTRvdkNWcmVveVR6OWdERlM2SmlIYW1TZz09IiwiaXZQYXJhbWV0ZXJTcGVjIjoiTm1ZNFR6RzZxQVhHamhhLyIsIm1hdGVyaWFsU2V0U2VyaWFsIjoxfQ==" +} + diff --git a/lambda-events/src/fixtures/example-codepipeline-action-execution-stage-change-event.json b/lambda-events/src/fixtures/example-codepipeline-action-execution-stage-change-event.json new file mode 100644 index 000000000..38f85f00c --- /dev/null +++ b/lambda-events/src/fixtures/example-codepipeline-action-execution-stage-change-event.json @@ -0,0 +1,27 @@ +{ + "version": "0", + "id": "CWE-event-id", + "detail-type": "CodePipeline Action Execution State Change", + "source": "aws.codepipeline", + "account": "123456789012", + "time": "2017-04-22T03:31:47Z", + "region": "us-east-1", + "resources": [ + "arn:aws:codepipeline:us-east-1:123456789012:pipeline:myPipeline" + ], + "detail": { + "pipeline": "myPipeline", + "version": 1, + "execution-id": "01234567-0123-0123-0123-012345678901", + "stage": "Prod", + "action": "myAction", + "state": "STARTED", + "region":"us-west-2", + "type": { + "owner": "AWS", + "category": "Deploy", + "provider": "CodeDeploy", + "version": 1 + } + } +} diff --git a/lambda-events/src/fixtures/example-codepipeline-execution-stage-change-event.json b/lambda-events/src/fixtures/example-codepipeline-execution-stage-change-event.json new file mode 100644 index 000000000..cb311262f --- /dev/null +++ b/lambda-events/src/fixtures/example-codepipeline-execution-stage-change-event.json @@ -0,0 +1,18 @@ +{ + "version": "0", + "id": "CWE-event-id", + "detail-type": "CodePipeline Stage Execution State Change", + "source": "aws.codepipeline", + "account": "123456789012", + "time": "2017-04-22T03:31:47Z", + "region": "us-east-1", + "resources": [ + "arn:aws:codepipeline:us-east-1:123456789012:pipeline:myPipeline" + ], + "detail": { + "pipeline": "myPipeline", + "version": 1, + "state": "STARTED", + "execution-id": "01234567-0123-0123-0123-012345678901" + } +} diff --git a/lambda-events/src/fixtures/example-codepipeline-execution-state-change-event.json b/lambda-events/src/fixtures/example-codepipeline-execution-state-change-event.json new file mode 100644 index 000000000..43ade4b61 --- /dev/null +++ b/lambda-events/src/fixtures/example-codepipeline-execution-state-change-event.json @@ -0,0 +1,18 @@ +{ + "version": "0", + "id": "CWE-event-id", + "detail-type": "CodePipeline Pipeline Execution State Change", + "source": "aws.codepipeline", + "account": "123456789012", + "time": "2017-04-22T03:31:47Z", + "region": "us-east-1", + "resources": [ + "arn:aws:codepipeline:us-east-1:123456789012:pipeline:myPipeline" + ], + "detail": { + "pipeline": "myPipeline", + "version": 1, + "state": "STARTED", + "execution-id": "01234567-0123-0123-0123-012345678901" + } +} diff --git a/lambda-events/src/fixtures/example-codepipeline_job-event.json b/lambda-events/src/fixtures/example-codepipeline_job-event.json new file mode 100644 index 000000000..d43d2d0f3 --- /dev/null +++ b/lambda-events/src/fixtures/example-codepipeline_job-event.json @@ -0,0 +1,34 @@ +{ + "CodePipeline.job": { + "id": "11111111-abcd-1111-abcd-111111abcdef", + "accountId": "111111111111", + "data": { + "actionConfiguration": { + "configuration": { + "FunctionName": "MyLambdaFunctionForAWSCodePipeline", + "UserParameters": "some-input-such-as-a-URL" + } + }, + "inputArtifacts": [ + { + "location": { + "s3Location": { + "bucketName": "the name of the bucket configured as the pipeline artifact store in Amazon S3, for example codepipeline-us-east-2-1234567890", + "objectKey": "the name of the application, for example CodePipelineDemoApplication.zip" + }, + "type": "S3" + }, + "revision": null, + "name": "ArtifactName" + } + ], + "outputArtifacts": [], + "artifactCredentials": { + "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + "sessionToken": "MIICiTCCAfICCQD6m7oRw0uXOjANBgkqhkiG9w0BAQUFADCBiDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZBbWF6b24xFDASBgNVBAsTC0lBTSBDb25zb2xlMRIwEAYDVQQDEwlUZXN0Q2lsYWMxHzAdBgkqhkiG9w0BCQEWEG5vb25lQGFtYXpvbi5jb20wHhcNMTEwNDI1MjA0NTIxWhcNMTIwNDI0MjA0NTIxWjCBiDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZBbWF6b24xFDASBgNVBAsTC0lBTSBDb25zb2xlMRIwEAYDVQQDEwlUZXN0Q2lsYWMxHzAdBgkqhkiG9w0BCQEWEG5vb25lQGFtYXpvbi5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMaK0dn+a4GmWIWJ21uUSfwfEvySWtC2XADZ4nB+BLYgVIk60CpiwsZ3G93vUEIO3IyNoH/f0wYK8m9TrDHudUZg3qX4waLG5M43q7Wgc/MbQITxOUSQv7c7ugFFDzQGBzZswY6786m86gpEIbb3OhjZnzcvQAaRHhdlQWIMm2nrAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAtCu4nUhVVxYUntneD9+h8Mg9q6q+auNKyExzyLwaxlAoo7TJHidbtS4J5iNmZgXL0FkbFFBjvSfpJIlJ00zbhNYS5f6GuoEDmFJl0ZxBHjJnyp378OD8uTs7fLvjx79LjSTbNYiytVbZPQUQ5Yaxu2jXnimvw3rrszlaEXAMPLE=", + "accessKeyId": "AKIAIOSFODNN7EXAMPLE" + }, + "continuationToken": "A continuation token if continuing job" + } + } +} diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-create-auth-challenge-user-not-found.json b/lambda-events/src/fixtures/example-cognito-event-userpools-create-auth-challenge-user-not-found.json new file mode 100644 index 000000000..40ce2a2b6 --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-create-auth-challenge-user-not-found.json @@ -0,0 +1,41 @@ +{ + "version": "1", + "region": "us-west-2", + "userPoolId": "", + "userName": "", + "callerContext": { + "awsSdkVersion": "aws-sdk-unknown-unknown", + "clientId": "" + }, + "triggerSource": "CreateAuthChallenge_Authentication", + "request": { + "userAttributes": { + "sub": "", + "cognito:user_status": "CONFIRMED", + "phone_number_verified": "true", + "cognito:phone_number_alias": "+12223334455", + "phone_number": "+12223334455" + }, + "challengeName": "CUSTOM_CHALLENGE", + "session": [ + { + "challengeName": "PASSWORD_VERIFIER", + "challengeResult": true, + "challengeMetadata": "metadata" + } + ], + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + }, + "userNotFound": true + }, + "response": { + "publicChallengeParameters": { + "a": "b" + }, + "privateChallengeParameters": { + "c": "d" + }, + "challengeMetadata": "challengeMetadata" + } +} diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-create-auth-challenge.json b/lambda-events/src/fixtures/example-cognito-event-userpools-create-auth-challenge.json new file mode 100644 index 000000000..2d0f2a83e --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-create-auth-challenge.json @@ -0,0 +1,41 @@ +{ + "version": "1", + "region": "us-west-2", + "userPoolId": "", + "userName": "", + "callerContext": { + "awsSdkVersion": "aws-sdk-unknown-unknown", + "clientId": "" + }, + "triggerSource": "CreateAuthChallenge_Authentication", + "request": { + "userAttributes": { + "sub": "", + "cognito:user_status": "CONFIRMED", + "phone_number_verified": "true", + "cognito:phone_number_alias": "+12223334455", + "phone_number": "+12223334455" + }, + "challengeName": "CUSTOM_CHALLENGE", + "session": [ + { + "challengeName": "PASSWORD_VERIFIER", + "challengeResult": true, + "challengeMetadata": "metadata" + } + ], + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + }, + "userNotFound": false + }, + "response": { + "publicChallengeParameters": { + "a": "b" + }, + "privateChallengeParameters": { + "c": "d" + }, + "challengeMetadata": "challengeMetadata" + } +} diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-custommessage.json b/lambda-events/src/fixtures/example-cognito-event-userpools-custommessage.json new file mode 100644 index 000000000..8b2ca55d7 --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-custommessage.json @@ -0,0 +1,27 @@ +{ + "version": "1", + "triggerSource": "CustomMessage_VerifyUserAttribute", + "region": "", + "userPoolId": "", + "userName": "", + "callerContext": { + "awsSdkVersion": "", + "clientId": "" + }, + "request": { + "userAttributes": { + "phone_number_verified": true, + "email_verified": false + }, + "codeParameter": "####", + "usernameParameter": "{username}", + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + } + }, + "response": { + "smsMessage": "", + "emailMessage": "", + "emailSubject": "" + } +} diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-define-auth-challenge-optional-response-fields.json b/lambda-events/src/fixtures/example-cognito-event-userpools-define-auth-challenge-optional-response-fields.json new file mode 100644 index 000000000..c878e6617 --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-define-auth-challenge-optional-response-fields.json @@ -0,0 +1,34 @@ +{ + "version": "1", + "region": "us-west-2", + "userPoolId": "", + "userName": "", + "callerContext": { + "awsSdkVersion": "aws-sdk-unknown-unknown", + "clientId": "" + }, + "triggerSource": "DefineAuthChallenge_Authentication", + "request": { + "userAttributes": { + "sub": "", + "cognito:user_status": "CONFIRMED", + "phone_number_verified": "true", + "cognito:phone_number_alias": "+12223334455", + "phone_number": "+12223334455" + }, + "session": [ + { + "challengeName": "PASSWORD_VERIFIER", + "challengeResult": true, + "challengeMetadata": "metadata" + } + ], + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + }, + "userNotFound": false + }, + "response": { + "challengeName": "challengeName" + } +} diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-define-auth-challenge-user-not-found.json b/lambda-events/src/fixtures/example-cognito-event-userpools-define-auth-challenge-user-not-found.json new file mode 100644 index 000000000..1ad40e2a5 --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-define-auth-challenge-user-not-found.json @@ -0,0 +1,36 @@ +{ + "version": "1", + "region": "us-west-2", + "userPoolId": "", + "userName": "", + "callerContext": { + "awsSdkVersion": "aws-sdk-unknown-unknown", + "clientId": "" + }, + "triggerSource": "DefineAuthChallenge_Authentication", + "request": { + "userAttributes": { + "sub": "", + "cognito:user_status": "CONFIRMED", + "phone_number_verified": "true", + "cognito:phone_number_alias": "+12223334455", + "phone_number": "+12223334455" + }, + "session": [ + { + "challengeName": "PASSWORD_VERIFIER", + "challengeResult": true, + "challengeMetadata": "metadata" + } + ], + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + }, + "userNotFound": true + }, + "response": { + "challengeName": "challengeName", + "issueTokens": true, + "failAuthentication": true + } +} diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-define-auth-challenge.json b/lambda-events/src/fixtures/example-cognito-event-userpools-define-auth-challenge.json new file mode 100644 index 000000000..33106f88f --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-define-auth-challenge.json @@ -0,0 +1,36 @@ +{ + "version": "1", + "region": "us-west-2", + "userPoolId": "", + "userName": "", + "callerContext": { + "awsSdkVersion": "aws-sdk-unknown-unknown", + "clientId": "" + }, + "triggerSource": "DefineAuthChallenge_Authentication", + "request": { + "userAttributes": { + "sub": "", + "cognito:user_status": "CONFIRMED", + "phone_number_verified": "true", + "cognito:phone_number_alias": "+12223334455", + "phone_number": "+12223334455" + }, + "session": [ + { + "challengeName": "PASSWORD_VERIFIER", + "challengeResult": true, + "challengeMetadata": "metadata" + } + ], + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + }, + "userNotFound": false + }, + "response": { + "challengeName": "challengeName", + "issueTokens": true, + "failAuthentication": true + } +} diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-migrateuser-null-fields.json b/lambda-events/src/fixtures/example-cognito-event-userpools-migrateuser-null-fields.json new file mode 100644 index 000000000..3862902ea --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-migrateuser-null-fields.json @@ -0,0 +1,31 @@ +{ + "version": "1", + "triggerSource": "UserMigration_Authentication", + "region": "", + "userPoolId": "", + "userName": "", + "callerContext": { + "awsSdkVersion": "", + "clientId": "" + }, + "request": { + "password": "", + "validationData": { + "exampleMetadataKey": "example metadata value" + }, + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + } + }, + "response": { + "userAttributes": null, + "finalUserStatus": "", + "messageAction": "", + "desiredDeliveryMediums": [ + "", + "" + ], + "forceAliasCreation": null + } +} + diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-migrateuser.json b/lambda-events/src/fixtures/example-cognito-event-userpools-migrateuser.json new file mode 100644 index 000000000..958b40c30 --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-migrateuser.json @@ -0,0 +1,34 @@ +{ + "version": "1", + "triggerSource": "UserMigration_Authentication", + "region": "", + "userPoolId": "", + "userName": "", + "callerContext": { + "awsSdkVersion": "", + "clientId": "" + }, + "request": { + "password": "", + "validationData": { + "exampleMetadataKey": "example metadata value" + }, + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + } + }, + "response": { + "userAttributes": { + "email": "", + "phone_number": "" + }, + "finalUserStatus": "", + "messageAction": "", + "desiredDeliveryMediums": [ + "", + "" + ], + "forceAliasCreation": true + } +} + diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-postauthentication.json b/lambda-events/src/fixtures/example-cognito-event-userpools-postauthentication.json new file mode 100644 index 000000000..1af00a201 --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-postauthentication.json @@ -0,0 +1,22 @@ +{ + "version": "1", + "triggerSource": "PostAuthentication_Authentication", + "region": "", + "userPoolId": "", + "userName": "", + "callerContext": { + "awsSdkVersion": "", + "clientId": "" + }, + "request": { + "newDeviceUsed": true, + "userAttributes" : { + "email": "", + "phone_number": "" + }, + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + } + }, + "response": {} +} diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-postconfirmation.json b/lambda-events/src/fixtures/example-cognito-event-userpools-postconfirmation.json new file mode 100644 index 000000000..c4e4aa18e --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-postconfirmation.json @@ -0,0 +1,21 @@ +{ + "version": "1", + "triggerSource": "PostConfirmation_ConfirmSignUp", + "region": "", + "userPoolId": "", + "userName": "", + "callerContext": { + "awsSdkVersion": "", + "clientId": "" + }, + "request": { + "userAttributes" : { + "email": "", + "phone_number": "" + }, + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + } + }, + "response": {} +} diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-preauthentication.json b/lambda-events/src/fixtures/example-cognito-event-userpools-preauthentication.json new file mode 100644 index 000000000..45e76bfff --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-preauthentication.json @@ -0,0 +1,21 @@ +{ + "version": "1", + "triggerSource": "PreAuthentication_Authentication", + "region": "", + "userPoolId": "", + "userName": "", + "callerContext": { + "awsSdkVersion": "", + "clientId": "" + }, + "request": { + "userAttributes": { + "email": "" + }, + "validationData": { + "k1": "v1", + "k2": "v2" + } + }, + "response": {} +} diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-presignup.json b/lambda-events/src/fixtures/example-cognito-event-userpools-presignup.json new file mode 100644 index 000000000..e579e3b68 --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-presignup.json @@ -0,0 +1,29 @@ +{ + "version": "1", + "triggerSource": "PreSignUp_SignUp", + "region": "", + "userPoolId": "", + "userName": "", + "callerContext": { + "awsSdkVersion": "", + "clientId": "" + }, + "request": { + "userAttributes": { + "email": "", + "phone_number": "" + }, + "validationData": { + "k1": "v1", + "k2": "v2" + }, + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + } + }, + "response": { + "autoConfirmUser": false, + "autoVerifyEmail": true, + "autoVerifyPhone": true + } +} diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen-incoming.json b/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen-incoming.json new file mode 100644 index 000000000..ae78b7c7e --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen-incoming.json @@ -0,0 +1,32 @@ +{ + "version": "1", + "triggerSource": "TokenGeneration_Authentication", + "region": "region", + "userPoolId": "userPoolId", + "userName": "userName", + "callerContext": { + "awsSdkVersion": "calling aws sdk with version", + "clientId": "apps client id" + }, + "request": { + "userAttributes": { + "email": "email", + "phone_number": "phone_number" + }, + "groupConfiguration": { + "groupsToOverride": ["group-A", "group-B", "group-C"], + "iamRolesToOverride": [ + "arn:aws:iam::XXXXXXXXXXXX:role/sns_callerA", + "arn:aws:iam::XXXXXXXXX:role/sns_callerB", + "arn:aws:iam::XXXXXXXXXX:role/sns_callerC" + ], + "preferredRole": "arn:aws:iam::XXXXXXXXXXX:role/sns_caller" + }, + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + } + }, + "response": { + "claimsOverrideDetails": null + } +} diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen-null-group-configuration.json b/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen-null-group-configuration.json new file mode 100644 index 000000000..be7fd4e2f --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen-null-group-configuration.json @@ -0,0 +1,39 @@ +{ + "version": "1", + "triggerSource": "TokenGeneration_HostedAuth", + "region": "region", + "userPoolId": "userPoolId", + "userName": "userName", + "callerContext": { + "awsSdkVersion": "calling aws sdk with version", + "clientId": "apps client id" + }, + "request": { + "userAttributes": { + "email": "email", + "phone_number": "phone_number" + }, + "groupConfiguration": null, + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + } + }, + "response": { + "claimsOverrideDetails": { + "claimsToAddOrOverride": { + "attribute_key2": "attribute_value2", + "attribute_key": "attribute_value" + }, + "claimsToSuppress": ["email"], + "groupOverrideDetails": { + "groupsToOverride": ["group-A", "group-B", "group-C"], + "iamRolesToOverride": [ + "arn:aws:iam::XXXXXXXXXXXX:role/sns_callerA", + "arn:aws:iam::XXXXXXXXX:role/sns_callerB", + "arn:aws:iam::XXXXXXXXXX:role/sns_callerC" + ], + "preferredRole": "arn:aws:iam::XXXXXXXXXXX:role/sns_caller" + } + } + } +} diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen-v2-incoming.json b/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen-v2-incoming.json new file mode 100644 index 000000000..e5c776d84 --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen-v2-incoming.json @@ -0,0 +1,33 @@ +{ + "version": "1", + "triggerSource": "TokenGeneration_HostedAuth", + "region": "region", + "userPoolId": "userPoolId", + "userName": "userName", + "callerContext": { + "awsSdkVersion": "calling aws sdk with version", + "clientId": "apps client id" + }, + "request": { + "userAttributes": { + "email": "email", + "phone_number": "phone_number" + }, + "scopes": ["scope-1", "scope-2"], + "groupConfiguration": { + "groupsToOverride": ["group-A", "group-B", "group-C"], + "iamRolesToOverride": [ + "arn:aws:iam::XXXXXXXXXXXX:role/sns_callerA", + "arn:aws:iam::XXXXXXXXX:role/sns_callerB", + "arn:aws:iam::XXXXXXXXXX:role/sns_callerC" + ], + "preferredRole": "arn:aws:iam::XXXXXXXXXXX:role/sns_caller" + }, + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + } + }, + "response": { + "claimsOverrideDetails": null + } +} diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen-v2-null-group-configuration.json b/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen-v2-null-group-configuration.json new file mode 100644 index 000000000..d128a7ba1 --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen-v2-null-group-configuration.json @@ -0,0 +1,50 @@ +{ + "version": "1", + "triggerSource": "TokenGeneration_HostedAuth", + "region": "region", + "userPoolId": "userPoolId", + "userName": "userName", + "callerContext": { + "awsSdkVersion": "calling aws sdk with version", + "clientId": "apps client id" + }, + "request": { + "userAttributes": { + "email": "email", + "phone_number": "phone_number" + }, + "scopes": ["scope-1", "scope-2"], + "groupConfiguration": null, + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + } + }, + "response": { + "claimsAndScopeOverrideDetails": { + "idTokenGeneration": { + "claimsToAddOrOverride": { + "string": "string" + }, + "claimsToSuppress": ["string", "string"] + }, + "accessTokenGeneration": { + "claimsToAddOrOverride": { + "attribute_key2": "attribute_value2", + "attribute_key": "attribute_value" + }, + "claimsToSuppress": ["email", "phone"], + "scopesToAdd": ["scope-B", "scope-B"], + "scopesToSuppress": ["scope-C", "scope-D"] + }, + "groupOverrideDetails": { + "groupsToOverride": ["group-A", "group-B", "group-C"], + "iamRolesToOverride": [ + "arn:aws:iam::XXXXXXXXXXXX:role/sns_callerA", + "arn:aws:iam::XXXXXXXXX:role/sns_callerB", + "arn:aws:iam::XXXXXXXXXX:role/sns_callerC" + ], + "preferredRole": "arn:aws:iam::XXXXXXXXXXX:role/sns_caller" + } + } + } +} diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen-v2.json b/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen-v2.json new file mode 100644 index 000000000..6046d4460 --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen-v2.json @@ -0,0 +1,58 @@ +{ + "version": "1", + "triggerSource": "TokenGeneration_HostedAuth", + "region": "region", + "userPoolId": "userPoolId", + "userName": "userName", + "callerContext": { + "awsSdkVersion": "calling aws sdk with version", + "clientId": "apps client id" + }, + "request": { + "userAttributes": { + "email": "email", + "phone_number": "phone_number" + }, + "scopes": ["scope-1", "scope-2"], + "groupConfiguration": { + "groupsToOverride": ["group-A", "group-B", "group-C"], + "iamRolesToOverride": [ + "arn:aws:iam::XXXXXXXXXXXX:role/sns_callerA", + "arn:aws:iam::XXXXXXXXX:role/sns_callerB", + "arn:aws:iam::XXXXXXXXXX:role/sns_callerC" + ], + "preferredRole": "arn:aws:iam::XXXXXXXXXXX:role/sns_caller" + }, + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + } + }, + "response": { + "claimsAndScopeOverrideDetails": { + "idTokenGeneration": { + "claimsToAddOrOverride": { + "string": "string" + }, + "claimsToSuppress": ["string", "string"] + }, + "accessTokenGeneration": { + "claimsToAddOrOverride": { + "attribute_key2": "attribute_value2", + "attribute_key": "attribute_value" + }, + "claimsToSuppress": ["email", "phone"], + "scopesToAdd": ["scope-B", "scope-B"], + "scopesToSuppress": ["scope-C", "scope-D"] + }, + "groupOverrideDetails": { + "groupsToOverride": ["group-A", "group-B", "group-C"], + "iamRolesToOverride": [ + "arn:aws:iam::XXXXXXXXXXXX:role/sns_callerA", + "arn:aws:iam::XXXXXXXXX:role/sns_callerB", + "arn:aws:iam::XXXXXXXXXX:role/sns_callerC" + ], + "preferredRole": "arn:aws:iam::XXXXXXXXXXX:role/sns_caller" + } + } + } +} diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen.json b/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen.json new file mode 100644 index 000000000..f79e8573e --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen.json @@ -0,0 +1,47 @@ +{ + "version": "1", + "triggerSource": "TokenGeneration_HostedAuth", + "region": "region", + "userPoolId": "userPoolId", + "userName": "userName", + "callerContext": { + "awsSdkVersion": "calling aws sdk with version", + "clientId": "apps client id" + }, + "request": { + "userAttributes": { + "email": "email", + "phone_number": "phone_number" + }, + "groupConfiguration": { + "groupsToOverride": ["group-A", "group-B", "group-C"], + "iamRolesToOverride": [ + "arn:aws:iam::XXXXXXXXXXXX:role/sns_callerA", + "arn:aws:iam::XXXXXXXXX:role/sns_callerB", + "arn:aws:iam::XXXXXXXXXX:role/sns_callerC" + ], + "preferredRole": "arn:aws:iam::XXXXXXXXXXX:role/sns_caller" + }, + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + } + }, + "response": { + "claimsOverrideDetails": { + "claimsToAddOrOverride": { + "attribute_key2": "attribute_value2", + "attribute_key": "attribute_value" + }, + "claimsToSuppress": ["email"], + "groupOverrideDetails": { + "groupsToOverride": ["group-A", "group-B", "group-C"], + "iamRolesToOverride": [ + "arn:aws:iam::XXXXXXXXXXXX:role/sns_callerA", + "arn:aws:iam::XXXXXXXXX:role/sns_callerB", + "arn:aws:iam::XXXXXXXXXX:role/sns_callerC" + ], + "preferredRole": "arn:aws:iam::XXXXXXXXXXX:role/sns_caller" + } + } + } +} diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-verify-auth-challenge-null-answer-correct.json b/lambda-events/src/fixtures/example-cognito-event-userpools-verify-auth-challenge-null-answer-correct.json new file mode 100644 index 000000000..964061f26 --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-verify-auth-challenge-null-answer-correct.json @@ -0,0 +1,31 @@ +{ + "version": "1", + "region": "us-west-2", + "userPoolId": "", + "userName": "", + "callerContext": { + "awsSdkVersion": "aws-sdk-unknown-unknown", + "clientId": "" + }, + "triggerSource": "VerifyAuthChallengeResponse_Authentication", + "request": { + "userAttributes": { + "sub": "", + "cognito:user_status": "CONFIRMED", + "phone_number_verified": "true", + "cognito:phone_number_alias": "+12223334455", + "phone_number": "+12223334455" + }, + "privateChallengeParameters": { + "secret": "11122233" + }, + "challengeAnswer": "123xxxx", + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + }, + "userNotFound": false + }, + "response": { + "answerCorrect": null + } +} diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-verify-auth-challenge-optional-answer-correct.json b/lambda-events/src/fixtures/example-cognito-event-userpools-verify-auth-challenge-optional-answer-correct.json new file mode 100644 index 000000000..f6f7ca094 --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-verify-auth-challenge-optional-answer-correct.json @@ -0,0 +1,30 @@ +{ + "version": "1", + "region": "us-west-2", + "userPoolId": "", + "userName": "", + "callerContext": { + "awsSdkVersion": "aws-sdk-unknown-unknown", + "clientId": "" + }, + "triggerSource": "VerifyAuthChallengeResponse_Authentication", + "request": { + "userAttributes": { + "sub": "", + "cognito:user_status": "CONFIRMED", + "phone_number_verified": "true", + "cognito:phone_number_alias": "+12223334455", + "phone_number": "+12223334455" + }, + "privateChallengeParameters": { + "secret": "11122233" + }, + "challengeAnswer": "123xxxx", + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + }, + "userNotFound": false + }, + "response": { + } +} diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-verify-auth-challenge-user-not-found.json b/lambda-events/src/fixtures/example-cognito-event-userpools-verify-auth-challenge-user-not-found.json new file mode 100644 index 000000000..a5068eaa5 --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-verify-auth-challenge-user-not-found.json @@ -0,0 +1,31 @@ +{ + "version": "1", + "region": "us-west-2", + "userPoolId": "", + "userName": "", + "callerContext": { + "awsSdkVersion": "aws-sdk-unknown-unknown", + "clientId": "" + }, + "triggerSource": "VerifyAuthChallengeResponse_Authentication", + "request": { + "userAttributes": { + "sub": "", + "cognito:user_status": "CONFIRMED", + "phone_number_verified": "true", + "cognito:phone_number_alias": "+12223334455", + "phone_number": "+12223334455" + }, + "privateChallengeParameters": { + "secret": "11122233" + }, + "challengeAnswer": "123xxxx", + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + }, + "userNotFound": true + }, + "response": { + "answerCorrect": true + } +} diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-verify-auth-challenge.json b/lambda-events/src/fixtures/example-cognito-event-userpools-verify-auth-challenge.json new file mode 100644 index 000000000..6bff9974a --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-verify-auth-challenge.json @@ -0,0 +1,31 @@ +{ + "version": "1", + "region": "us-west-2", + "userPoolId": "", + "userName": "", + "callerContext": { + "awsSdkVersion": "aws-sdk-unknown-unknown", + "clientId": "" + }, + "triggerSource": "VerifyAuthChallengeResponse_Authentication", + "request": { + "userAttributes": { + "sub": "", + "cognito:user_status": "CONFIRMED", + "phone_number_verified": "true", + "cognito:phone_number_alias": "+12223334455", + "phone_number": "+12223334455" + }, + "privateChallengeParameters": { + "secret": "11122233" + }, + "challengeAnswer": "123xxxx", + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + }, + "userNotFound": false + }, + "response": { + "answerCorrect": true + } +} diff --git a/lambda-events/src/fixtures/example-cognito-event.json b/lambda-events/src/fixtures/example-cognito-event.json new file mode 100644 index 000000000..eec738d3f --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event.json @@ -0,0 +1,17 @@ +{ + "datasetName": "datasetName", + "eventType": "SyncTrigger", + "region": "us-east-1", + "identityId": "identityId", + "datasetRecords": + { + "SampleKey1": + { + "newValue": "newValue1", + "oldValue": "oldValue1", + "op": "replace" + } + }, + "identityPoolId": "identityPoolId", + "version": 2 +} diff --git a/lambda-events/src/fixtures/example-config-event.json b/lambda-events/src/fixtures/example-config-event.json new file mode 100644 index 000000000..e269c762c --- /dev/null +++ b/lambda-events/src/fixtures/example-config-event.json @@ -0,0 +1,13 @@ +{ + "configRuleId": "config-rule-0123456", + "version": "1.0", + "configRuleName": "periodic-config-rule", + "configRuleArn": "arn:aws:config:us-east-1:012345678912:config-rule/config-rule-0123456", + "invokingEvent": "{\"configSnapshotId\":\"00000000-0000-0000-0000-000000000000\",\"s3ObjectKey\":\"AWSLogs/000000000000/Config/us-east-1/2016/2/24/ConfigSnapshot/000000000000_Config_us-east-1_ConfigSnapshot_20160224T182319Z_00000000-0000-0000-0000-000000000000.json.gz\",\"s3Bucket\":\"config-bucket\",\"notificationCreationTime\":\"2016-02-24T18:23:20.328Z\",\"messageType\":\"ConfigurationSnapshotDeliveryCompleted\",\"recordVersion\":\"1.1\"}", + "resultToken": "myResultToken", + "eventLeftScope": false, + "ruleParameters": "{\"myParameterKey\":\"myParameterValue\"}", + "executionRoleArn": "arn:aws:iam::012345678912:role/config-role", + "accountId": "012345678912" +} + diff --git a/lambda-events/src/fixtures/example-connect-event-without-queue.json b/lambda-events/src/fixtures/example-connect-event-without-queue.json new file mode 100644 index 000000000..c3d626dbb --- /dev/null +++ b/lambda-events/src/fixtures/example-connect-event-without-queue.json @@ -0,0 +1,29 @@ +{ + "Name": "ContactFlowExecution", + "Details": { + "Parameters": { + "key1": "value1", + "key2": "value2" + }, + "ContactData": { + "ContactId": "ASDAcxcasDFSSDFs", + "InitialContactId": "Acxsada-asdasdaxA", + "PreviousContactId": "Acxsada-asdasdaxA", + "Channel": "Voice", + "InstanceARN": "", + "InitiationMethod": "INBOUND/OUTBOUND/TRANSFER/CALLBACK", + "SystemEndpoint": { + "Type": "TELEPHONE_NUMBER", + "Address": "01234567" + }, + "CustomerEndpoint": { + "Type": "TELEPHONE_NUMBER", + "Address": "+14802021091" + }, + "Attributes": { + "key1": "value", + "key2": "value" + } + } + } +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-connect-event.json b/lambda-events/src/fixtures/example-connect-event.json new file mode 100644 index 000000000..2480de70b --- /dev/null +++ b/lambda-events/src/fixtures/example-connect-event.json @@ -0,0 +1,33 @@ +{ + "Name": "ContactFlowExecution", + "Details": { + "Parameters": { + "key1": "value1", + "key2": "value2" + }, + "ContactData": { + "ContactId": "ASDAcxcasDFSSDFs", + "InitialContactId": "Acxsada-asdasdaxA", + "PreviousContactId": "Acxsada-asdasdaxA", + "Channel": "Voice", + "InstanceARN": "", + "InitiationMethod": "INBOUND/OUTBOUND/TRANSFER/CALLBACK", + "SystemEndpoint": { + "Type": "TELEPHONE_NUMBER", + "Address": "01234567" + }, + "CustomerEndpoint": { + "Type": "TELEPHONE_NUMBER", + "Address": "+14802021091" + }, + "Queue": { + "Name": "PrimaryPhoneQueue", + "ARN": "" + }, + "Attributes": { + "key1": "value", + "key2": "value" + } + } + } +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-controltower-create-managed-account.json b/lambda-events/src/fixtures/example-controltower-create-managed-account.json new file mode 100644 index 000000000..d42d5387b --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-create-managed-account.json @@ -0,0 +1,32 @@ +{ + "eventVersion": "1.05", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "CreateManagedAccount", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "serviceEventDetails": { + "createManagedAccountStatus": { + "organizationalUnit": { + "organizationalUnitName": "Sandbox", + "organizationalUnitId": "ou-abcd-12345678" + }, + "account": { + "accountName": "TestAccount", + "accountId": "987654321098" + }, + "state": "SUCCEEDED", + "message": "AWS Control Tower successfully created a managed account.", + "requestedTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + } +} diff --git a/lambda-events/src/fixtures/example-controltower-deregister-organizational-unit.json b/lambda-events/src/fixtures/example-controltower-deregister-organizational-unit.json new file mode 100644 index 000000000..c4c78c738 --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-deregister-organizational-unit.json @@ -0,0 +1,28 @@ +{ + "eventVersion": "1.05", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "DeregisterOrganizationalUnit", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "serviceEventDetails": { + "deregisterOrganizationalUnitStatus": { + "state": "SUCCEEDED", + "message": "AWS Control Tower successfully deregistered an organizational unit.", + "organizationalUnit": { + "organizationalUnitName": "Production", + "organizationalUnitId": "ou-abcd-33333333" + }, + "requestedTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + } +} diff --git a/lambda-events/src/fixtures/example-controltower-disable-baseline.json b/lambda-events/src/fixtures/example-controltower-disable-baseline.json new file mode 100644 index 000000000..fcdba1096 --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-disable-baseline.json @@ -0,0 +1,51 @@ +{ + "eventVersion": "1.11", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "DisableBaseline", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "requestParameters": null, + "responseElements": null, + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "managementEvent": true, + "recipientAccountId": "123456789012", + "serviceEventDetails": { + "disableBaselineStatus": { + "enabledBaselineDetails": { + "arn": "arn:aws:controltower:us-east-1:123456789012:enabledbaseline/ABCDEFGHIJKLMNOP", + "parentIdentifier": "arn:aws:organizations::123456789012:ou/o-abcde12345/ou-abcd-12345678", + "targetIdentifier": "arn:aws:organizations::123456789012:ou/o-abcde12345/ou-abcd-33333333", + "baselineIdentifier": "arn:aws:controltower:us-east-1::baseline/ABCDE12345678", + "baselineVersion": "1.0", + "statusSummary": { + "lastOperationIdentifier": "aaaabbbb-cccc-dddd-eeee-ffffffffffff", + "status": "SUCCEEDED" + }, + "parameters": [] + }, + "baselineDetails": { + "arn": "arn:aws:controltower:us-east-1:123456789012:enabledbaseline/ABCDEFGHIJKLMNOP", + "parentIdentifier": "arn:aws:organizations::123456789012:ou/o-abcde12345/ou-abcd-12345678", + "targetIdentifier": "arn:aws:organizations::123456789012:ou/o-abcde12345/ou-abcd-33333333", + "baselineIdentifier": "arn:aws:controltower:us-east-1::baseline/ABCDE12345678", + "baselineVersion": "1.0", + "statusSummary": { + "lastOperationIdentifier": "aaaabbbb-cccc-dddd-eeee-ffffffffffff", + "status": "SUCCEEDED" + }, + "parameters": [] + }, + "requestedTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + }, + "eventCategory": "Management" +} diff --git a/lambda-events/src/fixtures/example-controltower-disable-guardrail.json b/lambda-events/src/fixtures/example-controltower-disable-guardrail.json new file mode 100644 index 000000000..bfd089131 --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-disable-guardrail.json @@ -0,0 +1,36 @@ +{ + "eventVersion": "1.05", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "DisableGuardrail", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "serviceEventDetails": { + "disableGuardrailStatus": { + "organizationalUnits": [ + { + "organizationalUnitName": "Sandbox", + "organizationalUnitId": "ou-abcd-12345678" + } + ], + "guardrails": [ + { + "guardrailId": "AWS-GR_ENCRYPTED_VOLUMES", + "guardrailBehavior": "DETECTIVE" + } + ], + "state": "SUCCEEDED", + "message": "AWS Control Tower successfully disabled a guardrail on an organizational unit.", + "requestTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + } +} diff --git a/lambda-events/src/fixtures/example-controltower-enable-baseline.json b/lambda-events/src/fixtures/example-controltower-enable-baseline.json new file mode 100644 index 000000000..999e69c7e --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-enable-baseline.json @@ -0,0 +1,48 @@ +{ + "eventVersion": "1.11", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "EnableBaseline", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "requestParameters": null, + "responseElements": null, + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "managementEvent": true, + "recipientAccountId": "123456789012", + "serviceEventDetails": { + "enableBaselineStatus": { + "enabledBaselineDetails": { + "arn": "arn:aws:controltower:us-east-1:123456789012:enabledbaseline/ABCDEFGHIJKLMNOP", + "parentIdentifier": "arn:aws:organizations::123456789012:ou/o-abcde12345/ou-abcd-12345678", + "targetIdentifier": "arn:aws:organizations::123456789012:ou/o-abcde12345/ou-abcd-33333333", + "baselineIdentifier": "arn:aws:controltower:us-east-1::baseline/ABCDE12345678", + "baselineVersion": "1.0", + "statusSummary": { + "lastOperationIdentifier": "aaaabbbb-cccc-dddd-eeee-ffffffffffff", + "status": "SUCCEEDED" + }, + "parameters": [ + { + "key": "IdentityCenterEnabledBaselineArn", + "value": { + "untyped": { + "object": "arn:aws:controltower:us-east-1:123456789012:enabledbaseline/QRSTUVWXYZ123456" + } + } + } + ] + }, + "requestedTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + }, + "eventCategory": "Management" +} diff --git a/lambda-events/src/fixtures/example-controltower-enable-guardrail.json b/lambda-events/src/fixtures/example-controltower-enable-guardrail.json new file mode 100644 index 000000000..ce8e89c8a --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-enable-guardrail.json @@ -0,0 +1,36 @@ +{ + "eventVersion": "1.05", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "EnableGuardrail", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "serviceEventDetails": { + "enableGuardrailStatus": { + "organizationalUnits": [ + { + "organizationalUnitName": "Sandbox", + "organizationalUnitId": "ou-abcd-12345678" + } + ], + "guardrails": [ + { + "guardrailId": "AWS-GR_ENCRYPTED_VOLUMES", + "guardrailBehavior": "DETECTIVE" + } + ], + "state": "SUCCEEDED", + "message": "AWS Control Tower successfully enabled a guardrail on an organizational unit.", + "requestTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + } +} diff --git a/lambda-events/src/fixtures/example-controltower-precheck-organizational-unit.json b/lambda-events/src/fixtures/example-controltower-precheck-organizational-unit.json new file mode 100644 index 000000000..803434acb --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-precheck-organizational-unit.json @@ -0,0 +1,41 @@ +{ + "eventVersion": "1.08", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "PrecheckOrganizationalUnit", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "managementEvent": true, + "recipientAccountId": "123456789012", + "serviceEventDetails": { + "precheckOrganizationalUnitStatus": { + "organizationalUnit": { + "organizationalUnitName": "Production", + "organizationalUnitId": "ou-abcd-33333333", + "failedPrechecks": [] + }, + "accounts": [ + { + "accountName": "ProdAccount1", + "accountId": "333333333333", + "failedPrechecks": [ + "CONFIG_RECORDER_CHECK" + ] + } + ], + "state": "FAILED", + "message": "AWS Control Tower failed the precheck for the organizational unit.", + "requestedTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + }, + "eventCategory": "Management" +} diff --git a/lambda-events/src/fixtures/example-controltower-register-organizational-unit.json b/lambda-events/src/fixtures/example-controltower-register-organizational-unit.json new file mode 100644 index 000000000..7df2d25a6 --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-register-organizational-unit.json @@ -0,0 +1,28 @@ +{ + "eventVersion": "1.05", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "RegisterOrganizationalUnit", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "serviceEventDetails": { + "registerOrganizationalUnitStatus": { + "state": "SUCCEEDED", + "message": "AWS Control Tower successfully registered an organizational unit.", + "organizationalUnit": { + "organizationalUnitName": "Production", + "organizationalUnitId": "ou-abcd-33333333" + }, + "requestedTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + } +} diff --git a/lambda-events/src/fixtures/example-controltower-reset-enabled-baseline.json b/lambda-events/src/fixtures/example-controltower-reset-enabled-baseline.json new file mode 100644 index 000000000..eb6bd36d5 --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-reset-enabled-baseline.json @@ -0,0 +1,39 @@ +{ + "eventVersion": "1.11", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "ResetEnabledBaseline", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "requestParameters": null, + "responseElements": null, + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "managementEvent": true, + "recipientAccountId": "123456789012", + "serviceEventDetails": { + "resetEnabledBaselineStatus": { + "enabledBaselineDetails": { + "arn": "arn:aws:controltower:us-east-1:123456789012:enabledbaseline/ABCDEFGHIJKLMNOP", + "parentIdentifier": "arn:aws:organizations::123456789012:ou/o-abcde12345/ou-abcd-12345678", + "targetIdentifier": "arn:aws:organizations::123456789012:ou/o-abcde12345/ou-abcd-33333333", + "baselineIdentifier": "arn:aws:controltower:us-east-1::baseline/ABCDE12345678", + "baselineVersion": "1.0", + "statusSummary": { + "lastOperationIdentifier": "aaaabbbb-cccc-dddd-eeee-ffffffffffff", + "status": "SUCCEEDED" + }, + "parameters": [] + }, + "requestedTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + }, + "eventCategory": "Management" +} diff --git a/lambda-events/src/fixtures/example-controltower-setup-landing-zone.json b/lambda-events/src/fixtures/example-controltower-setup-landing-zone.json new file mode 100644 index 000000000..eab6e8191 --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-setup-landing-zone.json @@ -0,0 +1,45 @@ +{ + "eventVersion": "1.05", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "SetupLandingZone", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "serviceEventDetails": { + "setupLandingZoneStatus": { + "state": "SUCCEEDED", + "message": "AWS Control Tower successfully set up a new landing zone.", + "rootOrganizationalId": "r-abcd", + "organizationalUnits": [ + { + "organizationalUnitName": "Security", + "organizationalUnitId": "ou-abcd-11111111" + }, + { + "organizationalUnitName": "Sandbox", + "organizationalUnitId": "ou-abcd-22222222" + } + ], + "accounts": [ + { + "accountName": "Audit", + "accountId": "111111111111" + }, + { + "accountName": "Log archive", + "accountId": "222222222222" + } + ], + "requestedTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + } +} diff --git a/lambda-events/src/fixtures/example-controltower-update-enabled-baseline.json b/lambda-events/src/fixtures/example-controltower-update-enabled-baseline.json new file mode 100644 index 000000000..76bc40aad --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-update-enabled-baseline.json @@ -0,0 +1,48 @@ +{ + "eventVersion": "1.11", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "UpdateEnabledBaseline", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "requestParameters": null, + "responseElements": null, + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "managementEvent": true, + "recipientAccountId": "123456789012", + "serviceEventDetails": { + "updateEnabledBaselineStatus": { + "enabledBaselineDetails": { + "arn": "arn:aws:controltower:us-east-1:123456789012:enabledbaseline/ABCDEFGHIJKLMNOP", + "parentIdentifier": "arn:aws:organizations::123456789012:ou/o-abcde12345/ou-abcd-12345678", + "targetIdentifier": "arn:aws:organizations::123456789012:ou/o-abcde12345/ou-abcd-33333333", + "baselineIdentifier": "arn:aws:controltower:us-east-1::baseline/ABCDE12345678", + "baselineVersion": "2.0", + "statusSummary": { + "lastOperationIdentifier": "aaaabbbb-cccc-dddd-eeee-ffffffffffff", + "status": "SUCCEEDED" + }, + "parameters": [ + { + "key": "IdentityCenterEnabledBaselineArn", + "value": { + "untyped": { + "object": "arn:aws:controltower:us-east-1:123456789012:enabledbaseline/QRSTUVWXYZ123456" + } + } + } + ] + }, + "requestedTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + }, + "eventCategory": "Management" +} diff --git a/lambda-events/src/fixtures/example-controltower-update-landing-zone.json b/lambda-events/src/fixtures/example-controltower-update-landing-zone.json new file mode 100644 index 000000000..f99c3dc37 --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-update-landing-zone.json @@ -0,0 +1,45 @@ +{ + "eventVersion": "1.05", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "UpdateLandingZone", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "serviceEventDetails": { + "updateLandingZoneStatus": { + "state": "SUCCEEDED", + "message": "AWS Control Tower successfully updated a landing zone.", + "rootOrganizationalId": "r-abcd", + "organizationalUnits": [ + { + "organizationalUnitName": "Security", + "organizationalUnitId": "ou-abcd-11111111" + }, + { + "organizationalUnitName": "Sandbox", + "organizationalUnitId": "ou-abcd-22222222" + } + ], + "accounts": [ + { + "accountName": "Audit", + "accountId": "111111111111" + }, + { + "accountName": "Log archive", + "accountId": "222222222222" + } + ], + "requestedTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + } +} diff --git a/lambda-events/src/fixtures/example-controltower-update-managed-account.json b/lambda-events/src/fixtures/example-controltower-update-managed-account.json new file mode 100644 index 000000000..9a082f7b6 --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-update-managed-account.json @@ -0,0 +1,32 @@ +{ + "eventVersion": "1.05", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "UpdateManagedAccount", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "serviceEventDetails": { + "updateManagedAccountStatus": { + "organizationalUnit": { + "organizationalUnitName": "Production", + "organizationalUnitId": "ou-abcd-33333333" + }, + "account": { + "accountName": "TestAccount", + "accountId": "987654321098" + }, + "state": "SUCCEEDED", + "message": "AWS Control Tower successfully updated a managed account.", + "requestedTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + } +} diff --git a/lambda-events/src/fixtures/example-documentdb-delete-event.json b/lambda-events/src/fixtures/example-documentdb-delete-event.json new file mode 100644 index 000000000..fd9259da1 --- /dev/null +++ b/lambda-events/src/fixtures/example-documentdb-delete-event.json @@ -0,0 +1,30 @@ +{ + "eventSourceArn": "arn:aws:rds:us-east-1:123456789012:cluster:canaryclusterb2a659a2-qo5tcmqkcl03", + "events": [ + { + "event": { + "_id": { + "_data": "0163eeb6e7000000090100000009000041e1" + }, + "clusterTime": { + "$timestamp": { + "t": 1676588775, + "i": 9 + } + }, + "documentKey": { + "_id": { + "$oid": "63eeb6e7d418cd98afb1c1d7" + } + }, + "ns": { + "db": "test_database", + "coll": "test_collection" + }, + "operationType": "delete" + } + } + ], + "eventSource": "aws:docdb" + } + \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-documentdb-drop-database-event.json b/lambda-events/src/fixtures/example-documentdb-drop-database-event.json new file mode 100644 index 000000000..77a1cb932 --- /dev/null +++ b/lambda-events/src/fixtures/example-documentdb-drop-database-event.json @@ -0,0 +1,24 @@ +{ + "eventSourceArn": "arn:aws:rds:us-east-1:123456789012:cluster:canaryclusterb2a659a2-qo5tcmqkcl03", + "events": [ + { + "event": { + "_id": { + "_data": "0163eeb6e7000000090100000009000041e1" + }, + "clusterTime": { + "$timestamp": { + "t": 1676588775, + "i": 9 + } + }, + "ns": { + "db": "test_database" + }, + "operationType": "dropDatabase" + } + } + ], + "eventSource": "aws:docdb" + } + \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-documentdb-drop-event.json b/lambda-events/src/fixtures/example-documentdb-drop-event.json new file mode 100644 index 000000000..89d8cc8fd --- /dev/null +++ b/lambda-events/src/fixtures/example-documentdb-drop-event.json @@ -0,0 +1,30 @@ +{ + "eventSourceArn": "arn:aws:rds:us-east-1:123456789012:cluster:canaryclusterb2a659a2-qo5tcmqkcl03", + "events": [ + { + "event": { + "_id": { + "_data": "0163eeb6e7000000090100000009000041e1" + }, + "clusterTime": { + "$timestamp": { + "t": 1676588775, + "i": 9 + } + }, + "documentKey": { + "_id": { + "$oid": "63eeb6e7d418cd98afb1c1d7" + } + }, + "ns": { + "db": "test_database", + "coll": "test_collection" + }, + "operationType": "drop" + } + } + ], + "eventSource": "aws:docdb" + } + \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-documentdb-insert-event.json b/lambda-events/src/fixtures/example-documentdb-insert-event.json new file mode 100644 index 000000000..cd03e374c --- /dev/null +++ b/lambda-events/src/fixtures/example-documentdb-insert-event.json @@ -0,0 +1,29 @@ +{ + "eventSourceArn": "arn:aws:rds:us-east-1:123456789012:cluster:canaryclusterb2a659a2-qo5tcmqkcl03", + "events": [ + { + "event": { + "_id": { + "_data": "0163eeb6e7000000090100000009000041e1" + }, + "clusterTime": { + "$timestamp": { + "t": 1676588775, + "i": 9 + } + }, + "documentKey": { + "_id": { + "$oid": "63eeb6e7d418cd98afb1c1d7" + } + }, + "ns": { + "db": "test_database", + "coll": "test_collection" + }, + "operationType": "insert" + } + } + ], + "eventSource": "aws:docdb" +} diff --git a/lambda-events/src/fixtures/example-documentdb-invalidate-event.json b/lambda-events/src/fixtures/example-documentdb-invalidate-event.json new file mode 100644 index 000000000..59f5af65b --- /dev/null +++ b/lambda-events/src/fixtures/example-documentdb-invalidate-event.json @@ -0,0 +1,20 @@ +{ + "eventSourceArn": "arn:aws:rds:us-east-1:123456789012:cluster:canaryclusterb2a659a2-qo5tcmqkcl03", + "events": [ + { + "event": { + "_id": { + "_data": "0163eeb6e7000000090100000009000041e1" + }, + "clusterTime": { + "$timestamp": { + "t": 1676588775, + "i": 9 + } + }, + "operationType": "invalidate" + } + } + ], + "eventSource": "aws:docdb" + } \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-documentdb-rename-event.json b/lambda-events/src/fixtures/example-documentdb-rename-event.json new file mode 100644 index 000000000..654164703 --- /dev/null +++ b/lambda-events/src/fixtures/example-documentdb-rename-event.json @@ -0,0 +1,33 @@ +{ + "eventSourceArn": "arn:aws:rds:us-east-1:123456789012:cluster:canaryclusterb2a659a2-qo5tcmqkcl03", + "events": [ + { + "event": { + "_id": { + "_data": "0163eeb6e7000000090100000009000041e1" + }, + "clusterTime": { + "$timestamp": { + "t": 1676588775, + "i": 9 + } + }, + "documentKey": { + "_id": { + "$oid": "63eeb6e7d418cd98afb1c1d7" + } + }, + "ns": { + "db": "test_database", + "coll": "test_collection" + }, + "to": { + "db": "test_database_new", + "coll": "test_collection_new" + }, + "operationType": "rename" + } + } + ], + "eventSource": "aws:docdb" +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-documentdb-replace-event.json b/lambda-events/src/fixtures/example-documentdb-replace-event.json new file mode 100644 index 000000000..1c7fe559c --- /dev/null +++ b/lambda-events/src/fixtures/example-documentdb-replace-event.json @@ -0,0 +1,29 @@ +{ + "eventSourceArn": "arn:aws:rds:us-east-1:123456789012:cluster:canaryclusterb2a659a2-qo5tcmqkcl03", + "events": [ + { + "event": { + "_id": { + "_data": "0163eeb6e7000000090100000009000041e1" + }, + "operationType": "replace", + "clusterTime": { + "$timestamp": { + "t": 1676588775, + "i": 9 + } + }, + "ns": { + "db": "engineering", + "coll": "users" + }, + "documentKey": { + "_id": { + "$oid": "63eeb6e7d418cd98afb1c1d7" + } + } + } + } + ], + "eventSource": "aws:docdb" +} diff --git a/lambda-events/src/fixtures/example-documentdb-update-event.json b/lambda-events/src/fixtures/example-documentdb-update-event.json new file mode 100644 index 000000000..38f3e6590 --- /dev/null +++ b/lambda-events/src/fixtures/example-documentdb-update-event.json @@ -0,0 +1,43 @@ +{ + "eventSourceArn": "arn:aws:rds:us-east-1:123456789012:cluster:canaryclusterb2a659a2-qo5tcmqkcl03", + "events": [ + { + "event": { + "_id": { + "_data": "0163eeb6e7000000090100000009000041e1" + }, + "operationType": "update", + "clusterTime": { + "$timestamp": { + "t": 1676588775, + "i": 9 + } + }, + "ns": { + "db": "test_database", + "coll": "test_collection" + }, + "documentKey": { + "_id": { + "$oid": "63eeb6e7d418cd98afb1c1d7" + } + }, + "updateDescription": { + "updatedFields": { + "email": "alice@10gen.com" + }, + "removedFields": [ + "phoneNumber" + ], + "truncatedArrays": [ + { + "field": "vacation_time", + "newSize": 36 + } + ] + } + } + } + ], + "eventSource": "aws:docdb" +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-dynamodb-event-null-items.json b/lambda-events/src/fixtures/example-dynamodb-event-null-items.json new file mode 100644 index 000000000..ea24565e2 --- /dev/null +++ b/lambda-events/src/fixtures/example-dynamodb-event-null-items.json @@ -0,0 +1,131 @@ +{ + "Records": [ + { + "eventID": "f07f8ca4b0b26cb9c4e5e77e69f274ee", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "us-east-1", + "userIdentity":{ + "type":"Service", + "principalId":"dynamodb.amazonaws.com" + }, + "dynamodb": { + "ApproximateCreationDateTime": 1480642020, + "Keys": null, + "NewImage": null, + "OldImage": null, + "SequenceNumber": "1405400000000002063282832", + "SizeBytes": 54, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/Example-Table/stream/2016-12-01T00:00:00.000" + }, + { + "eventID": "f07f8ca4b0b26cb9c4e5e77e42f274ee", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "us-east-1", + "dynamodb": { + "ApproximateCreationDateTime": 1480642020, + "Keys": { + "val": { + "S": "data" + }, + "key": { + "S": "binary" + } + }, + "NewImage": { + "val": { + "S": "data" + }, + "asdf1": { + "B": "AAEqQQ==" + }, + "b2": { + "B": "test" + }, + "asdf2": { + "BS": [ + "AAEqQQ==", + "QSoBAA==", + "AAEqQQ==" + ] + }, + "key": { + "S": "binary" + }, + "Binary": { + "B": "AAEqQQ==" + }, + "Boolean": { + "BOOL": true + }, + "BinarySet": { + "BS": [ + "AAEqQQ==", + "AAEqQQ==" + ] + }, + "List": { + "L": [ + { + "S": "Cookies" + }, + { + "S": "Coffee" + }, + { + "N": "3.14159" + } + ] + }, + "Map": { + "M": { + "Name": { + "S": "Joe" + }, + "Age": { + "N": "35" + } + } + }, + "FloatNumber": { + "N": "123.45" + }, + "IntegerNumber": { + "N": "123" + }, + "NumberSet": { + "NS": [ + "1234", + "567.8" + ] + }, + "Null": { + "NULL": true + }, + "String": { + "S": "Hello" + }, + "StringSet": { + "SS": [ + "Giraffe", + "Zebra" + ] + }, + "EmptyStringSet": { + "SS": [] + } + }, + "SequenceNumber": "1405400000000002063282832", + "SizeBytes": 54, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/Example-Table/stream/2016-12-01T00:00:00.000" + } + ] +} + diff --git a/lambda-events/src/fixtures/example-dynamodb-event-record-with-optional-fields.json b/lambda-events/src/fixtures/example-dynamodb-event-record-with-optional-fields.json new file mode 100644 index 000000000..4f468c705 --- /dev/null +++ b/lambda-events/src/fixtures/example-dynamodb-event-record-with-optional-fields.json @@ -0,0 +1,25 @@ +{ + "awsRegion":"eu-west-1", + "eventID":"00000000-0000-0000-0000-000000000000", + "eventName":"INSERT", + "userIdentity":null, + "recordFormat":"application/json", + "tableName":"examples", + "dynamodb":{ + "Keys":{ + "id":{ + "S":"00000000-0000-0000-0000-000000000000" + } + }, + "NewImage":{ + "id":{ + "S":"00000000-0000-0000-0000-000000000000" + }, + "created":{ + "S":"2022-02-16T15:12:00.14Z" + } + }, + "SizeBytes":292 + }, + "eventSource":"aws:dynamodb" +} diff --git a/lambda-events/src/fixtures/example-dynamodb-event.json b/lambda-events/src/fixtures/example-dynamodb-event.json new file mode 100644 index 000000000..46a17695c --- /dev/null +++ b/lambda-events/src/fixtures/example-dynamodb-event.json @@ -0,0 +1,153 @@ +{ + "Records": [ + { + "eventID": "f07f8ca4b0b26cb9c4e5e77e69f274ee", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "us-east-1", + "userIdentity":{ + "type":"Service", + "principalId":"dynamodb.amazonaws.com" + }, + "dynamodb": { + "ApproximateCreationDateTime": 1480642020, + "Keys": { + "val": { + "S": "data" + }, + "key": { + "S": "binary" + } + }, + "NewImage": { + "val": { + "S": "data" + }, + "asdf1": { + "B": "AAEqQQ==" + }, + "asdf2": { + "BS": [ + "AAEqQQ==", + "QSoBAA==" + ] + }, + "key": { + "S": "binary" + } + }, + "SequenceNumber": "1405400000000002063282832", + "SizeBytes": 54, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/Example-Table/stream/2016-12-01T00:00:00.000" + }, + { + "eventID": "f07f8ca4b0b26cb9c4e5e77e42f274ee", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "us-east-1", + "dynamodb": { + "ApproximateCreationDateTime": 1480642020, + "Keys": { + "val": { + "S": "data" + }, + "key": { + "S": "binary" + } + }, + "NewImage": { + "val": { + "S": "data" + }, + "asdf1": { + "B": "AAEqQQ==" + }, + "b2": { + "B": "test" + }, + "asdf2": { + "BS": [ + "AAEqQQ==", + "QSoBAA==", + "AAEqQQ==" + ] + }, + "key": { + "S": "binary" + }, + "Binary": { + "B": "AAEqQQ==" + }, + "Boolean": { + "BOOL": true + }, + "BinarySet": { + "BS": [ + "AAEqQQ==", + "AAEqQQ==" + ] + }, + "List": { + "L": [ + { + "S": "Cookies" + }, + { + "S": "Coffee" + }, + { + "N": "3.14159" + } + ] + }, + "Map": { + "M": { + "Name": { + "S": "Joe" + }, + "Age": { + "N": "35" + } + } + }, + "FloatNumber": { + "N": "123.45" + }, + "IntegerNumber": { + "N": "123" + }, + "NumberSet": { + "NS": [ + "1234", + "567.8" + ] + }, + "Null": { + "NULL": true + }, + "String": { + "S": "Hello" + }, + "StringSet": { + "SS": [ + "Giraffe", + "Zebra" + ] + }, + "EmptyStringSet": { + "SS": [] + } + }, + "SequenceNumber": "1405400000000002063282832", + "SizeBytes": 54, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/Example-Table/stream/2016-12-01T00:00:00.000" + } + ] +} + diff --git a/lambda-events/src/fixtures/example-ecr-image-scan-event-with-missing-severities.json b/lambda-events/src/fixtures/example-ecr-image-scan-event-with-missing-severities.json new file mode 100644 index 000000000..883eb2d3c --- /dev/null +++ b/lambda-events/src/fixtures/example-ecr-image-scan-event-with-missing-severities.json @@ -0,0 +1,22 @@ +{ + "version": "0", + "id": "85fc3613-e913-7fc4-a80c-a3753e4aa9ae", + "detail-type": "ECR Image Scan", + "source": "aws.ecr", + "account": "123456789012", + "time": "2019-10-29T02:36:48Z", + "region": "us-east-1", + "resources": [ + "arn:aws:ecr:us-east-1:123456789012:repository/my-repository-name" + ], + "detail": { + "scan-status": "COMPLETE", + "repository-name": "my-repository-name", + "finding-severity-counts": { + "CRITICAL": 10, + "MEDIUM": 9 + }, + "image-digest": "sha256:7f5b2640fe6fb4f46592dfd3410c4a79dac4f89e4782432e0378abcd1234", + "image-tags": [] + } +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-ecr-image-scan-event.json b/lambda-events/src/fixtures/example-ecr-image-scan-event.json new file mode 100644 index 000000000..219b13a34 --- /dev/null +++ b/lambda-events/src/fixtures/example-ecr-image-scan-event.json @@ -0,0 +1,24 @@ +{ + "version": "0", + "id": "01234567-0123-0123-0123-012345678901", + "detail-type": "ECR Image Scan", + "source": "aws.ecr", + "account": "123456789012", + "time": "2019-10-30T21:32:27Z", + "region": "eu-north-1", + "resources": ["arn:aws:ecr:eu-north-1:123456789012:repository/tribble-image-scan-test"], + "detail": { + "scan-status": "COMPLETE", + "repository-name": "tribble-image-scan-test", + "finding-severity-counts": { + "CRITICAL": 10, + "HIGH": 2, + "MEDIUM": 9, + "LOW": 3, + "INFORMATIONAL": 0, + "UNDEFINED": 0 + }, + "image-digest": "sha256:d4a96ee9443e641fc100e763a0c10928720b50c6e3ea3342d05d7c3435fc5355", + "image-tags": ["1572471135"] + } +} diff --git a/lambda-events/src/fixtures/example-eventbridge-event-obj.json b/lambda-events/src/fixtures/example-eventbridge-event-obj.json new file mode 100644 index 000000000..e9b269684 --- /dev/null +++ b/lambda-events/src/fixtures/example-eventbridge-event-obj.json @@ -0,0 +1,15 @@ +{ + "id": "7bf73129-1428-4cd3-a780-95db273d1602", + "detail-type": "EC2 Instance State-change Notification", + "source": "aws.ec2", + "account": "123456789012", + "time": "2021-11-11T21:29:54Z", + "region": "us-east-1", + "resources": [ + "arn:aws:ec2:us-east-1:123456789012:instance/i-abcd1111" + ], + "detail": { + "instance-id": "i-abcd1111", + "state": "pending" + } +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-eventbridge-schedule.json b/lambda-events/src/fixtures/example-eventbridge-schedule.json new file mode 100644 index 000000000..602f83d28 --- /dev/null +++ b/lambda-events/src/fixtures/example-eventbridge-schedule.json @@ -0,0 +1,13 @@ +{ + "version": "0", + "id": "53dc4d37-cffa-4f76-80c9-8b7d4a4d2eaa", + "detail-type": "Scheduled Event", + "source": "aws.events", + "account": "123456789012", + "time": "2015-10-08T16:53:06Z", + "region": "us-east-1", + "resources": [ + "arn:aws:events:us-east-1:123456789012:rule/my-scheduled-rule" + ], + "detail": {} +} diff --git a/lambda-events/src/fixtures/example-firehose-event.json b/lambda-events/src/fixtures/example-firehose-event.json new file mode 100644 index 000000000..8874a5683 --- /dev/null +++ b/lambda-events/src/fixtures/example-firehose-event.json @@ -0,0 +1,33 @@ +{ + "invocationId": "invoked123", + "deliveryStreamArn": "aws:lambda:events", + "sourceKinesisStreamArn": "arn:aws:kinesis:us-east-1:123456789012:stream/test", + "region": "us-west-2", + "records": [ + { + "data": "SGVsbG8gV29ybGQ=", + "recordId": "record1", + "approximateArrivalTimestamp": 1507217624302, + "kinesisRecordMetadata": { + "shardId": "shardId-000000000000", + "partitionKey": "4d1ad2b9-24f8-4b9d-a088-76e9947c317a", + "approximateArrivalTimestamp": 1507217624302, + "sequenceNumber": "49546986683135544286507457936321625675700192471156785154", + "subsequenceNumber": 123456 + } + }, + { + "data": "SGVsbG8gV29ybGQ=", + "recordId": "record2", + "approximateArrivalTimestamp": 1507217624302, + "kinesisRecordMetadata": { + "shardId": "shardId-000000000001", + "partitionKey": "4d1ad2b9-24f8-4b9d-a088-76e9947c318a", + "approximateArrivalTimestamp": 1507217624302, + "sequenceNumber": "49546986683135544286507457936321625675700192471156785155", + "subsequenceNumber": 123457 + } + } + ] + } + diff --git a/lambda-events/src/fixtures/example-iot-custom-auth-request-x509.json b/lambda-events/src/fixtures/example-iot-custom-auth-request-x509.json new file mode 100644 index 000000000..cd31955fc --- /dev/null +++ b/lambda-events/src/fixtures/example-iot-custom-auth-request-x509.json @@ -0,0 +1,21 @@ +{ + "token": "aToken", + "signatureVerified": true, + "protocols": [ + "tls", + "mqtt" + ], + "protocolData": { + "tls": { + "serverName": "serverName", + "x509CertificatePem": "x509CertificatePem", + "principalId": "principalId" + }, + "mqtt": { + "clientId": "myClientId" + } + }, + "connectionMetadata": { + "id": "UUID" + } +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-iot-custom-auth-request.json b/lambda-events/src/fixtures/example-iot-custom-auth-request.json new file mode 100644 index 000000000..582c0e23d --- /dev/null +++ b/lambda-events/src/fixtures/example-iot-custom-auth-request.json @@ -0,0 +1,25 @@ +{ + "token" :"aToken", + "signatureVerified": true, + "protocols": ["tls", "http", "mqtt"], + "protocolData": { + "tls" : { + "serverName": "serverName" + }, + "http": { + "headers": { + "X-Request-ID": "abc123" + }, + "queryString": "?foo=bar" + }, + "mqtt": { + "username": "myUserName", + "password": "bXlQYXNzd29yZA==", + "clientId": "myClientId" + } + }, + "connectionMetadata": { + "id": "e56f08c3-c559-490f-aa9f-7e8427d0f57b" + } +} + diff --git a/lambda-events/src/fixtures/example-iot-custom-auth-response.json b/lambda-events/src/fixtures/example-iot-custom-auth-response.json new file mode 100644 index 000000000..61983975c --- /dev/null +++ b/lambda-events/src/fixtures/example-iot-custom-auth-response.json @@ -0,0 +1,19 @@ +{ + "isAuthenticated":true, + "principalId": "xxxxxxxx", + "disconnectAfterInSeconds": 86400, + "refreshAfterInSeconds": 300, + "policyDocuments": [ + { + "Version": "2012-10-17", + "Statement": [ + { + "Action": ["iot:Publish"], + "Effect": "Allow", + "Resource": ["arn:aws:iot:us-east-1::topic/customauthtesting"] + } + ] + } + ] +} + diff --git a/lambda-events/src/fixtures/example-iot_1_click-event.json b/lambda-events/src/fixtures/example-iot_1_click-event.json new file mode 100644 index 000000000..1f7eefea5 --- /dev/null +++ b/lambda-events/src/fixtures/example-iot_1_click-event.json @@ -0,0 +1,30 @@ +{ + "deviceEvent": { + "buttonClicked": { + "clickType": "SINGLE", + "reportedTime": "2018-05-04T23:26:33.747Z" + } + }, + "deviceInfo": { + "attributes": { + "key3": "value3", + "key1": "value1", + "key4": "value4" + }, + "type": "button", + "deviceId": "G030PMXXXXXXXXXX", + "remainingLife": 5.00 + }, + "placementInfo": { + "projectName": "test", + "placementName": "myPlacement", + "attributes": { + "location": "Seattle", + "equipment": "printer" + }, + "devices": { + "myButton": "G030PMXXXXXXXXXX" + } + } +} + diff --git a/lambda-events/src/fixtures/example-iot_button-event.json b/lambda-events/src/fixtures/example-iot_button-event.json new file mode 100644 index 000000000..1ffcef529 --- /dev/null +++ b/lambda-events/src/fixtures/example-iot_button-event.json @@ -0,0 +1,5 @@ +{ + "serialNumber": "ABCDEFG12345", + "clickType": "SINGLE", + "batteryVoltage": "2000 mV" +} diff --git a/lambda-events/src/fixtures/example-kafka-event.json b/lambda-events/src/fixtures/example-kafka-event.json new file mode 100644 index 000000000..97491c88f --- /dev/null +++ b/lambda-events/src/fixtures/example-kafka-event.json @@ -0,0 +1,24 @@ +{ + "eventSource": "aws:kafka", + "eventSourceArn": "arn:aws:kafka:us-west-2:012345678901:cluster/ExampleMSKCluster/e9f754c6-d29a-4430-a7db-958a19fd2c54-4", + "bootstrapServers": "b-2.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092,b-1.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092", + "records": { + "AWSKafkaTopic-0": [ + { + "topic": "AWSKafkaTopic", + "partition": 0, + "offset": 0, + "timestamp": 1595035749700, + "timestampType": "CREATE_TIME", + "key": "OGQ1NTk2YjQtMTgxMy00MjM4LWIyNGItNmRhZDhlM2QxYzBj", + "value": "OGQ1NTk2YjQtMTgxMy00MjM4LWIyNGItNmRhZDhlM2QxYzBj", + "headers": [ + { + "headerKey": [104, 101, 97, 100, 101, 114, 86, 97, 108, 117, 101] + } + ] + } + ] + } + } + diff --git a/lambda-events/src/fixtures/example-kinesis-event-encrypted.json b/lambda-events/src/fixtures/example-kinesis-event-encrypted.json new file mode 100644 index 000000000..7d1697b1b --- /dev/null +++ b/lambda-events/src/fixtures/example-kinesis-event-encrypted.json @@ -0,0 +1,37 @@ +{ + "Records": [ + { + "kinesis": { + "kinesisSchemaVersion": "1.0", + "partitionKey": "s1", + "sequenceNumber": "49568167373333333333333333333333333333333333333333333333", + "data": "SGVsbG8gV29ybGQ=", + "approximateArrivalTimestamp": 1480641523.477, + "encryptionType": "KMS" + }, + "eventSource": "aws:kinesis", + "eventVersion": "1.0", + "eventID": "shardId-000000000000:49568167373333333333333333333333333333333333333333333333", + "eventName": "aws:kinesis:record", + "invokeIdentityArn": "arn:aws:iam::123456789012:role/LambdaRole", + "awsRegion": "us-east-1", + "eventSourceARN": "arn:aws:kinesis:us-east-1:123456789012:stream/simple-stream" + }, + { + "kinesis": { + "kinesisSchemaVersion": "1.0", + "partitionKey": "s1", + "sequenceNumber": "49568167373333333334444444444444444444444444444444444444", + "data": "SGVsbG8gV29ybGQ=", + "approximateArrivalTimestamp": 1480841523.477 + }, + "eventSource": "aws:kinesis", + "eventVersion": "1.0", + "eventID": "shardId-000000000000:49568167373333333334444444444444444444444444444444444444", + "eventName": "aws:kinesis:record", + "invokeIdentityArn": "arn:aws:iam::123456789012:role/LambdaRole", + "awsRegion": "us-east-1", + "eventSourceARN": "arn:aws:kinesis:us-east-1:123456789012:stream/simple-stream" + } + ] +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-kinesis-event.json b/lambda-events/src/fixtures/example-kinesis-event.json new file mode 100644 index 000000000..be20b6f25 --- /dev/null +++ b/lambda-events/src/fixtures/example-kinesis-event.json @@ -0,0 +1,36 @@ +{ + "Records": [ + { + "kinesis": { + "kinesisSchemaVersion": "1.0", + "partitionKey": "s1", + "sequenceNumber": "49568167373333333333333333333333333333333333333333333333", + "data": "SGVsbG8gV29ybGQ=", + "approximateArrivalTimestamp": 1480641523.477 + }, + "eventSource": "aws:kinesis", + "eventVersion": "1.0", + "eventID": "shardId-000000000000:49568167373333333333333333333333333333333333333333333333", + "eventName": "aws:kinesis:record", + "invokeIdentityArn": "arn:aws:iam::123456789012:role/LambdaRole", + "awsRegion": "us-east-1", + "eventSourceARN": "arn:aws:kinesis:us-east-1:123456789012:stream/simple-stream" + }, + { + "kinesis": { + "kinesisSchemaVersion": "1.0", + "partitionKey": "s1", + "sequenceNumber": "49568167373333333334444444444444444444444444444444444444", + "data": "SGVsbG8gV29ybGQ=", + "approximateArrivalTimestamp": 1480841523.477 + }, + "eventSource": "aws:kinesis", + "eventVersion": "1.0", + "eventID": "shardId-000000000000:49568167373333333334444444444444444444444444444444444444", + "eventName": "aws:kinesis:record", + "invokeIdentityArn": "arn:aws:iam::123456789012:role/LambdaRole", + "awsRegion": "us-east-1", + "eventSourceARN": "arn:aws:kinesis:us-east-1:123456789012:stream/simple-stream" + } + ] +} diff --git a/lambda-events/src/fixtures/example-kinesis-firehose-event.json b/lambda-events/src/fixtures/example-kinesis-firehose-event.json new file mode 100644 index 000000000..8874a5683 --- /dev/null +++ b/lambda-events/src/fixtures/example-kinesis-firehose-event.json @@ -0,0 +1,33 @@ +{ + "invocationId": "invoked123", + "deliveryStreamArn": "aws:lambda:events", + "sourceKinesisStreamArn": "arn:aws:kinesis:us-east-1:123456789012:stream/test", + "region": "us-west-2", + "records": [ + { + "data": "SGVsbG8gV29ybGQ=", + "recordId": "record1", + "approximateArrivalTimestamp": 1507217624302, + "kinesisRecordMetadata": { + "shardId": "shardId-000000000000", + "partitionKey": "4d1ad2b9-24f8-4b9d-a088-76e9947c317a", + "approximateArrivalTimestamp": 1507217624302, + "sequenceNumber": "49546986683135544286507457936321625675700192471156785154", + "subsequenceNumber": 123456 + } + }, + { + "data": "SGVsbG8gV29ybGQ=", + "recordId": "record2", + "approximateArrivalTimestamp": 1507217624302, + "kinesisRecordMetadata": { + "shardId": "shardId-000000000001", + "partitionKey": "4d1ad2b9-24f8-4b9d-a088-76e9947c318a", + "approximateArrivalTimestamp": 1507217624302, + "sequenceNumber": "49546986683135544286507457936321625675700192471156785155", + "subsequenceNumber": 123457 + } + } + ] + } + diff --git a/lambda-events/src/fixtures/example-kinesis-firehose-response.json b/lambda-events/src/fixtures/example-kinesis-firehose-response.json new file mode 100644 index 000000000..31950770e --- /dev/null +++ b/lambda-events/src/fixtures/example-kinesis-firehose-response.json @@ -0,0 +1,32 @@ +{ + "records": [ + { + "data": "SGVsbG8gV29ybGQ=", + "recordId": "record1", + "result": "TRANSFORMED_STATE_OK", + "metadata": { + "partitionKeys": {} + } + }, + { + "data": "SGVsbG8gV29ybGQ=", + "recordId": "record2", + "result": "TRANSFORMED_STATE_DROPPED", + "metadata": { + "partitionKeys": {} + } + }, + { + "data": "SGVsbG8gV29ybGQ=", + "recordId": "record3", + "result": "TransformedStateOk", + "metadata": { + "partitionKeys": { + "iamKey1": "iamValue1", + "iamKey2": "iamValue2" + } + } + } + ] + } + diff --git a/lambda-events/src/fixtures/example-lex-event.json b/lambda-events/src/fixtures/example-lex-event.json new file mode 100644 index 000000000..ae995d9a6 --- /dev/null +++ b/lambda-events/src/fixtures/example-lex-event.json @@ -0,0 +1,44 @@ +{ + "currentIntent": { + "name": "intent-name", + "slots": { + "slot name1": "value1", + "slot name2": "value2" + }, + "slotDetails": { + "slot name1": { + "resolutions": [ + { "value1": "resolved value1" }, + { "value2": "resolved value2" } + ], + "originalValue": "original text" + }, + "slot name2": { + "resolutions": [ + { "value1": "resolved value1" }, + { "value2": "resolved value2" } + ], + "originalValue": "original text" + } + }, + "confirmationStatus": "None, Confirmed, or Denied (intent confirmation, if configured)" + }, + "bot": { + "name": "bot name", + "alias": "bot alias", + "version": "bot version" + }, + "userId": "User ID specified in the POST request to Amazon Lex.", + "inputTranscript": "Text used to process the request", + "invocationSource": "FulfillmentCodeHook or DialogCodeHook", + "outputDialogMode": "Text or Voice, based on ContentType request header in runtime API request", + "messageVersion": "1.0", + "sessionAttributes": { + "key1": "value1", + "key2": "value2" + }, + "requestAttributes": { + "key1": "value1", + "key2": "value2" + } +} diff --git a/lambda-events/src/fixtures/example-lex-response.json b/lambda-events/src/fixtures/example-lex-response.json new file mode 100644 index 000000000..b83cea2ea --- /dev/null +++ b/lambda-events/src/fixtures/example-lex-response.json @@ -0,0 +1,40 @@ +{ + "sessionAttributes": { + "key1": "value1", + "key2": "value2" + }, + "dialogAction": { + "type": "ElicitIntent, ElicitSlot, ConfirmIntent, Delegate, or Close", + "fulfillmentState": "Fulfilled or Failed", + "message": { + "contentType": "PlainText or SSML", + "content": "message to convey to the user" + }, + "intentName": "intent-name", + "slots": { + "slot-name1": "value1", + "slot-name2": "value2", + "slot-name3": "value3" + }, + "slotToElicit": "slot-name", + "responseCard": { + "version": 3, + "contentType": "application/vnd.amazonaws.card.generic", + "genericAttachments": [ + { + "title": "card-title", + "subTitle": "card-sub-title", + "imageUrl": "URL of the image to be shown", + "attachmentLinkUrl": "URL of the attachment to be associated with the card", + "buttons": [ + { + "text": "button-text", + "value": "value sent to server on button click" + } + ] + } + ] + } + } +} + diff --git a/lambda-events/src/fixtures/example-rabbitmq-event.json b/lambda-events/src/fixtures/example-rabbitmq-event.json new file mode 100644 index 000000000..49a8b5eff --- /dev/null +++ b/lambda-events/src/fixtures/example-rabbitmq-event.json @@ -0,0 +1,52 @@ +{ + "eventSource": "aws:rmq", + "eventSourceArn": "arn:aws:mq:us-west-2:112556298976:broker:test:b-9bcfa592-423a-4942-879d-eb284b418fc8", + "rmqMessagesByQueue": { + "test::/": [ + { + "basicProperties": { + "contentType": "text/plain", + "contentEncoding": null, + "headers": { + "header1": { + "bytes": [ + 118, + 97, + 108, + 117, + 101, + 49 + ] + }, + "header2": { + "bytes": [ + 118, + 97, + 108, + 117, + 101, + 50 + ] + }, + "numberInHeader": 10 + }, + "deliveryMode": 1, + "priority": 34, + "correlationId": null, + "replyTo": null, + "expiration": "60000", + "messageId": null, + "timestamp": "Jan 1, 1970, 12:33:41 AM", + "type": null, + "userId": "AIDACKCEVSQ6C2EXAMPLE", + "appId": null, + "clusterId": null, + "bodySize": 80 + }, + "redelivered": false, + "data": "eyJ0aW1lb3V0IjowLCJkYXRhIjoiQ1pybWYwR3c4T3Y0YnFMUXhENEUifQ==" + } + ] + } +} + diff --git a/lambda-events/src/fixtures/example-s3-event-with-decoded.json b/lambda-events/src/fixtures/example-s3-event-with-decoded.json new file mode 100644 index 000000000..a9c21c8ca --- /dev/null +++ b/lambda-events/src/fixtures/example-s3-event-with-decoded.json @@ -0,0 +1,41 @@ +{ + "Records": [ + { + "eventVersion": "2.0", + "eventSource": "aws:s3", + "awsRegion": "us-east-1", + "eventTime": "1970-01-01T00:00:00.123Z", + "eventName": "ObjectCreated:Put", + "userIdentity": { + "principalId": "EXAMPLE" + }, + "requestParameters": { + "sourceIPAddress": "127.0.0.1" + }, + "responseElements": { + "x-amz-request-id": "C3D13FE58DE4C810", + "x-amz-id-2": "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD" + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "testConfigRule", + "bucket": { + "name": "sourcebucket", + "ownerIdentity": { + "principalId": "EXAMPLE" + }, + "arn": "arn:aws:s3:::mybucket" + }, + "object": { + "key": "Happy%20Face.jpg", + "urlDecodedKey": "Happy Face.jpg", + "size": 1024, + "versionId": "version", + "eTag": "d41d8cd98f00b204e9800998ecf8427e", + "sequencer": "Happy Sequencer" + } + } + } + ] +} + diff --git a/lambda-events/src/fixtures/example-s3-event.json b/lambda-events/src/fixtures/example-s3-event.json new file mode 100644 index 000000000..d35f8a3da --- /dev/null +++ b/lambda-events/src/fixtures/example-s3-event.json @@ -0,0 +1,40 @@ +{ + "Records": [ + { + "eventVersion": "2.0", + "eventSource": "aws:s3", + "awsRegion": "us-east-1", + "eventTime": "1970-01-01T00:00:00.123Z", + "eventName": "ObjectCreated:Put", + "userIdentity": { + "principalId": "EXAMPLE" + }, + "requestParameters": { + "sourceIPAddress": "127.0.0.1" + }, + "responseElements": { + "x-amz-request-id": "C3D13FE58DE4C810", + "x-amz-id-2": "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD" + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "testConfigRule", + "bucket": { + "name": "sourcebucket", + "ownerIdentity": { + "principalId": "EXAMPLE" + }, + "arn": "arn:aws:s3:::mybucket" + }, + "object": { + "key": "Happy%20Face.jpg", + "size": 1024, + "versionId": "version", + "eTag": "d41d8cd98f00b204e9800998ecf8427e", + "sequencer": "Happy Sequencer" + } + } + } + ] +} + diff --git a/lambda-events/src/fixtures/example-s3-object-lambda-event-get-object-assumed-role.json b/lambda-events/src/fixtures/example-s3-object-lambda-event-get-object-assumed-role.json new file mode 100644 index 000000000..34aa55b1b --- /dev/null +++ b/lambda-events/src/fixtures/example-s3-object-lambda-event-get-object-assumed-role.json @@ -0,0 +1,42 @@ +{ + "xAmzRequestId": "requestId", + "getObjectContext": { + "inputS3Url": "https://my-s3-ap-111122223333.s3-accesspoint.us-east-1.amazonaws.com/example?X-Amz-Security-Token=", + "outputRoute": "io-use1-001", + "outputToken": "OutputToken" + }, + "configuration": { + "accessPointArn": "arn:aws:s3-object-lambda:us-east-1:111122223333:accesspoint/example-object-lambda-ap", + "supportingAccessPointArn": "arn:aws:s3:us-east-1:111122223333:accesspoint/example-ap", + "payload": "{}" + }, + "userRequest": { + "url": "https://object-lambda-111122223333.s3-object-lambda.us-east-1.amazonaws.com/example", + "headers": { + "Host": "object-lambda-111122223333.s3-object-lambda.us-east-1.amazonaws.com", + "Accept-Encoding": "identity", + "X-Amz-Content-SHA256": "e3b0c44298fc1example" + } + }, + "userIdentity": { + "type": "AssumedRole", + "principalId": "principalId", + "arn": "arn:aws:sts::111122223333:assumed-role/Admin/example", + "accountId": "111122223333", + "accessKeyId": "accessKeyId", + "sessionContext": { + "attributes": { + "mfaAuthenticated": "false", + "creationDate": "Wed Mar 10 23:41:52 UTC 2021" + }, + "sessionIssuer": { + "type": "Role", + "principalId": "principalId", + "arn": "arn:aws:iam::111122223333:role/Admin", + "accountId": "111122223333", + "userName": "Admin" + } + } + }, + "protocolVersion": "1.00" +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-s3-object-lambda-event-get-object-iam.json b/lambda-events/src/fixtures/example-s3-object-lambda-event-get-object-iam.json new file mode 100644 index 000000000..81b0ec71b --- /dev/null +++ b/lambda-events/src/fixtures/example-s3-object-lambda-event-get-object-iam.json @@ -0,0 +1,29 @@ +{ + "xAmzRequestId": "requestId", + "getObjectContext": { + "inputS3Url": "https://my-s3-ap-111122223333.s3-accesspoint.us-east-1.amazonaws.com/example?X-Amz-Security-Token=", + "outputRoute": "io-use1-001", + "outputToken": "OutputToken" + }, + "configuration": { + "accessPointArn": "arn:aws:s3-object-lambda:us-east-1:111122223333:accesspoint/example-object-lambda-ap", + "supportingAccessPointArn": "arn:aws:s3:us-east-1:111122223333:accesspoint/example-ap", + "payload": "{}" + }, + "userRequest": { + "url": "https://object-lambda-111122223333.s3-object-lambda.us-east-1.amazonaws.com/example", + "headers": { + "Host": "object-lambda-111122223333.s3-object-lambda.us-east-1.amazonaws.com", + "Accept-Encoding": "identity", + "X-Amz-Content-SHA256": "e3b0c44298fc1example" + } + }, + "userIdentity": { + "type": "IAMUser", + "principalId": "principalId", + "arn": "arn:aws:iam::111122223333:user/username", + "accountId": "111122223333", + "accessKeyId": "accessKeyId" + }, + "protocolVersion": "1.00" +} diff --git a/lambda-events/src/fixtures/example-s3-object-lambda-event-head-object-iam.json b/lambda-events/src/fixtures/example-s3-object-lambda-event-head-object-iam.json new file mode 100644 index 000000000..ec9208717 --- /dev/null +++ b/lambda-events/src/fixtures/example-s3-object-lambda-event-head-object-iam.json @@ -0,0 +1,28 @@ +{ + "xAmzRequestId": "requestId", + "headObjectContext": { + "inputS3Url": "https://my-s3-ap-111122223333.s3-accesspoint.us-east-1.amazonaws.com/example?X-Amz-Security-Token=" + }, + "configuration": { + "accessPointArn": "arn:aws:s3-object-lambda:us-east-1:111122223333:accesspoint/example-object-lambda-ap", + "supportingAccessPointArn": "arn:aws:s3:us-east-1:111122223333:accesspoint/example-ap", + "payload": "{}" + }, + "userRequest": { + "url": "https://object-lambda-111122223333.s3-object-lambda.us-east-1.amazonaws.com/example", + "headers": { + "Host": "object-lambda-111122223333.s3-object-lambda.us-east-1.amazonaws.com", + "Accept-Encoding": "identity", + "X-Amz-Content-SHA256": "e3b0c44298fc1example" + } + }, + "userIdentity": { + "type": "IAMUser", + "principalId": "principalId", + "arn": "arn:aws:iam::111122223333:user/username", + "accountId": "111122223333", + "accessKeyId": "accessKeyId" + }, + "protocolVersion": "1.01" +} + diff --git a/lambda-events/src/fixtures/example-s3-object-lambda-event-list-objects-iam.json b/lambda-events/src/fixtures/example-s3-object-lambda-event-list-objects-iam.json new file mode 100644 index 000000000..e1c6692c9 --- /dev/null +++ b/lambda-events/src/fixtures/example-s3-object-lambda-event-list-objects-iam.json @@ -0,0 +1,28 @@ +{ + "xAmzRequestId": "requestId", + "listObjectsContext": { + "inputS3Url": "https://my-s3-ap-111122223333.s3-accesspoint.us-east-1.amazonaws.com/example?X-Amz-Security-Token=" + }, + "configuration": { + "accessPointArn": "arn:aws:s3-object-lambda:us-east-1:111122223333:accesspoint/example-object-lambda-ap", + "supportingAccessPointArn": "arn:aws:s3:us-east-1:111122223333:accesspoint/example-ap", + "payload": "{}" + }, + "userRequest": { + "url": "https://object-lambda-111122223333.s3-object-lambda.us-east-1.amazonaws.com/example", + "headers": { + "Host": "object-lambda-111122223333.s3-object-lambda.us-east-1.amazonaws.com", + "Accept-Encoding": "identity", + "X-Amz-Content-SHA256": "e3b0c44298fc1example" + } + }, + "userIdentity": { + "type": "IAMUser", + "principalId": "principalId", + "arn": "arn:aws:iam::111122223333:user/username", + "accountId": "111122223333", + "accessKeyId": "accessKeyId" + }, + "protocolVersion": "1.01" +} + diff --git a/lambda-events/src/fixtures/example-s3-object-lambda-event-list-objects-v2-iam.json b/lambda-events/src/fixtures/example-s3-object-lambda-event-list-objects-v2-iam.json new file mode 100644 index 000000000..715dffa02 --- /dev/null +++ b/lambda-events/src/fixtures/example-s3-object-lambda-event-list-objects-v2-iam.json @@ -0,0 +1,28 @@ +{ + "xAmzRequestId": "requestId", + "listObjectsV2Context": { + "inputS3Url": "https://my-s3-ap-111122223333.s3-accesspoint.us-east-1.amazonaws.com/example?X-Amz-Security-Token=" + }, + "configuration": { + "accessPointArn": "arn:aws:s3-object-lambda:us-east-1:111122223333:accesspoint/example-object-lambda-ap", + "supportingAccessPointArn": "arn:aws:s3:us-east-1:111122223333:accesspoint/example-ap", + "payload": "{}" + }, + "userRequest": { + "url": "https://object-lambda-111122223333.s3-object-lambda.us-east-1.amazonaws.com/example", + "headers": { + "Host": "object-lambda-111122223333.s3-object-lambda.us-east-1.amazonaws.com", + "Accept-Encoding": "identity", + "X-Amz-Content-SHA256": "e3b0c44298fc1example" + } + }, + "userIdentity": { + "type": "IAMUser", + "principalId": "principalId", + "arn": "arn:aws:iam::111122223333:user/username", + "accountId": "111122223333", + "accessKeyId": "accessKeyId" + }, + "protocolVersion": "1.01" +} + diff --git a/lambda-events/src/fixtures/example-secretsmanager-secret-rotation-event.json b/lambda-events/src/fixtures/example-secretsmanager-secret-rotation-event.json new file mode 100644 index 000000000..f137ddbc1 --- /dev/null +++ b/lambda-events/src/fixtures/example-secretsmanager-secret-rotation-event.json @@ -0,0 +1,5 @@ +{ + "Step": "createSecret", + "SecretId": "arn:aws:secretsmanager:us-east-1:111122223333:secret:id-ABCD1E", + "ClientRequestToken": "1ab23456-cde7-8912-34fg-h56i78j9k12l" +} diff --git a/lambda-events/src/fixtures/example-ses-bcc-event.json b/lambda-events/src/fixtures/example-ses-bcc-event.json new file mode 100644 index 000000000..79cf188e6 --- /dev/null +++ b/lambda-events/src/fixtures/example-ses-bcc-event.json @@ -0,0 +1,98 @@ +{ + "Records": [ + { + "eventVersion": "1.0", + "ses": { + "mail": { + "commonHeaders": { + "from": [ + "Amazon Web Services " + ], + "returnPath": "aws@amazon.com", + "messageId": "", + "date": "Mon, 5 Dec 2016 18:40:08 -0800", + "subject": "Test Subject" + }, + "source": "aws@amazon.com", + "timestamp": "1970-01-01T00:00:00.123Z", + "destination": [ + "lambda@amazon.com" + ], + "headers": [ + { + "name": "Return-Path", + "value": "" + }, + { + "name": "Received", + "value": "from mx.amazon.com (mx.amazon.com [127.0.0.1]) by inbound-smtp.us-east-1.amazonaws.com with SMTP id 6n4thuhcbhpfiuf25gshf70rss364fuejrvmqko1 for lambda@amazon.com; Tue, 06 Dec 2016 02:40:10 +0000 (UTC)" + }, + { + "name": "DKIM-Signature", + "value": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=iatn.net; s=amazon; h=mime-version:from:date:message-id:subject:to; bh=chlJxa/vZ11+0O9lf4tKDM/CcPjup2nhhdITm+hSf3c=; b=SsoNPK0wX7umtWnw8pln3YSib+E09XO99d704QdSc1TR1HxM0OTti/UaFxVD4e5b0+okBqo3rgVeWgNZ0sWZEUhBaZwSL3kTd/nHkcPexeV0XZqEgms1vmbg75F6vlz9igWflO3GbXyTRBNMM0gUXKU/686hpVW6aryEIfM/rLY=" + }, + { + "name": "MIME-Version", + "value": "1.0" + }, + { + "name": "From", + "value": "Amazon Web Services " + }, + { + "name": "Date", + "value": "Mon, 5 Dec 2016 18:40:08 -0800" + }, + { + "name": "Message-ID", + "value": "" + }, + { + "name": "Subject", + "value": "Test Subject" + }, + { + "name": "Bcc", + "value": "lambda@amazon.com" + }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=94eb2c0742269658b10542f452a9" + } + ], + "headersTruncated": false, + "messageId": "6n4thuhcbhpfiuf25gshf70rss364fuejrvmqko1" + }, + "receipt": { + "recipients": [ + "lambda@amazon.com" + ], + "timestamp": "1970-01-01T00:00:00.123Z", + "spamVerdict": { + "status": "PASS" + }, + "dkimVerdict": { + "status": "PASS" + }, + "dmarcVerdict": { + "status": "PASS" + }, + "dmarcPolicy": "reject", + "processingTimeMillis": 574, + "action": { + "type": "Lambda", + "invocationType": "Event", + "functionArn": "arn:aws:lambda:us-east-1:000000000000:function:my-ses-lambda-function" + }, + "spfVerdict": { + "status": "PASS" + }, + "virusVerdict": { + "status": "PASS" + } + } + }, + "eventSource": "aws:ses" + } + ] +} diff --git a/lambda-events/src/fixtures/example-ses-event.json b/lambda-events/src/fixtures/example-ses-event.json new file mode 100644 index 000000000..77bee26a6 --- /dev/null +++ b/lambda-events/src/fixtures/example-ses-event.json @@ -0,0 +1,101 @@ +{ + "Records": [ + { + "eventVersion": "1.0", + "ses": { + "mail": { + "commonHeaders": { + "from": [ + "Amazon Web Services " + ], + "to": [ + "lambda@amazon.com" + ], + "returnPath": "aws@amazon.com", + "messageId": "", + "date": "Mon, 5 Dec 2016 18:40:08 -0800", + "subject": "Test Subject" + }, + "source": "aws@amazon.com", + "timestamp": "1970-01-01T00:00:00.123Z", + "destination": [ + "lambda@amazon.com" + ], + "headers": [ + { + "name": "Return-Path", + "value": "" + }, + { + "name": "Received", + "value": "from mx.amazon.com (mx.amazon.com [127.0.0.1]) by inbound-smtp.us-east-1.amazonaws.com with SMTP id 6n4thuhcbhpfiuf25gshf70rss364fuejrvmqko1 for lambda@amazon.com; Tue, 06 Dec 2016 02:40:10 +0000 (UTC)" + }, + { + "name": "DKIM-Signature", + "value": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=iatn.net; s=amazon; h=mime-version:from:date:message-id:subject:to; bh=chlJxa/vZ11+0O9lf4tKDM/CcPjup2nhhdITm+hSf3c=; b=SsoNPK0wX7umtWnw8pln3YSib+E09XO99d704QdSc1TR1HxM0OTti/UaFxVD4e5b0+okBqo3rgVeWgNZ0sWZEUhBaZwSL3kTd/nHkcPexeV0XZqEgms1vmbg75F6vlz9igWflO3GbXyTRBNMM0gUXKU/686hpVW6aryEIfM/rLY=" + }, + { + "name": "MIME-Version", + "value": "1.0" + }, + { + "name": "From", + "value": "Amazon Web Services " + }, + { + "name": "Date", + "value": "Mon, 5 Dec 2016 18:40:08 -0800" + }, + { + "name": "Message-ID", + "value": "" + }, + { + "name": "Subject", + "value": "Test Subject" + }, + { + "name": "To", + "value": "lambda@amazon.com" + }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=94eb2c0742269658b10542f452a9" + } + ], + "headersTruncated": false, + "messageId": "6n4thuhcbhpfiuf25gshf70rss364fuejrvmqko1" + }, + "receipt": { + "recipients": [ + "lambda@amazon.com" + ], + "timestamp": "1970-01-01T00:00:00.123Z", + "spamVerdict": { + "status": "PASS" + }, + "dkimVerdict": { + "status": "PASS" + }, + "dmarcVerdict": { + "status": "PASS" + }, + "dmarcPolicy": "reject", + "processingTimeMillis": 574, + "action": { + "type": "Lambda", + "invocationType": "Event", + "functionArn": "arn:aws:lambda:us-east-1:000000000000:function:my-ses-lambda-function" + }, + "spfVerdict": { + "status": "PASS" + }, + "virusVerdict": { + "status": "PASS" + } + } + }, + "eventSource": "aws:ses" + } + ] +} diff --git a/lambda-events/src/fixtures/example-ses-lambda-event.json b/lambda-events/src/fixtures/example-ses-lambda-event.json new file mode 100644 index 000000000..77bee26a6 --- /dev/null +++ b/lambda-events/src/fixtures/example-ses-lambda-event.json @@ -0,0 +1,101 @@ +{ + "Records": [ + { + "eventVersion": "1.0", + "ses": { + "mail": { + "commonHeaders": { + "from": [ + "Amazon Web Services " + ], + "to": [ + "lambda@amazon.com" + ], + "returnPath": "aws@amazon.com", + "messageId": "", + "date": "Mon, 5 Dec 2016 18:40:08 -0800", + "subject": "Test Subject" + }, + "source": "aws@amazon.com", + "timestamp": "1970-01-01T00:00:00.123Z", + "destination": [ + "lambda@amazon.com" + ], + "headers": [ + { + "name": "Return-Path", + "value": "" + }, + { + "name": "Received", + "value": "from mx.amazon.com (mx.amazon.com [127.0.0.1]) by inbound-smtp.us-east-1.amazonaws.com with SMTP id 6n4thuhcbhpfiuf25gshf70rss364fuejrvmqko1 for lambda@amazon.com; Tue, 06 Dec 2016 02:40:10 +0000 (UTC)" + }, + { + "name": "DKIM-Signature", + "value": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=iatn.net; s=amazon; h=mime-version:from:date:message-id:subject:to; bh=chlJxa/vZ11+0O9lf4tKDM/CcPjup2nhhdITm+hSf3c=; b=SsoNPK0wX7umtWnw8pln3YSib+E09XO99d704QdSc1TR1HxM0OTti/UaFxVD4e5b0+okBqo3rgVeWgNZ0sWZEUhBaZwSL3kTd/nHkcPexeV0XZqEgms1vmbg75F6vlz9igWflO3GbXyTRBNMM0gUXKU/686hpVW6aryEIfM/rLY=" + }, + { + "name": "MIME-Version", + "value": "1.0" + }, + { + "name": "From", + "value": "Amazon Web Services " + }, + { + "name": "Date", + "value": "Mon, 5 Dec 2016 18:40:08 -0800" + }, + { + "name": "Message-ID", + "value": "" + }, + { + "name": "Subject", + "value": "Test Subject" + }, + { + "name": "To", + "value": "lambda@amazon.com" + }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=94eb2c0742269658b10542f452a9" + } + ], + "headersTruncated": false, + "messageId": "6n4thuhcbhpfiuf25gshf70rss364fuejrvmqko1" + }, + "receipt": { + "recipients": [ + "lambda@amazon.com" + ], + "timestamp": "1970-01-01T00:00:00.123Z", + "spamVerdict": { + "status": "PASS" + }, + "dkimVerdict": { + "status": "PASS" + }, + "dmarcVerdict": { + "status": "PASS" + }, + "dmarcPolicy": "reject", + "processingTimeMillis": 574, + "action": { + "type": "Lambda", + "invocationType": "Event", + "functionArn": "arn:aws:lambda:us-east-1:000000000000:function:my-ses-lambda-function" + }, + "spfVerdict": { + "status": "PASS" + }, + "virusVerdict": { + "status": "PASS" + } + } + }, + "eventSource": "aws:ses" + } + ] +} diff --git a/lambda-events/src/fixtures/example-ses-s3-event.json b/lambda-events/src/fixtures/example-ses-s3-event.json new file mode 100644 index 000000000..1f2deb2e3 --- /dev/null +++ b/lambda-events/src/fixtures/example-ses-s3-event.json @@ -0,0 +1,115 @@ +{ + "Records": [ + { + "eventVersion": "1.0", + "ses": { + "receipt": { + "timestamp": "2015-09-11T20:32:33.936Z", + "processingTimeMillis": 406, + "recipients": [ + "recipient@example.com" + ], + "spamVerdict": { + "status": "PASS" + }, + "virusVerdict": { + "status": "PASS" + }, + "spfVerdict": { + "status": "PASS" + }, + "dkimVerdict": { + "status": "PASS" + }, + "dmarcVerdict": { + "status": "PASS" + }, + "dmarcPolicy": "reject", + "action": { + "type": "S3", + "topicArn": "arn:aws:sns:us-east-1:012345678912:example-topic", + "bucketName": "my-S3-bucket", + "objectKey": "email" + } + }, + "mail": { + "timestamp": "2015-09-11T20:32:33.936Z", + "source": "0000014fbe1c09cf-7cb9f704-7531-4e53-89a1-5fa9744f5eb6-000000@amazonses.com", + "messageId": "d6iitobk75ur44p8kdnnp7g2n800", + "destination": [ + "recipient@example.com" + ], + "headersTruncated": false, + "headers": [ + { + "name": "Return-Path", + "value": "<0000014fbe1c09cf-7cb9f704-7531-4e53-89a1-5fa9744f5eb6-000000@amazonses.com>" + }, + { + "name": "Received", + "value": "from a9-183.smtp-out.amazonses.com (a9-183.smtp-out.amazonses.com [54.240.9.183]) by inbound-smtp.us-east-1.amazonaws.com with SMTP id d6iitobk75ur44p8kdnnp7g2n800 for recipient@example.com; Fri, 11 Sep 2015 20:32:33 +0000 (UTC)" + }, + { + "name": "DKIM-Signature", + "value": "v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple; s=ug7nbtf4gccmlpwj322ax3p6ow6yfsug; d=amazonses.com; t=1442003552; h=From:To:Subject:MIME-Version:Content-Type:Content-Transfer-Encoding:Date:Message-ID:Feedback-ID; bh=DWr3IOmYWoXCA9ARqGC/UaODfghffiwFNRIb2Mckyt4=; b=p4ukUDSFqhqiub+zPR0DW1kp7oJZakrzupr6LBe6sUuvqpBkig56UzUwc29rFbJF hlX3Ov7DeYVNoN38stqwsF8ivcajXpQsXRC1cW9z8x875J041rClAjV7EGbLmudVpPX 4hHst1XPyX5wmgdHIhmUuh8oZKpVqGi6bHGzzf7g=" + }, + { + "name": "From", + "value": "sender@example.com" + }, + { + "name": "To", + "value": "recipient@example.com" + }, + { + "name": "Subject", + "value": "Example subject" + }, + { + "name": "MIME-Version", + "value": "1.0" + }, + { + "name": "Content-Type", + "value": "text/plain; charset=UTF-8" + }, + { + "name": "Content-Transfer-Encoding", + "value": "7bit" + }, + { + "name": "Date", + "value": "Fri, 11 Sep 2015 20:32:32 +0000" + }, + { + "name": "Message-ID", + "value": "<61967230-7A45-4A9D-BEC9-87CBCF2211C9@example.com>" + }, + { + "name": "X-SES-Outgoing", + "value": "2015.09.11-54.240.9.183" + }, + { + "name": "Feedback-ID", + "value": "1.us-east-1.Krv2FKpFdWV+KUYw3Qd6wcpPJ4Sv/pOPpEPSHn2u2o4=:AmazonSES" + } + ], + "commonHeaders": { + "returnPath": "0000014fbe1c09cf-7cb9f704-7531-4e53-89a1-5fa9744f5eb6-000000@amazonses.com", + "from": [ + "sender@example.com" + ], + "date": "Fri, 11 Sep 2015 20:32:32 +0000", + "to": [ + "recipient@example.com" + ], + "messageId": "<61967230-7A45-4A9D-BEC9-87CBCF2211C9@example.com>", + "subject": "Example subject" + } + } + }, + "eventSource": "aws:ses" + } + ] +} + diff --git a/lambda-events/src/fixtures/example-ses-sns-event.json b/lambda-events/src/fixtures/example-ses-sns-event.json new file mode 100644 index 000000000..bfededc18 --- /dev/null +++ b/lambda-events/src/fixtures/example-ses-sns-event.json @@ -0,0 +1,113 @@ +{ + "Records": [ + { + "eventVersion": "1.0", + "ses": { + "receipt": { + "timestamp": "2015-09-11T20:32:33.936Z", + "processingTimeMillis": 222, + "recipients": [ + "recipient@example.com" + ], + "spamVerdict": { + "status": "PASS" + }, + "virusVerdict": { + "status": "PASS" + }, + "spfVerdict": { + "status": "PASS" + }, + "dkimVerdict": { + "status": "PASS" + }, + "dmarcVerdict": { + "status": "PASS" + }, + "dmarcPolicy": "reject", + "action": { + "type": "SNS", + "topicArn": "arn:aws:sns:us-east-1:012345678912:example-topic" + } + }, + "mail": { + "timestamp": "2015-09-11T20:32:33.936Z", + "source": "61967230-7A45-4A9D-BEC9-87CBCF2211C9@example.com", + "messageId": "d6iitobk75ur44p8kdnnp7g2n800", + "destination": [ + "recipient@example.com" + ], + "headersTruncated": false, + "headers": [ + { + "name": "Return-Path", + "value": "<0000014fbe1c09cf-7cb9f704-7531-4e53-89a1-5fa9744f5eb6-000000@amazonses.com>" + }, + { + "name": "Received", + "value": "from a9-183.smtp-out.amazonses.com (a9-183.smtp-out.amazonses.com [54.240.9.183]) by inbound-smtp.us-east-1.amazonaws.com with SMTP id d6iitobk75ur44p8kdnnp7g2n800 for recipient@example.com; Fri, 11 Sep 2015 20:32:33 +0000 (UTC)" + }, + { + "name": "DKIM-Signature", + "value": "v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple; s=ug7nbtf4gccmlpwj322ax3p6ow6yfsug; d=amazonses.com; t=1442003552; h=From:To:Subject:MIME-Version:Content-Type:Content-Transfer-Encoding:Date:Message-ID:Feedback-ID; bh=DWr3IOmYWoXCA9ARqGC/UaODfghffiwFNRIb2Mckyt4=; b=p4ukUDSFqhqiub+zPR0DW1kp7oJZakrzupr6LBe6sUuvqpBkig56UzUwc29rFbJF hlX3Ov7DeYVNoN38stqwsF8ivcajXpQsXRC1cW9z8x875J041rClAjV7EGbLmudVpPX 4hHst1XPyX5wmgdHIhmUuh8oZKpVqGi6bHGzzf7g=" + }, + { + "name": "From", + "value": "sender@example.com" + }, + { + "name": "To", + "value": "recipient@example.com" + }, + { + "name": "Subject", + "value": "Example subject" + }, + { + "name": "MIME-Version", + "value": "1.0" + }, + { + "name": "Content-Type", + "value": "text/plain; charset=UTF-8" + }, + { + "name": "Content-Transfer-Encoding", + "value": "7bit" + }, + { + "name": "Date", + "value": "Fri, 11 Sep 2015 20:32:32 +0000" + }, + { + "name": "Message-ID", + "value": "<61967230-7A45-4A9D-BEC9-87CBCF2211C9@example.com>" + }, + { + "name": "X-SES-Outgoing", + "value": "2015.09.11-54.240.9.183" + }, + { + "name": "Feedback-ID", + "value": "1.us-east-1.Krv2FKpFdWV+KUYw3Qd6wcpPJ4Sv/pOPpEPSHn2u2o4=:AmazonSES" + } + ], + "commonHeaders": { + "returnPath": "0000014fbe1c09cf-7cb9f704-7531-4e53-89a1-5fa9744f5eb6-000000@amazonses.com", + "from": [ + "sender@example.com" + ], + "date": "Fri, 11 Sep 2015 20:32:32 +0000", + "to": [ + "recipient@example.com" + ], + "messageId": "<61967230-7A45-4A9D-BEC9-87CBCF2211C9@example.com>", + "subject": "Example subject" + } + } + }, + "eventSource": "aws:ses" + } + ] +} + diff --git a/lambda-events/src/fixtures/example-sns-event-obj.json b/lambda-events/src/fixtures/example-sns-event-obj.json new file mode 100644 index 000000000..c3144618d --- /dev/null +++ b/lambda-events/src/fixtures/example-sns-event-obj.json @@ -0,0 +1,22 @@ +{ + "Records": [ + { + "EventVersion": "1.0", + "EventSubscriptionArn": "arn:aws:sns:EXAMPLE", + "EventSource": "aws:sns", + "Sns": { + "Type" : "Notification", + "MessageId" : "82833b5c-8d5d-56d0-b0e1-7511f8253eb8", + "TopicArn" : "arn:aws:sns:us-east-1:246796806071:snsNetTest", + "Subject" : "Greetings", + "Message" : "{\"foo\":\"Hello world!\",\"bar\":123}", + "Timestamp" : "2015-08-18T18:02:32.111Z", + "SignatureVersion" : "1", + "Signature" : "e+khMfZriwAOTkF0OVm3tmdVq9eY6s5Bj6rXZty4B2TYssx7SSSBpvsDCiDuzgeHe++MNsGLDDT+5OpGEFBqCcd/K7iXhofz+KabMEtvM2Ku3aXcFixjOCAY1BF8hH6zU6nKzOy+m7K4UIoVqIOOhqsLWoXNFWgwQseBol1pFQ/MRi9UH84/WGdU8//dH+1/zjLxCud8Lg1vY9Yi/jxMU1HVpZ2JuvzJBdNBFJWc/VYAiw8K1r/J+dxAiLr87P96MgUqyg1wWxYe00HaEXGtjIctCNcd92s3pngOOeGvPYGaTIZEbYhSf2leMYd+CXujUHRqozru5K0Zp+l99fUNTg==", + "SigningCertURL" : "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-d6d679a1d18e95c2f9ffcf11f4f9e198.pem", + "UnsubscribeURL" : "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:246796806071:snsNetTest:228cc6c9-dcd8-4c92-9f3a-77f55176b9e3" + } + } + ] +} + diff --git a/lambda-events/src/fixtures/example-sns-event-pascal-case.json b/lambda-events/src/fixtures/example-sns-event-pascal-case.json new file mode 100644 index 000000000..ec1b125e1 --- /dev/null +++ b/lambda-events/src/fixtures/example-sns-event-pascal-case.json @@ -0,0 +1,22 @@ +{ + "Records": [ + { + "EventVersion": "1.0", + "EventSubscriptionArn": "arn:aws:sns:EXAMPLE", + "EventSource": "aws:sns", + "Sns": { + "Type" : "Notification", + "MessageId" : "82833b5c-8d5d-56d0-b0e1-7511f8253eb8", + "TopicArn" : "arn:aws:sns:us-east-1:246796806071:snsNetTest", + "Subject" : "Greetings", + "Message" : "Hello\r\nworld!", + "Timestamp" : "2015-08-18T18:02:32.111Z", + "SignatureVersion" : "1", + "Signature" : "e+khMfZriwAOTkF0OVm3tmdVq9eY6s5Bj6rXZty4B2TYssx7SSSBpvsDCiDuzgeHe++MNsGLDDT+5OpGEFBqCcd/K7iXhofz+KabMEtvM2Ku3aXcFixjOCAY1BF8hH6zU6nKzOy+m7K4UIoVqIOOhqsLWoXNFWgwQseBol1pFQ/MRi9UH84/WGdU8//dH+1/zjLxCud8Lg1vY9Yi/jxMU1HVpZ2JuvzJBdNBFJWc/VYAiw8K1r/J+dxAiLr87P96MgUqyg1wWxYe00HaEXGtjIctCNcd92s3pngOOeGvPYGaTIZEbYhSf2leMYd+CXujUHRqozru5K0Zp+l99fUNTg==", + "SigningCertUrl" : "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-d6d679a1d18e95c2f9ffcf11f4f9e198.pem", + "UnsubscribeUrl" : "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:246796806071:snsNetTest:228cc6c9-dcd8-4c92-9f3a-77f55176b9e3" + } + } + ] +} + diff --git a/lambda-events/src/fixtures/example-sns-event.json b/lambda-events/src/fixtures/example-sns-event.json new file mode 100644 index 000000000..091aa5796 --- /dev/null +++ b/lambda-events/src/fixtures/example-sns-event.json @@ -0,0 +1,22 @@ +{ + "Records": [ + { + "EventVersion": "1.0", + "EventSubscriptionArn": "arn:aws:sns:EXAMPLE", + "EventSource": "aws:sns", + "Sns": { + "Type" : "Notification", + "MessageId" : "82833b5c-8d5d-56d0-b0e1-7511f8253eb8", + "TopicArn" : "arn:aws:sns:us-east-1:246796806071:snsNetTest", + "Subject" : "Greetings", + "Message" : "Hello\r\nworld!", + "Timestamp" : "2015-08-18T18:02:32.111Z", + "SignatureVersion" : "1", + "Signature" : "e+khMfZriwAOTkF0OVm3tmdVq9eY6s5Bj6rXZty4B2TYssx7SSSBpvsDCiDuzgeHe++MNsGLDDT+5OpGEFBqCcd/K7iXhofz+KabMEtvM2Ku3aXcFixjOCAY1BF8hH6zU6nKzOy+m7K4UIoVqIOOhqsLWoXNFWgwQseBol1pFQ/MRi9UH84/WGdU8//dH+1/zjLxCud8Lg1vY9Yi/jxMU1HVpZ2JuvzJBdNBFJWc/VYAiw8K1r/J+dxAiLr87P96MgUqyg1wWxYe00HaEXGtjIctCNcd92s3pngOOeGvPYGaTIZEbYhSf2leMYd+CXujUHRqozru5K0Zp+l99fUNTg==", + "SigningCertURL" : "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-d6d679a1d18e95c2f9ffcf11f4f9e198.pem", + "UnsubscribeURL" : "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:246796806071:snsNetTest:228cc6c9-dcd8-4c92-9f3a-77f55176b9e3" + } + } + ] +} + diff --git a/lambda-events/src/fixtures/example-sns-subscription-confirmation.json b/lambda-events/src/fixtures/example-sns-subscription-confirmation.json new file mode 100644 index 000000000..563cf2b0d --- /dev/null +++ b/lambda-events/src/fixtures/example-sns-subscription-confirmation.json @@ -0,0 +1,14 @@ +{ + "Type": "SubscriptionConfirmation", + "MessageId": "165545c9-2a5c-472c-8df2-7ff2be2b3b1b", + "TopicArn": "arn:aws:sns:us-east-1:123456789012:MyTopic", + "Message": "You have chosen to subscribe to the topic arn:aws:sns:us-east-1:123456789012:MyTopic.\nTo confirm the subscription, visit the SubscribeURL included in this message.", + "Timestamp": "2012-04-26T20:45:04.751Z", + "SignatureVersion": "1", + "Signature": "EXAMPLEpH+DcEwjAPg8O9mY8dReBSwksfg2S7WKQcikcNKWLQjwu6A4VbeS0QHVCkhRS7fUQvi2egU3N858fiTDN6bkkOxYDVrY0Ad8L10Hs3zH81mtnPk5uvvolIC1CXGu43obcgFxeL3khZl8IKvO61GWB6jI9b5+gLPoBc1Q=", + "SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem", + "SubscribeURL": "https://sns.us-east-1.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-east-1:123456789012:MyTopic&Token=2336412f37fb687f5d51e6e2425dacbbffff", + "Token": "2336412f37fb687f5d51e6e2425dacbbffff", + "Subject": null, + "MessageAttributes": {} +} diff --git a/lambda-events/src/fixtures/example-sns-unsubscribe-confirmation.json b/lambda-events/src/fixtures/example-sns-unsubscribe-confirmation.json new file mode 100644 index 000000000..880355696 --- /dev/null +++ b/lambda-events/src/fixtures/example-sns-unsubscribe-confirmation.json @@ -0,0 +1,13 @@ +{ + "Type": "UnsubscribeConfirmation", + "MessageId": "47138184-6831-46b8-8466-7168d3b90898", + "TopicArn": "arn:aws:sns:us-east-1:123456789012:MyTopic", + "Message": "You have chosen to deactivate subscription arn:aws:sns:us-east-1:123456789012:MyTopic:00000000-0000-0000-0000-000000000000.\nTo cancel this operation and restore the subscription, visit the SubscribeURL included in this message.", + "Timestamp": "2012-04-26T20:45:04.751Z", + "SignatureVersion": "1", + "Signature": "EXAMPLEpH+DcEwjAPg8O9mY8dReBSwksfg2S7WKQcikcNKWLQjwu6A4VbeS0QHVCkhRS7fUQvi2egU3N858fiTDN6bkkOxYDVrY0Ad8L10Hs3zH81mtnPk5uvvolIC1CXGu43obcgFxeL3khZl8IKvO61GWB6jI9b5+gLPoBc1Q=", + "SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem", + "Token": "2336412f37fb687f5d51e6e2425dacbbeeee", + "Subject": null, + "MessageAttributes": {} +} diff --git a/lambda-events/src/fixtures/example-sqs-api-event-obj.json b/lambda-events/src/fixtures/example-sqs-api-event-obj.json new file mode 100644 index 000000000..39ab67cfc --- /dev/null +++ b/lambda-events/src/fixtures/example-sqs-api-event-obj.json @@ -0,0 +1,10 @@ +{ + "Messages": [ + { + "Body": "{\"country\": \"usa\", \"city\": \"provincetown\"}", + "Md5OfBody": "2b3e4f40b57e80d67ac5b9660c56d787", + "MessageId": "f663a189-97e2-41f5-9c0e-cfb595d8322c", + "ReceiptHandle": "AQEBdObBZIl7FWJiK9c3KmqKNvusy6+eqG51SLIp5Gs6lQ6+e4SI0lJ6Glw+qcOi+2RRrnfOjlsF8uDlo13TgubmtgP+CH7s+YKDdpbg2jA931vLi6qnU0ZFXcf/H8BDZ4kcz29npMu9/N2DT9F+kI9Q9pTfLsISg/7XFMvRTqAtjSfa2wI5TVcOPZBdkGqTLUoKqAYni0L7NTLzFUTjCN/HiOcvG+16zahhsTniM1MwOTSpbOO2uTZmY25V/PCfNdF1PBXtdNA9mWW2Ym6THV28ug3cuK6dXbFQBuxIGVhOq+mRVU6gKN/eZpZediiBt75oHD6ASu8jIUpJGeUWEZm6qSWU+YTivr6QoqGLwAVvI3CXOIZQ/+Wp/RJAxMQxtRIe/MOsOITcmGlFqhWnjlGQdg==" + } + ] +} diff --git a/lambda-events/src/fixtures/example-sqs-batch-response.json b/lambda-events/src/fixtures/example-sqs-batch-response.json new file mode 100644 index 000000000..50cb377bc --- /dev/null +++ b/lambda-events/src/fixtures/example-sqs-batch-response.json @@ -0,0 +1,10 @@ +{ + "batchItemFailures": [ + { + "itemIdentifier": "id2" + }, + { + "itemIdentifier": "id4" + } + ] +} diff --git a/lambda-events/src/fixtures/example-sqs-event-obj.json b/lambda-events/src/fixtures/example-sqs-event-obj.json new file mode 100644 index 000000000..764282750 --- /dev/null +++ b/lambda-events/src/fixtures/example-sqs-event-obj.json @@ -0,0 +1,41 @@ +{ + "Records": [ + { + "messageId" : "MessageID_1", + "receiptHandle" : "MessageReceiptHandle", + "body" : "{\"a\":\"Test\",\"b\":123}", + "md5OfBody" : "fce0ea8dd236ccb3ed9b37dae260836f", + "md5OfMessageAttributes" : "582c92c5c5b6ac403040a4f3ab3115c9", + "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:SQSQueue", + "eventSource": "aws:sqs", + "awsRegion": "us-west-2", + "attributes" : { + "ApproximateReceiveCount" : "2", + "SentTimestamp" : "1520621625029", + "SenderId" : "AROAIWPX5BD2BHG722MW4:sender", + "ApproximateFirstReceiveTimestamp" : "1520621634884" + }, + "messageAttributes" : { + "Attribute3" : { + "binaryValue" : "MTEwMA==", + "stringListValues" : ["abc", "123"], + "binaryListValues" : ["MA==", "MQ==", "MA=="], + "dataType" : "Binary" + }, + "Attribute2" : { + "stringValue" : "123", + "stringListValues" : [ ], + "binaryListValues" : ["MQ==", "MA=="], + "dataType" : "Number" + }, + "Attribute1" : { + "stringValue" : "AttributeValue1", + "stringListValues" : [ ], + "binaryListValues" : [ ], + "dataType" : "String" + } + } + } + ] +} + diff --git a/lambda-events/src/fixtures/example-sqs-event.json b/lambda-events/src/fixtures/example-sqs-event.json new file mode 100644 index 000000000..eea77f845 --- /dev/null +++ b/lambda-events/src/fixtures/example-sqs-event.json @@ -0,0 +1,40 @@ +{ + "Records": [ + { + "messageId" : "MessageID_1", + "receiptHandle" : "MessageReceiptHandle", + "body" : "Message Body", + "md5OfBody" : "fce0ea8dd236ccb3ed9b37dae260836f", + "md5OfMessageAttributes" : "582c92c5c5b6ac403040a4f3ab3115c9", + "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:SQSQueue", + "eventSource": "aws:sqs", + "awsRegion": "us-west-2", + "attributes" : { + "ApproximateReceiveCount" : "2", + "SentTimestamp" : "1520621625029", + "SenderId" : "AROAIWPX5BD2BHG722MW4:sender", + "ApproximateFirstReceiveTimestamp" : "1520621634884" + }, + "messageAttributes" : { + "Attribute3" : { + "binaryValue" : "MTEwMA==", + "stringListValues" : ["abc", "123"], + "binaryListValues" : ["MA==", "MQ==", "MA=="], + "dataType" : "Binary" + }, + "Attribute2" : { + "stringValue" : "123", + "stringListValues" : [ ], + "binaryListValues" : ["MQ==", "MA=="], + "dataType" : "Number" + }, + "Attribute1" : { + "stringValue" : "AttributeValue1", + "stringListValues" : [ ], + "binaryListValues" : [ ], + "dataType" : "String" + } + } + } + ] +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-vpc-lattice-response.json b/lambda-events/src/fixtures/example-vpc-lattice-response.json new file mode 100644 index 000000000..233996c06 --- /dev/null +++ b/lambda-events/src/fixtures/example-vpc-lattice-response.json @@ -0,0 +1,10 @@ +{ + "statusCode": 200, + "headers": { + "content-type": "application/json", + "x-request-id": "req-123456789", + "cache-control": "no-cache" + }, + "body": "{\"orderId\": \"order-789\", \"status\": \"created\", \"message\": \"Order successfully created\"}", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v1-request.json b/lambda-events/src/fixtures/example-vpc-lattice-v1-request.json new file mode 100644 index 000000000..bda2de8f9 --- /dev/null +++ b/lambda-events/src/fixtures/example-vpc-lattice-v1-request.json @@ -0,0 +1,19 @@ +{ + "raw_path": "/api/product", + "method": "POST", + "headers": { + "accept": "*/*", + "user-agent": "curl/7.68.0", + "x-forwarded-for": "10.0.2.100", + "authorization": "Bearer abc123def456", + "multi": "abcd, DEF" + }, + "query_string_parameters": { + "category": "electronics", + "sort": "price", + "limit": "10", + "tags": "tv" + }, + "body": "{\"id\": 5, \"description\": \"TV\"}", + "is_base64_encoded": false +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v2-request-base64.json b/lambda-events/src/fixtures/example-vpc-lattice-v2-request-base64.json new file mode 100644 index 000000000..8cf238b64 --- /dev/null +++ b/lambda-events/src/fixtures/example-vpc-lattice-v2-request-base64.json @@ -0,0 +1,29 @@ +{ + "version": "2.0", + "path": "/api/files/upload", + "method": "POST", + "headers": { + "content-type": ["multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW"], + "content-encoding": ["gzip"], + "content-length": ["1024"], + "x-forwarded-for": ["10.0.1.45"] + }, + "queryStringParameters": { + "uploadType": ["profile-image"] + }, + "body": "H4sIAAAAAAAAA+3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA", + "isBase64Encoded": true, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", + "type": "AWS_IAM", + "principal": "arn:aws:iam::123456789012:user/john.developer", + "principalOrgID": "o-50dc6c495c0c9188" + }, + "region": "ap-southeast-2", + "timeEpoch": "1724875299234567" + } +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v2-request-roles-anywhere.json b/lambda-events/src/fixtures/example-vpc-lattice-v2-request-roles-anywhere.json new file mode 100644 index 000000000..f4d1dbb40 --- /dev/null +++ b/lambda-events/src/fixtures/example-vpc-lattice-v2-request-roles-anywhere.json @@ -0,0 +1,37 @@ +{ + "version": "2.0", + "path": "/api/external/data-sync", + "method": "POST", + "headers": { + "content-type": ["application/json"], + "user-agent": ["ExternalDataSync/2.1.0"], + "x-forwarded-for": ["203.45.67.89"], + "x-client-cert": ["present"], + "authorization": ["X509-Cert"] + }, + "queryStringParameters": { + "sync-type": ["incremental"], + "validate": ["true"] + }, + "body": "{\"timestamp\": \"2025-08-28T14:30:00Z\", \"records\": [{\"id\": \"ext-001\", \"data\": \"sample-data\"}], \"source\": \"external-partner-system\"}", + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", + "type": "AWS_IAM", + "principal": "arn:aws:iam::123456789012:role/ExternalPartnerAccessRole", + "principalOrgID": "o-50dc6c495c0c9188", + "sessionName": "external-data-sync-session", + "x509IssuerOu": "Engineering Department", + "x509SanDns": "partner-system.external-company.com", + "x509SanNameCn": "Data Sync Service", + "x509SanUri": "https://partner-system.external-company.com/sync", + "x509SubjectCn": "external-partner-data-sync.external-company.com" + }, + "region": "ap-southeast-2", + "timeEpoch": "1724875199177430" + } +} diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v2-request.json b/lambda-events/src/fixtures/example-vpc-lattice-v2-request.json new file mode 100644 index 000000000..6901a37bd --- /dev/null +++ b/lambda-events/src/fixtures/example-vpc-lattice-v2-request.json @@ -0,0 +1,30 @@ +{ + "version": "2.0", + "path": "/health", + "method": "GET", + "headers": { + "accept": ["*/*"], + "user-agent": ["curl/7.68.0"], + "x-forwarded-for": ["10.0.2.100"], + "multi": ["x", "y"] + }, + "queryStringParameters": { + "state": ["prod"], + "multi": ["a", "DEF", "g"] + }, + "body": null, + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", + "type": "AWS_IAM", + "principal": "arn:aws:iam::123456789012:role/service-role/HealthChecker", + "principalOrgID": "o-50dc6c495c0c9188" + }, + "region": "ap-southeast-2", + "timeEpoch": "1724875399456789" + } +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/mod.rs b/lambda-events/src/fixtures/mod.rs new file mode 100644 index 000000000..c059e05e7 --- /dev/null +++ b/lambda-events/src/fixtures/mod.rs @@ -0,0 +1,19 @@ +use std::fmt::Debug; + +use serde::{de::DeserializeOwned, Serialize}; + +/// verify that the given UTF8 JSON data deserializes to an event, +/// and that the serialized representation is an equivalent event. +/// +/// ## Returns +/// The deserialized event, for further verification if needed. +pub(crate) fn verify_serde_roundtrip(json_text: &[u8]) -> T +where + T: DeserializeOwned + Serialize + PartialEq + Debug, +{ + let parsed: T = serde_json::from_slice(json_text).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: T = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + parsed +} diff --git a/lambda-events/src/lib.rs b/lambda-events/src/lib.rs new file mode 100644 index 000000000..541a4dc83 --- /dev/null +++ b/lambda-events/src/lib.rs @@ -0,0 +1,242 @@ +#![deny(rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#[cfg(feature = "http")] +#[cfg_attr(docsrs, doc(cfg(feature = "http")))] +pub use http; +#[cfg(feature = "query_map")] +#[cfg_attr(docsrs, doc(cfg(feature = "query_map")))] +pub use query_map; + +mod custom_serde; +/// Encodings used in AWS Lambda json event values. +pub mod encodings; +#[cfg(feature = "chrono")] +#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] +pub mod time_window; + +/// AWS Lambda event definitions. +pub mod event; + +/// Reusable unit test utilities. +#[cfg(test)] +pub(crate) mod fixtures; + +/// AWS Lambda event definitions for activemq. +#[cfg(feature = "activemq")] +#[cfg_attr(docsrs, doc(cfg(feature = "activemq")))] +pub use event::activemq; + +/// AWS Lambda event definitions for alb. +#[cfg(feature = "alb")] +#[cfg_attr(docsrs, doc(cfg(feature = "alb")))] +pub use event::alb; + +/// AWS Lambda event definitions for apigw. +#[cfg(feature = "apigw")] +#[cfg_attr(docsrs, doc(cfg(feature = "apigw")))] +pub use event::apigw; + +/// AWS Lambda event definitions for appsync. +#[cfg(feature = "appsync")] +#[cfg_attr(docsrs, doc(cfg(feature = "appsync")))] +pub use event::appsync; + +/// AWS Lambda event definitions for autoscaling. +#[cfg(feature = "autoscaling")] +#[cfg_attr(docsrs, doc(cfg(feature = "autoscaling")))] +pub use event::autoscaling; + +/// AWS Lambda event definitions for chime_bot. +#[cfg(feature = "chime_bot")] +#[cfg_attr(docsrs, doc(cfg(feature = "chime_bot")))] +pub use event::chime_bot; + +/// AWS Lambda event definitions for clientvpn. +#[cfg(feature = "clientvpn")] +#[cfg_attr(docsrs, doc(cfg(feature = "clientvpn")))] +pub use event::clientvpn; + +/// AWS Lambda event definitions for cloudformation +#[cfg(feature = "cloudformation")] +#[cfg_attr(docsrs, doc(cfg(feature = "cloudformation")))] +pub use event::cloudformation; + +/// AWS Lambda event definitions for CloudWatch alarms. +#[cfg(feature = "cloudwatch_alarms")] +#[cfg_attr(docsrs, doc(cfg(feature = "cloudwatch_alarms")))] +pub use event::cloudwatch_alarms; + +/// AWS Lambda event definitions for CloudWatch events. +#[cfg(feature = "cloudwatch_events")] +#[cfg_attr(docsrs, doc(cfg(feature = "cloudwatch_events")))] +pub use event::cloudwatch_events; + +/// AWS Lambda event definitions for cloudwatch_logs. +#[cfg(feature = "cloudwatch_logs")] +#[cfg_attr(docsrs, doc(cfg(feature = "cloudwatch_logs")))] +pub use event::cloudwatch_logs; + +/// AWS Lambda event definitions for code_commit. +#[cfg(feature = "code_commit")] +#[cfg_attr(docsrs, doc(cfg(feature = "code_commit")))] +pub use event::code_commit; + +/// AWS Lambda event definitions for codebuild. +#[cfg(feature = "codebuild")] +#[cfg_attr(docsrs, doc(cfg(feature = "codebuild")))] +pub use event::codebuild; + +/// AWS Lambda event definitions for codedeploy. +#[cfg(feature = "codedeploy")] +#[cfg_attr(docsrs, doc(cfg(feature = "codedeploy")))] +pub use event::codedeploy; + +/// AWS Lambda event definitions for codepipeline_cloudwatch. +#[cfg(feature = "codepipeline_cloudwatch")] +#[cfg_attr(docsrs, doc(cfg(feature = "codepipeline_cloudwatch")))] +pub use event::codepipeline_cloudwatch; + +/// AWS Lambda event definitions for codepipeline_job. +#[cfg(feature = "codepipeline_job")] +#[cfg_attr(docsrs, doc(cfg(feature = "codepipeline_job")))] +pub use event::codepipeline_job; + +/// AWS Lambda event definitions for cognito. +#[cfg(feature = "cognito")] +#[cfg_attr(docsrs, doc(cfg(feature = "cognito")))] +pub use event::cognito; + +/// AWS Lambda event definitions for config. +#[cfg(feature = "config")] +#[cfg_attr(docsrs, doc(cfg(feature = "config")))] +pub use event::config; + +/// AWS Lambda event definitions for connect. +#[cfg(feature = "connect")] +#[cfg_attr(docsrs, doc(cfg(feature = "connect")))] +pub use event::connect; + +/// AWS Lambda event definitions for AWS Control Tower lifecycle events. +#[cfg(feature = "controltower")] +#[cfg_attr(docsrs, doc(cfg(feature = "controltower")))] +pub use event::controltower; + +/// AWS Lambda event definitions for dynamodb. +#[cfg(feature = "dynamodb")] +#[cfg_attr(docsrs, doc(cfg(feature = "dynamodb")))] +pub use event::dynamodb; + +/// AWS Lambda event definitions for ecr_scan. +#[cfg(feature = "ecr_scan")] +#[cfg_attr(docsrs, doc(cfg(feature = "ecr_scan")))] +pub use event::ecr_scan; + +/// AWS Lambda event definitions for firehose. +#[cfg(feature = "firehose")] +#[cfg_attr(docsrs, doc(cfg(feature = "firehose")))] +pub use event::firehose; + +/// AWS Lambda event definitions for iam. +#[cfg(feature = "iam")] +#[cfg_attr(docsrs, doc(cfg(feature = "iam")))] +pub use event::iam; + +/// AWS Lambda event definitions for iot. +#[cfg(feature = "iot")] +#[cfg_attr(docsrs, doc(cfg(feature = "iot")))] +pub use event::iot; + +/// AWS Lambda event definitions for iot_1_click. +#[cfg(feature = "iot_1_click")] +#[cfg_attr(docsrs, doc(cfg(feature = "iot_1_click")))] +pub use event::iot_1_click; + +/// AWS Lambda event definitions for iot_button. +#[cfg(feature = "iot_button")] +#[cfg_attr(docsrs, doc(cfg(feature = "iot_button")))] +pub use event::iot_button; + +/// AWS Lambda event definitions for iot_deprecated. +#[cfg(feature = "iot_deprecated")] +#[cfg_attr(docsrs, doc(cfg(feature = "iot_deprecated")))] +pub use event::iot_deprecated; + +/// AWS Lambda event definitions for kafka. +#[cfg(feature = "kafka")] +#[cfg_attr(docsrs, doc(cfg(feature = "kafka")))] +pub use event::kafka; + +/// AWS Lambda event definitions for kinesis. +#[cfg(feature = "kinesis")] +#[cfg_attr(docsrs, doc(cfg(feature = "kinesis")))] +pub use event::kinesis; + +/// AWS Lambda event definitions for kinesis_analytics. +#[cfg(feature = "kinesis_analytics")] +#[cfg_attr(docsrs, doc(cfg(feature = "kinesis_analytics")))] +pub use event::kinesis::analytics as kinesis_analytics; + +/// AWS Lambda event definitions for lambda_function_urls. +#[cfg(feature = "lambda_function_urls")] +#[cfg_attr(docsrs, doc(cfg(feature = "lambda_function_urls")))] +pub use event::lambda_function_urls; + +/// AWS Lambda event definitions for lex. +#[cfg(feature = "lex")] +#[cfg_attr(docsrs, doc(cfg(feature = "lex")))] +pub use event::lex; + +/// AWS Lambda event definitions for rabbitmq. +#[cfg(feature = "rabbitmq")] +#[cfg_attr(docsrs, doc(cfg(feature = "rabbitmq")))] +pub use event::rabbitmq; + +/// AWS Lambda event definitions for s3. +#[cfg(feature = "s3")] +#[cfg_attr(docsrs, doc(cfg(feature = "s3")))] +pub use event::s3; + +/// AWS Lambda event definitions for s3_batch_job. +#[cfg(feature = "s3")] +#[cfg_attr(docsrs, doc(cfg(feature = "s3")))] +pub use event::s3::batch_job as s3_batch_job; + +/// AWS Lambda event definitions for secretsmanager. +#[cfg(feature = "secretsmanager")] +#[cfg_attr(docsrs, doc(cfg(feature = "secretsmanager")))] +pub use event::secretsmanager; + +/// AWS Lambda event definitions for ses. +#[cfg(feature = "ses")] +#[cfg_attr(docsrs, doc(cfg(feature = "ses")))] +pub use event::ses; + +/// AWS Lambda event definitions for SNS. +#[cfg(feature = "sns")] +#[cfg_attr(docsrs, doc(cfg(feature = "sns")))] +pub use event::sns; + +/// AWS Lambda event definitions for SQS. +#[cfg(feature = "sqs")] +#[cfg_attr(docsrs, doc(cfg(feature = "sqs")))] +pub use event::sqs; + +/// AWS Lambda event definitions for streams. +#[cfg(feature = "streams")] +#[cfg_attr(docsrs, doc(cfg(feature = "streams")))] +pub use event::streams; + +/// AWS Lambda event definitions for documentdb. +#[cfg(feature = "documentdb")] +#[cfg_attr(docsrs, doc(cfg(feature = "documentdb")))] +pub use event::documentdb; + +/// AWS Lambda event definitions for EventBridge. +#[cfg(feature = "eventbridge")] +#[cfg_attr(docsrs, doc(cfg(feature = "eventbridge")))] +pub use event::eventbridge; + +/// AWS Lambda event definitions for VPC lattice. +#[cfg(feature = "vpc_lattice")] +#[cfg_attr(docsrs, doc(cfg(feature = "vpc_lattice")))] +pub use event::vpc_lattice; diff --git a/lambda-events/src/time_window.rs b/lambda-events/src/time_window.rs new file mode 100644 index 000000000..35c038979 --- /dev/null +++ b/lambda-events/src/time_window.rs @@ -0,0 +1,83 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use crate::custom_serde::deserialize_nullish; + +/// `Window` is the object that captures the time window for the records in the event when using the tumbling windows feature +/// Kinesis: +/// DDB: +#[non_exhaustive] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Window { + pub start: DateTime, + pub end: DateTime, +} + +impl Default for Window { + fn default() -> Self { + Window { + start: Utc::now(), + end: Utc::now(), + } + } +} + +/// `TimeWindowProperties` is the object that captures properties that relate to the tumbling windows feature +/// Kinesis: +/// DDB: +#[non_exhaustive] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TimeWindowProperties { + /// Time window for the records in the event. + pub window: Window, + /// State being built up to this invoke in the time window. + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub state: HashMap, + /// Shard id of the records + #[serde(default)] + pub shard_id: Option, + /// The event source ARN of the service that generated the event (eg. DynamoDB or Kinesis) + #[serde(default)] + #[serde(rename = "eventSourceARN")] + pub event_source_arn: Option, + /// Set to true for the last invoke of the time window. + /// Subsequent invoke will start a new time window along with a fresh state. + pub is_final_invoke_for_window: bool, + /// Set to true if window is terminated prematurely. + /// Subsequent invoke will continue the same window with a fresh state. + pub is_window_terminated_early: bool, +} + +/// `TimeWindowEventResponseProperties` is the object that captures response properties that relate to the tumbling windows feature +/// Kinesis: +/// DDB: +#[non_exhaustive] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TimeWindowEventResponseProperties { + /// State being built up to this invoke in the time window. + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub state: HashMap, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_window_deserializer() { + let v = serde_json::json!({ + "start": "2020-12-09T07:04:00Z", + "end": "2020-12-09T07:06:00Z", + }); + + let parsed: Window = serde_json::from_value(v).unwrap(); + assert_eq!("2020-12-09T07:04:00+00:00", &parsed.start.to_rfc3339()); + assert_eq!("2020-12-09T07:06:00+00:00", &parsed.end.to_rfc3339()); + } +} diff --git a/lambda-extension/CHANGELOG.md b/lambda-extension/CHANGELOG.md new file mode 100644 index 000000000..66621a59e --- /dev/null +++ b/lambda-extension/CHANGELOG.md @@ -0,0 +1,32 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.1.1](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda-extension-v1.1.0...lambda-extension-v1.1.1) - 2026-09-02 + +### Other + +- updated the following local packages: lambda_runtime_api_client + +## [1.1.0](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda-extension-v1.0.4...lambda-extension-v1.1.0) - 2026-07-09 + +### Added + +- add SnapStart support via SnapStartResource trait ([#1150](https://github.com/aws/aws-lambda-rust-runtime/pull/1150)) + +## [1.0.4](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda-extension-v1.0.3...lambda-extension-v1.0.4) - 2026-03-19 + +### Other + +- updated the following local packages: lambda_runtime_api_client + +## [1.0.3](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda-extension-v1.0.2...lambda-extension-v1.0.3) - 2026-03-12 + +### Added + +- Make Telemetry API log record type generic ([#1098](https://github.com/aws/aws-lambda-rust-runtime/pull/1098)) diff --git a/lambda-extension/Cargo.toml b/lambda-extension/Cargo.toml index 823e3f82b..0aefbd84d 100644 --- a/lambda-extension/Cargo.toml +++ b/lambda-extension/Cargo.toml @@ -1,29 +1,42 @@ [package] name = "lambda-extension" -version = "0.7.0" +version = "1.1.1" edition = "2021" +rust-version = "1.84.0" authors = [ "David Calavera ", - "Harold Sun " + "Harold Sun ", ] description = "AWS Lambda Extension API" license = "Apache-2.0" -repository = "https://github.com/awslabs/aws-lambda-rust-runtime" +repository = "https://github.com/aws/aws-lambda-rust-runtime" categories = ["web-programming::http-server"] keywords = ["AWS", "Lambda", "API"] readme = "README.md" +[features] +default = ["tracing"] +tracing = ["lambda_runtime_api_client/tracing"] + [dependencies] async-stream = "0.3" -bytes = "1.0" -chrono = { version = "0.4", features = ["serde"] } -http = "0.2" -hyper = { version = "0.14.20", features = ["http1", "client", "server", "stream", "runtime"] } -lambda_runtime_api_client = { version = "0.7", path = "../lambda-runtime-api-client" } +chrono = { workspace = true, features = ["serde"] } +http = { workspace = true } +http-body-util = { workspace = true } +hyper = { workspace = true, features = ["http1", "client", "server"] } +hyper-util = { workspace = true } +lambda_runtime_api_client = { version = "1.1.1", path = "../lambda-runtime-api-client" } serde = { version = "1", features = ["derive"] } serde_json = "^1" -tracing = { version = "0.1", features = ["log"] } -tokio = { version = "1.0", features = ["macros", "io-util", "sync", "rt-multi-thread"] } +tokio = { version = "1.0", features = [ + "macros", + "io-util", + "sync", + "rt-multi-thread", +] } tokio-stream = "0.1.2" -tower = { version = "0.4", features = ["make", "util"] } +tower = { workspace = true, features = ["make", "util"] } +tracing = { version = "0.1", features = ["log"] } +[package.metadata.docs.rs] +all-features = true diff --git a/lambda-extension/README.md b/lambda-extension/README.md index 0f132d753..00cd77c4a 100644 --- a/lambda-extension/README.md +++ b/lambda-extension/README.md @@ -8,7 +8,7 @@ ### Simple extension -The code below creates a simple extension that's registered to every `INVOKE` and `SHUTDOWN` events, and logs them in CloudWatch. +The code below creates a simple extension that's registered to every `INVOKE` and `SHUTDOWN` events. ```rust,no_run use lambda_extension::{service_fn, Error, LambdaEvent, NextEvent}; @@ -56,7 +56,6 @@ async fn handler(logs: Vec) -> Result<(), Error> { LambdaLogRecord::Extension(_record) => { // do something with the extension log record }, - }, _ => (), } } @@ -75,6 +74,45 @@ async fn main() -> Result<(), Error> { ``` +### Telemetry processor extension + +```rust,no_run +use lambda_extension::{service_fn, Error, Extension, LambdaTelemetry, LambdaTelemetryRecord, SharedService}; +use tracing::info; + +async fn handler(events: Vec) -> Result<(), Error> { + for event in events { + match event.record { + LambdaTelemetryRecord::Function(record) => { + // do something with the function log record + }, + LambdaTelemetryRecord::PlatformInitStart { + initialization_type: _, + phase: _, + runtime_version: _, + runtime_version_arn: _, + } => { + // do something with the PlatformInitStart event + }, + // more types of telemetry events are available + _ => (), + } + } + + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + let telemetry_processor = SharedService::new(service_fn(handler)); + + Extension::new().with_telemetry_processor(telemetry_processor).run().await?; + + Ok(()) +} + +``` + ## Deployment Lambda extensions can be added to your functions either using [Lambda layers](https://docs.aws.amazon.com/lambda/latest/dg/using-extensions.html#using-extensions-config), or adding them to [containers images](https://docs.aws.amazon.com/lambda/latest/dg/using-extensions.html#invocation-extensions-images). @@ -87,13 +125,13 @@ Regardless of how you deploy them, the extensions MUST be compiled against the s - Build the extension with: -``` +```bash cargo lambda build --release --extension ``` If you want to run the extension in ARM processors, add the `--arm64` flag to the previous command: -``` +```bash cargo lambda build --release --extension --arm64 ``` @@ -103,13 +141,12 @@ This previous command will generate a binary file in `target/lambda/extensions` - Make sure you have the right credentials in your terminal by running the AWS CLI configure command: -``` +```bash aws configure ``` - Deploy the extension as a layer with: -``` +```bash cargo lambda deploy --extension ``` - diff --git a/lambda-extension/src/error.rs b/lambda-extension/src/error.rs index 2c3e23b31..4f6a99097 100644 --- a/lambda-extension/src/error.rs +++ b/lambda-extension/src/error.rs @@ -1,5 +1,5 @@ /// Error type that extensions may result in -pub type Error = lambda_runtime_api_client::Error; +pub type Error = lambda_runtime_api_client::BoxError; /// Simple error that encapsulates human readable descriptions #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/lambda-extension/src/events.rs b/lambda-extension/src/events.rs index 87fd62a4d..db872d96d 100644 --- a/lambda-extension/src/events.rs +++ b/lambda-extension/src/events.rs @@ -1,7 +1,7 @@ use serde::Deserialize; /// Request tracing information -#[derive(Debug, Deserialize)] +#[derive(Debug, Default, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Tracing { /// The type of tracing exposed to the extension @@ -20,6 +20,7 @@ pub struct InvokeEvent { /// The function's Amazon Resource Name pub invoked_function_arn: String, /// The request tracing information + #[serde(default)] pub tracing: Tracing, } @@ -55,17 +56,12 @@ impl NextEvent { /// Wrapper with information about the next /// event that the Lambda Runtime is going to process pub struct LambdaEvent { - /// ID assigned to this extension by the Lambda Runtime - pub extension_id: String, /// Next incoming event pub next: NextEvent, } impl LambdaEvent { - pub(crate) fn new(ex_id: &str, next: NextEvent) -> LambdaEvent { - LambdaEvent { - extension_id: ex_id.into(), - next, - } + pub(crate) fn new(next: NextEvent) -> LambdaEvent { + LambdaEvent { next } } } diff --git a/lambda-extension/src/extension.rs b/lambda-extension/src/extension.rs index ec83ce717..16cfd1f66 100644 --- a/lambda-extension/src/extension.rs +++ b/lambda-extension/src/extension.rs @@ -1,20 +1,36 @@ +use http::Request; +use http_body_util::BodyExt; +use hyper::{body::Incoming, server::conn::http1, service::service_fn}; + +use hyper_util::rt::tokio::TokioIo; +use lambda_runtime_api_client::PooledClient as Client; +use serde::{de::DeserializeOwned, Deserialize}; use std::{ - convert::Infallible, fmt, future::ready, future::Future, net::SocketAddr, path::PathBuf, pin::Pin, sync::Arc, + convert::Infallible, + fmt, + future::{ready, Future}, + marker::PhantomData, + net::SocketAddr, + path::PathBuf, + pin::Pin, + sync::Arc, }; - -use hyper::{server::conn::AddrStream, Server}; -use lambda_runtime_api_client::Client; -use tokio::sync::Mutex; +use tokio::{net::TcpListener, sync::Mutex}; use tokio_stream::StreamExt; -use tower::{service_fn, MakeService, Service, ServiceExt}; -use tracing::{error, trace}; +use tower::{MakeService, Service, ServiceExt}; +use tracing::trace; -use crate::{logs::*, requests, Error, ExtensionError, LambdaEvent, NextEvent}; +use crate::{ + logs::*, + requests::{self, Api}, + telemetry_wrapper, Error, ExtensionError, LambdaEvent, LambdaTelemetry, NextEvent, +}; const DEFAULT_LOG_PORT_NUMBER: u16 = 9002; +const DEFAULT_TELEMETRY_PORT_NUMBER: u16 = 9003; -/// An Extension that runs event and log processors -pub struct Extension<'a, E, L> { +/// An Extension that runs event, log and telemetry processors +pub struct Extension<'a, E, L, T, TL = String> { extension_name: Option<&'a str>, events: Option<&'a [&'a str]>, events_processor: E, @@ -22,9 +38,14 @@ pub struct Extension<'a, E, L> { logs_processor: Option, log_buffering: Option, log_port_number: u16, + telemetry_types: Option<&'a [&'a str]>, + telemetry_processor: Option, + telemetry_buffering: Option, + telemetry_port_number: u16, + _telemetry_record_type: PhantomData, } -impl<'a> Extension<'a, Identity, MakeIdentity>> { +impl Extension<'_, Identity, MakeIdentity>, MakeIdentity>> { /// Create a new base [`Extension`] with a no-op events processor pub fn new() -> Self { Extension { @@ -35,29 +56,45 @@ impl<'a> Extension<'a, Identity, MakeIdentity>> { log_buffering: None, logs_processor: None, log_port_number: DEFAULT_LOG_PORT_NUMBER, + telemetry_types: None, + telemetry_buffering: None, + telemetry_processor: None, + telemetry_port_number: DEFAULT_TELEMETRY_PORT_NUMBER, + _telemetry_record_type: PhantomData, } } } -impl<'a> Default for Extension<'a, Identity, MakeIdentity>> { +impl Default + for Extension<'_, Identity, MakeIdentity>, MakeIdentity>> +{ fn default() -> Self { Self::new() } } -impl<'a, E, L> Extension<'a, E, L> +impl<'a, E, L, T, TL> Extension<'a, E, L, T, TL> where E: Service, E::Future: Future>, - E::Error: Into> + fmt::Display + fmt::Debug, + E::Error: Into + fmt::Display + fmt::Debug, // Fixme: 'static bound might be too restrictive L: MakeService<(), Vec, Response = ()> + Send + Sync + 'static, L::Service: Service, Response = ()> + Send + Sync, >>::Future: Send + 'a, - L::Error: Into> + fmt::Debug, - L::MakeError: Into> + fmt::Debug, + L::Error: Into + fmt::Debug, + L::MakeError: Into + fmt::Debug, L::Future: Send, + + // Fixme: 'static bound might be too restrictive + T: MakeService<(), Vec>, Response = ()> + Send + Sync + 'static, + T::Service: Service>, Response = ()> + Send + Sync, + >>>::Future: Send + 'a, + T::Error: Into + fmt::Debug, + T::MakeError: Into + fmt::Debug, + T::Future: Send, + TL: DeserializeOwned + Send + 'static, { /// Create a new [`Extension`] with a given extension name pub fn with_extension_name(self, extension_name: &'a str) -> Self { @@ -77,11 +114,11 @@ where } /// Create a new [`Extension`] with a service that receives Lambda events. - pub fn with_events_processor(self, ep: N) -> Extension<'a, N, L> + pub fn with_events_processor(self, ep: N) -> Extension<'a, N, L, T, TL> where N: Service, N::Future: Future>, - N::Error: Into> + fmt::Display, + N::Error: Into + fmt::Display, { Extension { events_processor: ep, @@ -91,15 +128,20 @@ where log_buffering: self.log_buffering, logs_processor: self.logs_processor, log_port_number: self.log_port_number, + telemetry_types: self.telemetry_types, + telemetry_buffering: self.telemetry_buffering, + telemetry_processor: self.telemetry_processor, + telemetry_port_number: self.telemetry_port_number, + _telemetry_record_type: self._telemetry_record_type, } } /// Create a new [`Extension`] with a service that receives Lambda logs. - pub fn with_logs_processor(self, lp: N) -> Extension<'a, E, N> + pub fn with_logs_processor(self, lp: N) -> Extension<'a, E, N, T, TL> where N: Service<()>, N::Future: Future>, - N::Error: Into> + fmt::Display, + N::Error: Into + fmt::Display, { Extension { logs_processor: Some(lp), @@ -109,6 +151,11 @@ where log_types: self.log_types, log_buffering: self.log_buffering, log_port_number: self.log_port_number, + telemetry_types: self.telemetry_types, + telemetry_buffering: self.telemetry_buffering, + telemetry_processor: self.telemetry_processor, + telemetry_port_number: self.telemetry_port_number, + _telemetry_record_type: self._telemetry_record_type, } } @@ -137,48 +184,254 @@ where } } - /// Execute the given extension - pub async fn run(self) -> Result<(), Error> { - let client = &Client::builder().build()?; + /// Create a new [`Extension`] with a service that receives Lambda telemetry data. + /// + /// By default, telemetry log records are deserialized as `String`, but + /// it's possible to configure Lambda functions to emit logs in JSON format. + /// For more information, refer to [`Self::with_telemetry_record_type`]. + pub fn with_telemetry_processor(self, lp: N) -> Extension<'a, E, L, N, TL> + where + N: Service<()>, + N::Future: Future>, + N::Error: Into + fmt::Display, + { + Extension { + telemetry_processor: Some(lp), + events_processor: self.events_processor, + extension_name: self.extension_name, + events: self.events, + log_types: self.log_types, + log_buffering: self.log_buffering, + logs_processor: self.logs_processor, + log_port_number: self.log_port_number, + telemetry_types: self.telemetry_types, + telemetry_buffering: self.telemetry_buffering, + telemetry_port_number: self.telemetry_port_number, + _telemetry_record_type: self._telemetry_record_type, + } + } - let extension_id = register(client, self.extension_name, self.events).await?; - let extension_id = extension_id.to_str()?; - let mut ep = self.events_processor; + /// Create a new [`Extension`] with a list of telemetry types to subscribe. + /// The only accepted telemetry types are `function`, `platform`, and `extension`. + pub fn with_telemetry_types(self, telemetry_types: &'a [&'a str]) -> Self { + Extension { + telemetry_types: Some(telemetry_types), + ..self + } + } + + /// Create a new [`Extension`] with specific configuration to buffer telemetry. + pub fn with_telemetry_buffering(self, lb: LogBuffering) -> Self { + Extension { + telemetry_buffering: Some(lb), + ..self + } + } + + /// Create a new [`Extension`] with a different port number to listen to telemetry. + pub fn with_telemetry_port_number(self, port_number: u16) -> Self { + Extension { + telemetry_port_number: port_number, + ..self + } + } + + /// Register the extension. + /// + /// Performs the + /// [init phase](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html#runtimes-lifecycle-ib) + /// Lambda lifecycle operations to register the extension. When implementing an internal Lambda + /// extension, it is safe to call `lambda_runtime::run` once the future returned by this + /// function resolves. + pub async fn register(self) -> Result, Error> { + let client = &Client::builder().build(); + + let register_res = register(client, self.extension_name, self.events).await?; + // Logs API subscriptions must be requested during the Lambda init phase (see + // https://docs.aws.amazon.com/lambda/latest/dg/runtimes-logs-api.html#runtimes-logs-api-subscribing). if let Some(mut log_processor) = self.logs_processor { trace!("Log processor found"); - // Spawn task to run processor + + validate_buffering_configuration(self.log_buffering)?; + let addr = SocketAddr::from(([0, 0, 0, 0], self.log_port_number)); - let make_service = service_fn(move |_socket: &AddrStream| { - trace!("Creating new log processor Service"); - let service = log_processor.make_service(()); - async move { - let service = Arc::new(Mutex::new(service.await?)); - Ok::<_, L::MakeError>(service_fn(move |req| log_wrapper(service.clone(), req))) - } - }); - let server = Server::bind(&addr).serve(make_service); - tokio::spawn(async move { - if let Err(e) = server.await { - error!("Error while running log processor: {}", e); + let service = log_processor.make_service(()); + let service = Arc::new(Mutex::new(service.await.unwrap())); + tokio::task::spawn(async move { + trace!("Creating new logs processor Service"); + + loop { + let service: Arc> = service.clone(); + let make_service = service_fn(move |req: Request| log_wrapper(service.clone(), req)); + + let listener = TcpListener::bind(addr).await.unwrap(); + let (tcp, _) = listener.accept().await.unwrap(); + let io = TokioIo::new(tcp); + tokio::task::spawn(async move { + if let Err(err) = http1::Builder::new().serve_connection(io, make_service).await { + println!("Error serving connection: {err:?}"); + } + }); } }); + trace!("Log processor started"); // Call Logs API to start receiving events - let req = requests::subscribe_logs_request( - extension_id, + let req = requests::subscribe_request( + Api::LogsApi, + ®ister_res.extension_id, self.log_types, self.log_buffering, self.log_port_number, )?; let res = client.call(req).await?; - if res.status() != http::StatusCode::OK { - return Err(ExtensionError::boxed("unable to initialize the logs api")); + if !res.status().is_success() { + let err = format!("unable to initialize the logs api: {}", res.status()); + return Err(ExtensionError::boxed(err)); } trace!("Registered extension with Logs API"); } + // Telemetry API subscriptions must be requested during the Lambda init phase (see + // https://docs.aws.amazon.com/lambda/latest/dg/telemetry-api.html#telemetry-api-registration + if let Some(mut telemetry_processor) = self.telemetry_processor { + trace!("Telemetry processor found"); + + validate_buffering_configuration(self.telemetry_buffering)?; + + let addr = SocketAddr::from(([0, 0, 0, 0], self.telemetry_port_number)); + let service = telemetry_processor.make_service(()); + let service = Arc::new(Mutex::new(service.await.unwrap())); + tokio::task::spawn(async move { + trace!("Creating new telemetry processor Service"); + + loop { + let service = service.clone(); + let make_service = service_fn(move |req| telemetry_wrapper(service.clone(), req)); + + let listener = TcpListener::bind(addr).await.unwrap(); + let (tcp, _) = listener.accept().await.unwrap(); + let io = TokioIo::new(tcp); + tokio::task::spawn(async move { + if let Err(err) = http1::Builder::new().serve_connection(io, make_service).await { + println!("Error serving connection: {err:?}"); + } + }); + } + }); + + trace!("Telemetry processor started"); + + // Call Telemetry API to start receiving events + let req = requests::subscribe_request( + Api::TelemetryApi, + ®ister_res.extension_id, + self.telemetry_types, + self.telemetry_buffering, + self.telemetry_port_number, + )?; + let res = client.call(req).await?; + if !res.status().is_success() { + let err = format!("unable to initialize the telemetry api: {}", res.status()); + return Err(ExtensionError::boxed(err)); + } + trace!("Registered extension with Telemetry API"); + } + + Ok(RegisteredExtension { + extension_id: register_res.extension_id, + function_name: register_res.function_name, + function_version: register_res.function_version, + handler: register_res.handler, + account_id: register_res.account_id, + events_processor: self.events_processor, + }) + } + + /// Execute the given extension. + pub async fn run(self) -> Result<(), Error> { + self.register().await?.run().await + } +} + +impl<'a, E, L> Extension<'a, E, L, MakeIdentity>> { + /// Set the deserialization type for telemetry log records. + /// + /// By default, telemetry log records are deserialized as `String`, but + /// it's possible to configure Lambda functions to emit logs in JSON format. + /// Use this method to deserialize into a different type, such as + /// `serde_json::Value`. + /// + /// Must be called before [`Self::with_telemetry_processor`]. + /// + /// ``` + /// use lambda_extension::{Extension, LambdaTelemetry, SharedService, service_fn}; + /// + /// async fn handler(events: Vec>) -> Result<(), lambda_extension::Error> { + /// for event in &events { + /// println!("{event:?}"); + /// } + /// Ok(()) + /// } + /// + /// let _ext = Extension::new() + /// .with_telemetry_record_type::() + /// .with_telemetry_processor(SharedService::new(service_fn(handler))); + /// ``` + pub fn with_telemetry_record_type(self) -> Extension<'a, E, L, MakeIdentity>>, N> { + Extension { + _telemetry_record_type: PhantomData, + telemetry_processor: None, + events_processor: self.events_processor, + extension_name: self.extension_name, + events: self.events, + log_types: self.log_types, + log_buffering: self.log_buffering, + logs_processor: self.logs_processor, + log_port_number: self.log_port_number, + telemetry_types: self.telemetry_types, + telemetry_buffering: self.telemetry_buffering, + telemetry_port_number: self.telemetry_port_number, + } + } +} + +/// An extension registered by calling [`Extension::register`]. +pub struct RegisteredExtension { + /// The ID of the registered extension. This ID is unique per extension and remains constant + pub extension_id: String, + /// The ID of the account the extension was registered to. + /// This will be `None` if the register request doesn't send the Lambda-Extension-Accept-Feature header + pub account_id: Option, + /// The name of the Lambda function that the extension is registered with + pub function_name: String, + /// The version of the Lambda function that the extension is registered with + pub function_version: String, + /// The Lambda function handler that AWS Lambda invokes + pub handler: String, + events_processor: E, +} + +impl RegisteredExtension +where + E: Service, + E::Future: Future>, + E::Error: Into> + fmt::Display + fmt::Debug, +{ + /// Execute the extension's run loop. + /// + /// Performs the + /// [invoke](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html#runtimes-lifecycle-invoke) + /// and, for external Lambda extensions registered to receive the `SHUTDOWN` event, the + /// [shutdown](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html#runtimes-lifecycle-shutdown) + /// Lambda lifecycle phases. + pub async fn run(self) -> Result<(), Error> { + let client = &Client::builder().build(); + let mut ep = self.events_processor; + let extension_id = &self.extension_id; + let incoming = async_stream::stream! { loop { trace!("Waiting for next event (incoming loop)"); @@ -194,41 +447,43 @@ where let event = event?; let (_parts, body) = event.into_parts(); - let body = hyper::body::to_bytes(body).await?; + let body = body.collect().await?.to_bytes(); trace!("{}", std::str::from_utf8(&body)?); // this may be very verbose let event: NextEvent = serde_json::from_slice(&body)?; let is_invoke = event.is_invoke(); - let event = LambdaEvent::new(extension_id, event); + let event = LambdaEvent::new(event); let ep = match ep.ready().await { Ok(ep) => ep, - Err(error) => { - println!("Inner service is not ready: {:?}", error); + Err(err) => { + println!("Inner service is not ready: {err:?}"); let req = if is_invoke { - requests::init_error(extension_id, &error.to_string(), None)? + requests::init_error(extension_id, &err.to_string(), None)? } else { - requests::exit_error(extension_id, &error.to_string(), None)? + requests::exit_error(extension_id, &err.to_string(), None)? }; client.call(req).await?; - return Err(error.into()); + return Err(err.into()); } }; let res = ep.call(event).await; - if let Err(error) = res { - println!("{:?}", error); + if let Err(err) = res { + println!("{err:?}"); let req = if is_invoke { - requests::init_error(extension_id, &error.to_string(), None)? + requests::init_error(extension_id, &err.to_string(), None)? } else { - requests::exit_error(extension_id, &error.to_string(), None)? + requests::exit_error(extension_id, &err.to_string(), None)? }; client.call(req).await?; - return Err(error.into()); + return Err(err.into()); } } + + // Unreachable. Ok(()) } } @@ -284,12 +539,30 @@ where } } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct RegisterResponseBody { + function_name: String, + function_version: String, + handler: String, + account_id: Option, +} + +#[derive(Debug)] +struct RegisterResponse { + extension_id: String, + function_name: String, + function_version: String, + handler: String, + account_id: Option, +} + /// Initialize and register the extension in the Extensions API async fn register<'a>( client: &'a Client, extension_name: Option<&'a str>, events: Option<&'a [&'a str]>, -) -> Result { +) -> Result { let name = match extension_name { Some(name) => name.into(), None => { @@ -307,8 +580,9 @@ async fn register<'a>( let req = requests::register_request(&name, events)?; let res = client.call(req).await?; - if res.status() != http::StatusCode::OK { - return Err(ExtensionError::boxed("unable to register the extension")); + if !res.status().is_success() { + let err = format!("unable to register the extension: {}", res.status()); + return Err(ExtensionError::boxed(err)); } let header = res @@ -316,5 +590,17 @@ async fn register<'a>( .get(requests::EXTENSION_ID_HEADER) .ok_or_else(|| ExtensionError::boxed("missing extension id header")) .map_err(|e| ExtensionError::boxed(e.to_string()))?; - Ok(header.clone()) + let extension_id = header.to_str()?.to_string(); + + let (_, body) = res.into_parts(); + let body = body.collect().await?.to_bytes(); + let response: RegisterResponseBody = serde_json::from_slice(&body)?; + + Ok(RegisterResponse { + extension_id, + function_name: response.function_name, + function_version: response.function_version, + handler: response.handler, + account_id: response.account_id, + }) } diff --git a/lambda-extension/src/lib.rs b/lambda-extension/src/lib.rs index e3e72eba4..b6aec18fd 100644 --- a/lambda-extension/src/lib.rs +++ b/lambda-extension/src/lib.rs @@ -1,6 +1,7 @@ #![deny(clippy::all, clippy::cargo)] #![allow(clippy::multiple_crate_versions, clippy::type_complexity)] #![warn(missing_docs, nonstandard_style, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg))] //! This module includes utilities to create Lambda Runtime Extensions. //! @@ -17,10 +18,17 @@ mod events; pub use events::*; mod logs; pub use logs::*; +mod telemetry; +pub use telemetry::*; /// Include several request builders to interact with the Extension API. pub mod requests; +/// Utilities to initialize and use `tracing` and `tracing-subscriber` in Lambda Functions. +#[cfg(feature = "tracing")] +#[cfg_attr(docsrs, doc(cfg(feature = "tracing")))] +pub use lambda_runtime_api_client::tracing; + /// Execute the given events processor pub async fn run(events_processor: E) -> Result<(), Error> where diff --git a/lambda-extension/src/logs.rs b/lambda-extension/src/logs.rs index 5fd32e4f8..541dedc2d 100644 --- a/lambda-extension/src/logs.rs +++ b/lambda-extension/src/logs.rs @@ -1,12 +1,18 @@ use chrono::{DateTime, Utc}; +use http::{Request, Response}; +use http_body_util::BodyExt; +use hyper::body::Incoming; +use lambda_runtime_api_client::body::Body; use serde::{Deserialize, Serialize}; -use std::{boxed::Box, fmt, sync::Arc}; +use std::{fmt, sync::Arc}; use tokio::sync::Mutex; use tower::Service; use tracing::{error, trace}; +use crate::{Error, ExtensionError}; + /// Payload received from the Lambda Logs API -/// See: https://docs.aws.amazon.com/lambda/latest/dg/runtimes-logs-api.html#runtimes-logs-api-msg +/// See: #[derive(Clone, Debug, Deserialize, PartialEq)] pub struct LambdaLog { /// Time when the log was generated @@ -109,8 +115,8 @@ pub struct LogPlatformReportMetrics { } /// Log buffering configuration. -/// Allows Lambda to buffer logs before deliverying them to a subscriber. -#[derive(Debug, Serialize)] +/// Allows Lambda to buffer logs before delivering them to a subscriber. +#[derive(Debug, Serialize, Copy, Clone)] #[serde(rename_all = "camelCase")] pub struct LogBuffering { /// The maximum time (in milliseconds) to buffer a batch. @@ -124,6 +130,40 @@ pub struct LogBuffering { pub max_items: usize, } +static LOG_BUFFERING_MIN_TIMEOUT_MS: usize = 25; +static LOG_BUFFERING_MAX_TIMEOUT_MS: usize = 30_000; +static LOG_BUFFERING_MIN_BYTES: usize = 262_144; +static LOG_BUFFERING_MAX_BYTES: usize = 1_048_576; +static LOG_BUFFERING_MIN_ITEMS: usize = 1_000; +static LOG_BUFFERING_MAX_ITEMS: usize = 10_000; + +impl LogBuffering { + fn validate(&self) -> Result<(), Error> { + if self.timeout_ms < LOG_BUFFERING_MIN_TIMEOUT_MS || self.timeout_ms > LOG_BUFFERING_MAX_TIMEOUT_MS { + let error = format!( + "LogBuffering validation error: Invalid timeout_ms: {}. Allowed values: Minumun: {}. Maximum: {}", + self.timeout_ms, LOG_BUFFERING_MIN_TIMEOUT_MS, LOG_BUFFERING_MAX_TIMEOUT_MS + ); + return Err(ExtensionError::boxed(error)); + } + if self.max_bytes < LOG_BUFFERING_MIN_BYTES || self.max_bytes > LOG_BUFFERING_MAX_BYTES { + let error = format!( + "LogBuffering validation error: Invalid max_bytes: {}. Allowed values: Minumun: {}. Maximum: {}", + self.max_bytes, LOG_BUFFERING_MIN_BYTES, LOG_BUFFERING_MAX_BYTES + ); + return Err(ExtensionError::boxed(error)); + } + if self.max_items < LOG_BUFFERING_MIN_ITEMS || self.max_items > LOG_BUFFERING_MAX_ITEMS { + let error = format!( + "LogBuffering validation error: Invalid max_items: {}. Allowed values: Minumun: {}. Maximum: {}", + self.max_items, LOG_BUFFERING_MIN_ITEMS, LOG_BUFFERING_MAX_ITEMS + ); + return Err(ExtensionError::boxed(error)); + } + Ok(()) + } +} + impl Default for LogBuffering { fn default() -> Self { LogBuffering { @@ -134,38 +174,47 @@ impl Default for LogBuffering { } } +/// Validate the `LogBuffering` configuration (if present) +/// +/// # Errors +/// +/// This function will return an error if `LogBuffering` is present and configured incorrectly +pub(crate) fn validate_buffering_configuration(log_buffering: Option) -> Result<(), Error> { + match log_buffering { + Some(log_buffering) => log_buffering.validate(), + None => Ok(()), + } +} + /// Wrapper function that sends logs to the subscriber Service /// /// This takes an `hyper::Request` and transforms it into `Vec` for the /// underlying `Service` to process. -pub(crate) async fn log_wrapper( - service: Arc>, - req: hyper::Request, -) -> Result, Box> +pub(crate) async fn log_wrapper(service: Arc>, req: Request) -> Result, Error> where S: Service, Response = ()>, - S::Error: Into> + fmt::Debug, + S::Error: Into + fmt::Debug, S::Future: Send, { trace!("Received logs request"); // Parse the request body as a Vec - let body = match hyper::body::to_bytes(req.into_body()).await { + let body = match req.into_body().collect().await { Ok(body) => body, Err(e) => { error!("Error reading logs request body: {}", e); return Ok(hyper::Response::builder() .status(hyper::StatusCode::BAD_REQUEST) - .body(hyper::Body::empty()) + .body(Body::empty()) .unwrap()); } }; - let logs: Vec = match serde_json::from_slice(&body) { + let logs: Vec = match serde_json::from_slice(&body.to_bytes()) { Ok(logs) => logs, Err(e) => { error!("Error parsing logs: {}", e); return Ok(hyper::Response::builder() .status(hyper::StatusCode::BAD_REQUEST) - .body(hyper::Body::empty()) + .body(Body::empty()) .unwrap()); } }; @@ -174,23 +223,27 @@ where let mut service = service.lock().await; match service.call(logs).await { Ok(_) => (), - Err(err) => println!("{:?}", err), + Err(err) => println!("{err:?}"), } } - Ok(hyper::Response::new(hyper::Body::empty())) + Ok(hyper::Response::new(Body::empty())) } #[cfg(test)] mod tests { use super::*; - use chrono::TimeZone; + use chrono::{TimeDelta, TimeZone}; #[test] fn deserialize_full() { let data = r#"{"time": "2020-08-20T12:31:32.123Z","type": "function", "record": "hello world"}"#; let expected = LambdaLog { - time: Utc.ymd(2020, 8, 20).and_hms_milli(12, 31, 32, 123), + time: Utc + .with_ymd_and_hms(2020, 8, 20, 12, 31, 32) + .unwrap() + .checked_add_signed(TimeDelta::try_milliseconds(123).unwrap()) + .unwrap(), record: LambdaLogRecord::Function("hello world".to_string()), }; @@ -299,4 +352,67 @@ mod tests { }, ), } + + macro_rules! log_buffering_configuration_tests { + ($($name:ident: $value:expr,)*) => { + $( + #[test] + fn $name() { + let (input, expected) = $value; + let result = validate_buffering_configuration(input); + + if let Some(expected) = expected { + assert!(result.is_err()); + assert_eq!(result.unwrap_err().to_string(), expected.to_string()); + } else { + assert!(result.is_ok()); + } + + } + )* + } + } + + log_buffering_configuration_tests! { + log_buffer_configuration_none_success: ( + None, + None:: + ), + log_buffer_configuration_default_success: ( + Some(LogBuffering::default()), + None:: + ), + log_buffer_configuration_min_success: ( + Some(LogBuffering { timeout_ms: LOG_BUFFERING_MIN_TIMEOUT_MS, max_bytes: LOG_BUFFERING_MIN_BYTES, max_items: LOG_BUFFERING_MIN_ITEMS }), + None:: + ), + log_buffer_configuration_max_success: ( + Some(LogBuffering { timeout_ms: LOG_BUFFERING_MAX_TIMEOUT_MS, max_bytes: LOG_BUFFERING_MAX_BYTES, max_items: LOG_BUFFERING_MAX_ITEMS }), + None:: + ), + min_timeout_ms_error: ( + Some(LogBuffering { timeout_ms: LOG_BUFFERING_MIN_TIMEOUT_MS-1, max_bytes: LOG_BUFFERING_MAX_BYTES, max_items: LOG_BUFFERING_MAX_ITEMS }), + Some(ExtensionError::boxed("LogBuffering validation error: Invalid timeout_ms: 24. Allowed values: Minumun: 25. Maximum: 30000")) + ), + max_timeout_ms_error: ( + Some(LogBuffering { timeout_ms: LOG_BUFFERING_MAX_TIMEOUT_MS+1, max_bytes: LOG_BUFFERING_MAX_BYTES, max_items: LOG_BUFFERING_MAX_ITEMS }), + Some(ExtensionError::boxed("LogBuffering validation error: Invalid timeout_ms: 30001. Allowed values: Minumun: 25. Maximum: 30000")) + ), + min_max_bytes_error: ( + Some(LogBuffering { timeout_ms: LOG_BUFFERING_MAX_TIMEOUT_MS, max_bytes: LOG_BUFFERING_MIN_BYTES-1, max_items: LOG_BUFFERING_MAX_ITEMS }), + Some(ExtensionError::boxed("LogBuffering validation error: Invalid max_bytes: 262143. Allowed values: Minumun: 262144. Maximum: 1048576")) + ), + max_max_bytes_error: ( + Some(LogBuffering { timeout_ms: LOG_BUFFERING_MAX_TIMEOUT_MS, max_bytes: LOG_BUFFERING_MAX_BYTES+1, max_items: LOG_BUFFERING_MAX_ITEMS }), + Some(ExtensionError::boxed("LogBuffering validation error: Invalid max_bytes: 1048577. Allowed values: Minumun: 262144. Maximum: 1048576")) + ), + min_max_items_error: ( + Some(LogBuffering { timeout_ms: LOG_BUFFERING_MAX_TIMEOUT_MS, max_bytes: LOG_BUFFERING_MAX_BYTES, max_items: LOG_BUFFERING_MIN_ITEMS-1 }), + Some(ExtensionError::boxed("LogBuffering validation error: Invalid max_items: 999. Allowed values: Minumun: 1000. Maximum: 10000")) + ), + max_max_items_error: ( + Some(LogBuffering { timeout_ms: LOG_BUFFERING_MAX_TIMEOUT_MS, max_bytes: LOG_BUFFERING_MAX_BYTES, max_items: LOG_BUFFERING_MAX_ITEMS+1 }), + Some(ExtensionError::boxed("LogBuffering validation error: Invalid max_items: 10001. Allowed values: Minumun: 1000. Maximum: 10000")) + ), + } } diff --git a/lambda-extension/src/requests.rs b/lambda-extension/src/requests.rs index 6cff70b60..522b8402b 100644 --- a/lambda-extension/src/requests.rs +++ b/lambda-extension/src/requests.rs @@ -1,12 +1,18 @@ use crate::{Error, LogBuffering}; use http::{Method, Request}; -use hyper::Body; -use lambda_runtime_api_client::build_request; +use lambda_runtime_api_client::{body::Body, build_request}; use serde::Serialize; const EXTENSION_NAME_HEADER: &str = "Lambda-Extension-Name"; pub(crate) const EXTENSION_ID_HEADER: &str = "Lambda-Extension-Identifier"; const EXTENSION_ERROR_TYPE_HEADER: &str = "Lambda-Extension-Function-Error-Type"; +const CONTENT_TYPE_HEADER_NAME: &str = "Content-Type"; +const CONTENT_TYPE_HEADER_VALUE: &str = "application/json"; + +// Comma separated list of features the extension supports. +// `accountId` is currently the only supported feature. +const EXTENSION_ACCEPT_FEATURE: &str = "Lambda-Extension-Accept-Feature"; +const EXTENSION_ACCEPT_FEATURE_VALUE: &str = "accountId"; pub(crate) fn next_event_request(extension_id: &str) -> Result, Error> { let req = build_request() @@ -24,12 +30,36 @@ pub(crate) fn register_request(extension_name: &str, events: &[&str]) -> Result< .method(Method::POST) .uri("/2020-01-01/extension/register") .header(EXTENSION_NAME_HEADER, extension_name) + .header(EXTENSION_ACCEPT_FEATURE, EXTENSION_ACCEPT_FEATURE_VALUE) + .header(CONTENT_TYPE_HEADER_NAME, CONTENT_TYPE_HEADER_VALUE) .body(Body::from(serde_json::to_string(&events)?))?; Ok(req) } -pub(crate) fn subscribe_logs_request( +pub(crate) enum Api { + LogsApi, + TelemetryApi, +} + +impl Api { + pub(crate) fn schema_version(&self) -> &str { + match *self { + Api::LogsApi => "2021-03-18", + Api::TelemetryApi => "2022-07-01", + } + } + + pub(crate) fn uri(&self) -> &str { + match *self { + Api::LogsApi => "/2020-08-15/logs", + Api::TelemetryApi => "/2022-07-01/telemetry", + } + } +} + +pub(crate) fn subscribe_request( + api: Api, extension_id: &str, types: Option<&[&str]>, buffering: Option, @@ -38,19 +68,20 @@ pub(crate) fn subscribe_logs_request( let types = types.unwrap_or(&["platform", "function"]); let data = serde_json::json!({ - "schemaVersion": "2021-03-18", + "schemaVersion": api.schema_version(), "types": types, "buffering": buffering.unwrap_or_default(), "destination": { "protocol": "HTTP", - "URI": format!("http://sandbox.localdomain:{}", port_number), + "URI": format!("http://sandbox.localdomain:{port_number}"), } }); let req = build_request() .method(Method::PUT) - .uri("/2020-08-15/logs") + .uri(api.uri()) .header(EXTENSION_ID_HEADER, extension_id) + .header(CONTENT_TYPE_HEADER_NAME, CONTENT_TYPE_HEADER_VALUE) .body(Body::from(serde_json::to_string(&data)?))?; Ok(req) @@ -69,30 +100,30 @@ pub struct ErrorRequest<'a> { } /// Create a new init error request to send to the Extensions API -pub fn init_error<'a>( +pub fn init_error( extension_id: &str, error_type: &str, - request: Option>, + request: Option>, ) -> Result, Error> { error_request("init", extension_id, error_type, request) } /// Create a new exit error request to send to the Extensions API -pub fn exit_error<'a>( +pub fn exit_error( extension_id: &str, error_type: &str, - request: Option>, + request: Option>, ) -> Result, Error> { error_request("exit", extension_id, error_type, request) } -fn error_request<'a>( +fn error_request( error_type: &str, extension_id: &str, error_str: &str, - request: Option>, + request: Option>, ) -> Result, Error> { - let uri = format!("/2020-01-01/extension/{}/error", error_type); + let uri = format!("/2020-01-01/extension/{error_type}/error"); let body = match request { None => Body::empty(), diff --git a/lambda-extension/src/telemetry.rs b/lambda-extension/src/telemetry.rs new file mode 100644 index 000000000..6227001f9 --- /dev/null +++ b/lambda-extension/src/telemetry.rs @@ -0,0 +1,691 @@ +use chrono::{DateTime, Utc}; +use http::{Request, Response}; +use http_body_util::BodyExt; +use hyper::body::Incoming; +use lambda_runtime_api_client::body::Body; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::{boxed::Box, fmt, sync::Arc}; +use tokio::sync::Mutex; +use tower::Service; +use tracing::{error, trace}; + +/// Payload received from the Telemetry API +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +pub struct LambdaTelemetry { + /// Time when the telemetry was generated + pub time: DateTime, + /// Telemetry record entry + #[serde(flatten)] + pub record: LambdaTelemetryRecord, +} + +/// Record in a LambdaTelemetry entry +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(tag = "type", content = "record", rename_all = "lowercase")] +pub enum LambdaTelemetryRecord { + /// Function log records + Function(L), + + /// Extension log records + Extension(L), + + /// Platform init start record + #[serde(rename = "platform.initStart", rename_all = "camelCase")] + PlatformInitStart { + /// Type of initialization + initialization_type: InitType, + /// Phase of initialisation + phase: InitPhase, + /// Lambda runtime version + #[serde(skip_serializing_if = "Option::is_none")] + runtime_version: Option, + /// Lambda runtime version ARN + #[serde(skip_serializing_if = "Option::is_none")] + runtime_version_arn: Option, + }, + /// Platform init runtime done record + #[serde(rename = "platform.initRuntimeDone", rename_all = "camelCase")] + PlatformInitRuntimeDone { + /// Type of initialization + initialization_type: InitType, + /// Phase of initialisation + #[serde(skip_serializing_if = "Option::is_none")] + phase: Option, + /// Status of initalization + status: Status, + /// When the status = failure, the error_type describes what kind of error occurred + #[serde(skip_serializing_if = "Option::is_none")] + error_type: Option, + /// Spans + #[serde(default)] + spans: Vec, + }, + /// Platform init start record + #[serde(rename = "platform.initReport", rename_all = "camelCase")] + PlatformInitReport { + /// Type of initialization + initialization_type: InitType, + /// Phase of initialisation + phase: InitPhase, + /// Metrics + metrics: InitReportMetrics, + /// Spans + #[serde(default)] + spans: Vec, + }, + /// Record marking start of an invocation + #[serde(rename = "platform.start", rename_all = "camelCase")] + PlatformStart { + /// Request identifier + request_id: String, + /// Version of the Lambda function + #[serde(skip_serializing_if = "Option::is_none")] + version: Option, + /// Trace Context + #[serde(skip_serializing_if = "Option::is_none")] + tracing: Option, + }, + /// Record marking the completion of an invocation + #[serde(rename = "platform.runtimeDone", rename_all = "camelCase")] + PlatformRuntimeDone { + /// Request identifier + request_id: String, + /// Status of the invocation + status: Status, + /// When unsuccessful, the error_type describes what kind of error occurred + #[serde(skip_serializing_if = "Option::is_none")] + error_type: Option, + /// Metrics corresponding to the runtime + #[serde(skip_serializing_if = "Option::is_none")] + metrics: Option, + /// Spans + #[serde(default)] + spans: Vec, + /// Trace Context + #[serde(skip_serializing_if = "Option::is_none")] + tracing: Option, + }, + /// Platfor report record + #[serde(rename = "platform.report", rename_all = "camelCase")] + PlatformReport { + /// Request identifier + request_id: String, + /// Status of the invocation + status: Status, + /// When unsuccessful, the error_type describes what kind of error occurred + #[serde(skip_serializing_if = "Option::is_none")] + error_type: Option, + /// Metrics + metrics: ReportMetrics, + /// Spans + #[serde(default)] + spans: Vec, + /// Trace Context + #[serde(skip_serializing_if = "Option::is_none")] + tracing: Option, + }, + + /// Extension-specific record + #[serde(rename = "platform.extension", rename_all = "camelCase")] + PlatformExtension { + /// Name of the extension + name: String, + /// State of the extension + state: String, + /// Events sent to the extension + events: Vec, + }, + /// Telemetry processor-specific record + #[serde(rename = "platform.telemetrySubscription", rename_all = "camelCase")] + PlatformTelemetrySubscription { + /// Name of the extension + name: String, + /// State of the extensions + state: String, + /// Types of records sent to the extension + types: Vec, + }, + /// Record generated when the telemetry processor is falling behind + #[serde(rename = "platform.logsDropped", rename_all = "camelCase")] + PlatformLogsDropped { + /// Reason for dropping the logs + reason: String, + /// Number of records dropped + dropped_records: u64, + /// Total size of the dropped records + dropped_bytes: u64, + }, +} + +/// Type of Initialization +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub enum InitType { + /// Initialised on demand + OnDemand, + /// Initialized to meet the provisioned concurrency + ProvisionedConcurrency, + /// SnapStart + SnapStart, +} + +/// Phase in which initialization occurs +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub enum InitPhase { + /// Initialization phase + Init, + /// Invocation phase + Invoke, +} + +/// Status of invocation/initialization +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub enum Status { + /// Success + Success, + /// Error + Error, + /// Failure + Failure, + /// Timeout + Timeout, +} + +/// Span +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Span { + /// Duration of the span + pub duration_ms: f64, + /// Name of the span + pub name: String, + /// Start of the span + pub start: DateTime, +} + +/// Tracing Context +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct TraceContext { + /// Span ID + pub span_id: Option, + /// Type of tracing + pub r#type: TracingType, + /// A string containing tracing information like trace_id. The contents may depend on the TracingType. + pub value: String, +} + +/// Type of tracing +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +pub enum TracingType { + /// Amazon trace type + #[serde(rename = "X-Amzn-Trace-Id")] + AmznTraceId, +} + +///Init report metrics +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct InitReportMetrics { + /// Duration of initialization + pub duration_ms: f64, +} + +/// Report metrics +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ReportMetrics { + /// Duration in milliseconds + pub duration_ms: f64, + /// Billed duration in milliseconds + pub billed_duration_ms: u64, + /// Memory allocated in megabytes + #[serde(rename = "memorySizeMB")] + pub memory_size_mb: u64, + /// Maximum memory used for the invoke in megabytes + #[serde(rename = "maxMemoryUsedMB")] + pub max_memory_used_mb: u64, + /// Init duration in case of a cold start + #[serde(default = "Option::default", skip_serializing_if = "Option::is_none")] + pub init_duration_ms: Option, + /// Restore duration in milliseconds + #[serde(default = "Option::default", skip_serializing_if = "Option::is_none")] + pub restore_duration_ms: Option, +} + +/// Runtime done metrics +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct RuntimeDoneMetrics { + /// Duration in milliseconds + pub duration_ms: f64, + /// Number of bytes produced as a result of the invocation + pub produced_bytes: Option, +} + +/// Wrapper function that sends telemetry to the subscriber Service +/// +/// This takes an `hyper::Request` and transforms it into `Vec` for the +/// underlying `Service` to process. +pub(crate) async fn telemetry_wrapper( + service: Arc>, + req: Request, +) -> Result, Box> +where + S: Service>, Response = ()>, + S::Error: Into> + fmt::Debug, + S::Future: Send, + L: DeserializeOwned, +{ + trace!("Received telemetry request"); + // Parse the request body as a Vec + let body = match req.into_body().collect().await { + Ok(body) => body, + Err(e) => { + error!("Error reading telemetry request body: {}", e); + return Ok(hyper::Response::builder() + .status(hyper::StatusCode::BAD_REQUEST) + .body(Body::empty()) + .unwrap()); + } + }; + + let telemetry: Vec> = match serde_json::from_slice(&body.to_bytes()) { + Ok(telemetry) => telemetry, + Err(e) => { + error!("Error parsing telemetry: {}", e); + return Ok(hyper::Response::builder() + .status(hyper::StatusCode::BAD_REQUEST) + .body(Body::empty()) + .unwrap()); + } + }; + + { + let mut service = service.lock().await; + match service.call(telemetry).await { + Ok(_) => (), + Err(err) => println!("{err:?}"), + } + } + + Ok(hyper::Response::new(Body::empty())) +} + +#[cfg(test)] +mod deserialization_tests { + use super::*; + use chrono::{TimeDelta, TimeZone}; + + macro_rules! deserialize_tests { + ($($name:ident$(<$log:ty>)?: $value:expr,)*) => { + $( + #[test] + fn $name() { + let (input, expected) = $value; + let actual = serde_json::from_str::)?>(&input).expect("unable to deserialize"); + + assert!(actual.record == expected); + } + )* + }; + } + + deserialize_tests! { + // function + function: ( + r#"{"time": "2020-08-20T12:31:32.123Z","type": "function", "record": "hello world"}"#, + LambdaTelemetryRecord::Function("hello world".to_string()), + ), + + // function (json) + function_generic: ( + r#"{"time": "2020-08-20T12:31:32.123Z","type": "function", "record": true}"#, + LambdaTelemetryRecord::Function(true), + ), + + // extension + extension: ( + r#"{"time": "2020-08-20T12:31:32.123Z","type": "extension", "record": "hello world"}"#, + LambdaTelemetryRecord::Extension("hello world".to_string()), + ), + + // extension (json) + extension_generic: ( + r#"{"time": "2020-08-20T12:31:32.123Z","type": "extension", "record": true}"#, + LambdaTelemetryRecord::Extension(true), + ), + + // platform.start + platform_start: ( + r#"{"time":"2022-10-21T14:05:03.165Z","type":"platform.start","record":{"requestId":"459921b5-681c-4a96-beb0-81e0aa586026","version":"$LATEST","tracing":{"spanId":"24cd7d670fa455f0","type":"X-Amzn-Trace-Id","value":"Root=1-6352a70e-1e2c502e358361800241fd45;Parent=35465b3a9e2f7c6a;Sampled=1"}}}"#, + LambdaTelemetryRecord::PlatformStart { + request_id: "459921b5-681c-4a96-beb0-81e0aa586026".to_string(), + version: Some("$LATEST".to_string()), + tracing: Some(TraceContext{ + span_id: Some("24cd7d670fa455f0".to_string()), + r#type: TracingType::AmznTraceId, + value: "Root=1-6352a70e-1e2c502e358361800241fd45;Parent=35465b3a9e2f7c6a;Sampled=1".to_string(), + }), + }, + ), + // platform.initStart + platform_init_start: ( + r#"{"time":"2022-10-19T13:52:15.636Z","type":"platform.initStart","record":{"initializationType":"on-demand","phase":"init"}}"#, + LambdaTelemetryRecord::PlatformInitStart { + initialization_type: InitType::OnDemand, + phase: InitPhase::Init, + runtime_version: None, + runtime_version_arn: None, + }, + ), + // platform.runtimeDone + platform_runtime_done: ( + r#"{"time":"2022-10-21T14:05:05.764Z","type":"platform.runtimeDone","record":{"requestId":"459921b5-681c-4a96-beb0-81e0aa586026","status":"success","tracing":{"spanId":"24cd7d670fa455f0","type":"X-Amzn-Trace-Id","value":"Root=1-6352a70e-1e2c502e358361800241fd45;Parent=35465b3a9e2f7c6a;Sampled=1"},"spans":[{"name":"responseLatency","start":"2022-10-21T14:05:03.165Z","durationMs":2598.0},{"name":"responseDuration","start":"2022-10-21T14:05:05.763Z","durationMs":0.0}],"metrics":{"durationMs":2599.0,"producedBytes":8}}}"#, + LambdaTelemetryRecord::PlatformRuntimeDone { + request_id: "459921b5-681c-4a96-beb0-81e0aa586026".to_string(), + status: Status::Success, + error_type: None, + metrics: Some(RuntimeDoneMetrics { + duration_ms: 2599.0, + produced_bytes: Some(8), + }), + spans: vec!( + Span { + name:"responseLatency".to_string(), + start: Utc + .with_ymd_and_hms(2022, 10, 21, 14, 5, 3) + .unwrap() + .checked_add_signed(TimeDelta::try_milliseconds(165).unwrap()) + .unwrap(), + duration_ms: 2598.0 + }, + Span { + name:"responseDuration".to_string(), + start: Utc + .with_ymd_and_hms(2022, 10, 21, 14, 5, 5) + .unwrap() + .checked_add_signed(TimeDelta::try_milliseconds(763).unwrap()) + .unwrap(), + duration_ms: 0.0 + }, + ), + tracing: Some(TraceContext{ + span_id: Some("24cd7d670fa455f0".to_string()), + r#type: TracingType::AmznTraceId, + value: "Root=1-6352a70e-1e2c502e358361800241fd45;Parent=35465b3a9e2f7c6a;Sampled=1".to_string(), + }), + }, + ), + // platform.report + platform_report: ( + r#"{"time":"2022-10-21T14:05:05.766Z","type":"platform.report","record":{"requestId":"459921b5-681c-4a96-beb0-81e0aa586026","metrics":{"durationMs":2599.4,"billedDurationMs":2600,"memorySizeMB":128,"maxMemoryUsedMB":94,"initDurationMs":549.04},"tracing":{"spanId":"24cd7d670fa455f0","type":"X-Amzn-Trace-Id","value":"Root=1-6352a70e-1e2c502e358361800241fd45;Parent=35465b3a9e2f7c6a;Sampled=1"},"status":"success"}}"#, + LambdaTelemetryRecord::PlatformReport { + request_id: "459921b5-681c-4a96-beb0-81e0aa586026".to_string(), + status: Status::Success, + error_type: None, + metrics: ReportMetrics { + duration_ms: 2599.4, + billed_duration_ms: 2600, + memory_size_mb:128, + max_memory_used_mb:94, + init_duration_ms: Some(549.04), + restore_duration_ms: None, + }, + spans: Vec::new(), + tracing: Some(TraceContext { + span_id: Some("24cd7d670fa455f0".to_string()), + r#type: TracingType::AmznTraceId, + value: "Root=1-6352a70e-1e2c502e358361800241fd45;Parent=35465b3a9e2f7c6a;Sampled=1".to_string(), + }), + }, + ), + // platform.telemetrySubscription + platform_telemetry_subscription: ( + r#"{"time":"2022-10-19T13:52:15.667Z","type":"platform.telemetrySubscription","record":{"name":"my-extension","state":"Subscribed","types":["platform","function"]}}"#, + LambdaTelemetryRecord::PlatformTelemetrySubscription { + name: "my-extension".to_string(), + state: "Subscribed".to_string(), + types: vec!("platform".to_string(), "function".to_string()), + }, + ), + // platform.initRuntimeDone + platform_init_runtime_done: ( + r#"{"time":"2022-10-19T13:52:16.136Z","type":"platform.initRuntimeDone","record":{"initializationType":"on-demand","status":"success"}}"#, + LambdaTelemetryRecord::PlatformInitRuntimeDone { + initialization_type: InitType::OnDemand, + status: Status::Success, + phase: None, + error_type: None, + spans: Vec::new(), + }, + ), + // platform.extension + platform_extension: ( + r#"{"time":"2022-10-19T13:52:16.136Z","type":"platform.extension","record":{"name":"my-extension","state":"Ready","events":["SHUTDOWN","INVOKE"]}}"#, + LambdaTelemetryRecord::PlatformExtension { + name: "my-extension".to_string(), + state: "Ready".to_string(), + events: vec!("SHUTDOWN".to_string(), "INVOKE".to_string()), + }, + ), + // platform.initReport + platform_init_report: ( + r#"{"time":"2022-10-19T13:52:16.136Z","type":"platform.initReport","record":{"initializationType":"on-demand","metrics":{"durationMs":500.0},"phase":"init"}}"#, + LambdaTelemetryRecord::PlatformInitReport { + initialization_type: InitType::OnDemand, + phase: InitPhase::Init, + metrics: InitReportMetrics { duration_ms: 500.0 }, + spans: Vec::new(), + } + ), + } +} + +#[cfg(test)] +mod serialization_tests { + use chrono::{TimeDelta, TimeZone}; + + use super::*; + macro_rules! serialize_tests { + ($($name:ident$(<$log:ty>)?: $value:expr,)*) => { + $( + #[test] + fn $name() { + let (input, expected): (LambdaTelemetry$(<$log>)?, &str) = $value; + let actual = serde_json::to_string(&input).expect("unable to serialize"); + println!("Input: {:?}\n", input); + println!("Expected:\n {:?}\n", expected); + println!("Actual:\n {:?}\n", actual); + + assert!(actual == expected); + } + )* + }; + } + + serialize_tests! { + // function + function: ( + LambdaTelemetry { + time: Utc.with_ymd_and_hms(2023, 11, 28, 12, 0, 9).unwrap(), + record: LambdaTelemetryRecord::Function("hello world".to_string()), + }, + r#"{"time":"2023-11-28T12:00:09Z","type":"function","record":"hello world"}"#, + ), + // function (json) + function_generic: ( + LambdaTelemetry { + time: Utc.with_ymd_and_hms(2023, 11, 28, 12, 0, 9).unwrap(), + record: LambdaTelemetryRecord::Function(true), + }, + r#"{"time":"2023-11-28T12:00:09Z","type":"function","record":true}"#, + ), + // extension + extension: ( + LambdaTelemetry { + time: Utc.with_ymd_and_hms(2023, 11, 28, 12, 0, 9).unwrap(), + record: LambdaTelemetryRecord::Extension("hello world".to_string()), + }, + r#"{"time":"2023-11-28T12:00:09Z","type":"extension","record":"hello world"}"#, + ), + // extension (json) + extension_generic: ( + LambdaTelemetry { + time: Utc.with_ymd_and_hms(2023, 11, 28, 12, 0, 9).unwrap(), + record: LambdaTelemetryRecord::Extension(true), + }, + r#"{"time":"2023-11-28T12:00:09Z","type":"extension","record":true}"#, + ), + //platform.Start + platform_start: ( + LambdaTelemetry{ + time: Utc.with_ymd_and_hms(2023, 11, 28, 12, 0, 9).unwrap(), + record: LambdaTelemetryRecord::PlatformStart { + request_id: "459921b5-681c-4a96-beb0-81e0aa586026".to_string(), + version: Some("$LATEST".to_string()), + tracing: Some(TraceContext{ + span_id: Some("24cd7d670fa455f0".to_string()), + r#type: TracingType::AmznTraceId, + value: "Root=1-6352a70e-1e2c502e358361800241fd45;Parent=35465b3a9e2f7c6a;Sampled=1".to_string(), + }), + } + }, + r#"{"time":"2023-11-28T12:00:09Z","type":"platform.start","record":{"requestId":"459921b5-681c-4a96-beb0-81e0aa586026","version":"$LATEST","tracing":{"spanId":"24cd7d670fa455f0","type":"X-Amzn-Trace-Id","value":"Root=1-6352a70e-1e2c502e358361800241fd45;Parent=35465b3a9e2f7c6a;Sampled=1"}}}"#, + ), + // platform.initStart + platform_init_start: ( + LambdaTelemetry{ + time: Utc.with_ymd_and_hms(2023, 11, 28, 12, 0, 9).unwrap(), + record: LambdaTelemetryRecord::PlatformInitStart { + initialization_type: InitType::OnDemand, + phase: InitPhase::Init, + runtime_version: None, + runtime_version_arn: None, + }, + }, + r#"{"time":"2023-11-28T12:00:09Z","type":"platform.initStart","record":{"initializationType":"on-demand","phase":"init"}}"#, + ), + // platform.runtimeDone + platform_runtime_done: ( + LambdaTelemetry{ + time: Utc.with_ymd_and_hms(2023, 11, 28, 12, 0, 9).unwrap(), + record: LambdaTelemetryRecord::PlatformRuntimeDone { + request_id: "459921b5-681c-4a96-beb0-81e0aa586026".to_string(), + status: Status::Success, + error_type: None, + metrics: Some(RuntimeDoneMetrics { + duration_ms: 2599.0, + produced_bytes: Some(8), + }), + spans: vec!( + Span { + name:"responseLatency".to_string(), + start: Utc + .with_ymd_and_hms(2022, 10, 21, 14, 5, 3) + .unwrap() + .checked_add_signed(TimeDelta::try_milliseconds(165).unwrap()) + .unwrap(), + duration_ms: 2598.0 + }, + Span { + name:"responseDuration".to_string(), + start: Utc + .with_ymd_and_hms(2022, 10, 21, 14, 5, 5) + .unwrap() + .checked_add_signed(TimeDelta::try_milliseconds(763).unwrap()) + .unwrap(), + duration_ms: 0.0 + }, + ), + tracing: Some(TraceContext{ + span_id: Some("24cd7d670fa455f0".to_string()), + r#type: TracingType::AmznTraceId, + value: "Root=1-6352a70e-1e2c502e358361800241fd45;Parent=35465b3a9e2f7c6a;Sampled=1".to_string(), + }), + }, + }, + r#"{"time":"2023-11-28T12:00:09Z","type":"platform.runtimeDone","record":{"requestId":"459921b5-681c-4a96-beb0-81e0aa586026","status":"success","metrics":{"durationMs":2599.0,"producedBytes":8},"spans":[{"durationMs":2598.0,"name":"responseLatency","start":"2022-10-21T14:05:03.165Z"},{"durationMs":0.0,"name":"responseDuration","start":"2022-10-21T14:05:05.763Z"}],"tracing":{"spanId":"24cd7d670fa455f0","type":"X-Amzn-Trace-Id","value":"Root=1-6352a70e-1e2c502e358361800241fd45;Parent=35465b3a9e2f7c6a;Sampled=1"}}}"#, + ), + // platform.report + platform_report: ( + LambdaTelemetry{ + time: Utc.with_ymd_and_hms(2023, 11, 28, 12, 0, 9).unwrap(), + record: LambdaTelemetryRecord::PlatformReport { + request_id: "459921b5-681c-4a96-beb0-81e0aa586026".to_string(), + status: Status::Success, + error_type: None, + metrics: ReportMetrics { + duration_ms: 2599.4, + billed_duration_ms: 2600, + memory_size_mb:128, + max_memory_used_mb:94, + init_duration_ms: Some(549.04), + restore_duration_ms: None, + }, + spans: Vec::new(), + tracing: Some(TraceContext { + span_id: Some("24cd7d670fa455f0".to_string()), + r#type: TracingType::AmznTraceId, + value: "Root=1-6352a70e-1e2c502e358361800241fd45;Parent=35465b3a9e2f7c6a;Sampled=1".to_string(), + }), + }, + }, + r#"{"time":"2023-11-28T12:00:09Z","type":"platform.report","record":{"requestId":"459921b5-681c-4a96-beb0-81e0aa586026","status":"success","metrics":{"durationMs":2599.4,"billedDurationMs":2600,"memorySizeMB":128,"maxMemoryUsedMB":94,"initDurationMs":549.04},"spans":[],"tracing":{"spanId":"24cd7d670fa455f0","type":"X-Amzn-Trace-Id","value":"Root=1-6352a70e-1e2c502e358361800241fd45;Parent=35465b3a9e2f7c6a;Sampled=1"}}}"#, + ), + // platform.telemetrySubscription + platform_telemetry_subscription: ( + LambdaTelemetry{ + time: Utc.with_ymd_and_hms(2023, 11, 28, 12, 0, 9).unwrap(), + record: LambdaTelemetryRecord::PlatformTelemetrySubscription { + name: "my-extension".to_string(), + state: "Subscribed".to_string(), + types: vec!("platform".to_string(), "function".to_string()), + }, + }, + r#"{"time":"2023-11-28T12:00:09Z","type":"platform.telemetrySubscription","record":{"name":"my-extension","state":"Subscribed","types":["platform","function"]}}"#, + ), + // platform.initRuntimeDone + platform_init_runtime_done: ( + LambdaTelemetry{ + time: Utc.with_ymd_and_hms(2023, 11, 28, 12, 0, 9).unwrap(), + record: LambdaTelemetryRecord::PlatformInitRuntimeDone { + initialization_type: InitType::OnDemand, + status: Status::Success, + phase: None, + error_type: None, + spans: Vec::new(), + }, + }, + r#"{"time":"2023-11-28T12:00:09Z","type":"platform.initRuntimeDone","record":{"initializationType":"on-demand","status":"success","spans":[]}}"#, + ), + // platform.extension + platform_extension: ( + LambdaTelemetry { + time: Utc.with_ymd_and_hms(2023, 11, 28, 12, 0, 9).unwrap(), + record: LambdaTelemetryRecord::PlatformExtension { + name: "my-extension".to_string(), + state: "Ready".to_string(), + events: vec!("SHUTDOWN".to_string(), "INVOKE".to_string()), + }, + }, + r#"{"time":"2023-11-28T12:00:09Z","type":"platform.extension","record":{"name":"my-extension","state":"Ready","events":["SHUTDOWN","INVOKE"]}}"#, + ), + // platform.initReport + platform_init_report: ( + LambdaTelemetry { + time: Utc.with_ymd_and_hms(2023, 11, 28, 12, 0, 9).unwrap(), + record: LambdaTelemetryRecord::PlatformInitReport { + initialization_type: InitType::OnDemand, + phase: InitPhase::Init, + metrics: InitReportMetrics { duration_ms: 500.0 }, + spans: Vec::new(), + }, + }, + r#"{"time":"2023-11-28T12:00:09Z","type":"platform.initReport","record":{"initializationType":"on-demand","phase":"init","metrics":{"durationMs":500.0},"spans":[]}}"#, + ), + + } +} diff --git a/lambda-http/CHANGELOG.md b/lambda-http/CHANGELOG.md new file mode 100644 index 000000000..e7965a4a2 --- /dev/null +++ b/lambda-http/CHANGELOG.md @@ -0,0 +1,88 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.3.2](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_http-v1.3.1...lambda_http-v1.3.2) - 2026-09-29 + +### Other + +- updated the following local packages: lambda_runtime + +## [1.3.1](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_http-v1.3.0...lambda_http-v1.3.1) - 2026-09-02 + +### Other + +- updated the following local packages: lambda_runtime_api_client, lambda_runtime + +## [1.3.0](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_http-v1.2.1...lambda_http-v1.3.0) - 2026-07-09 + +### Added + +- add SnapStart support via SnapStartResource trait ([#1150](https://github.com/aws/aws-lambda-rust-runtime/pull/1150)) + +## [1.2.1](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_http-v1.2.0...lambda_http-v1.2.1) - 2026-05-25 + +### Other + +- updated the following local packages: lambda_runtime + +## [1.2.0](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_http-v1.1.3...lambda_http-v1.2.0) - 2026-05-08 + +### Added + +- Add VPC Lattice as an upstream source for lambda-http ([#1136](https://github.com/aws/aws-lambda-rust-runtime/pull/1136)) + +### Fixed + +- *(lambda-http)* into_api_gateway_v2_request joins cookies with "; " ([#1143](https://github.com/aws/aws-lambda-rust-runtime/pull/1143)) + +### Other + +- Added `builders` pass-through feature to enable `aws_lambda_events/builders` ([#1146](https://github.com/aws/aws-lambda-rust-runtime/pull/1146)) + +## [1.1.3](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_http-v1.1.2...lambda_http-v1.1.3) - 2026-04-16 + +### Other + +- updated the following local packages: lambda_runtime + +## [1.1.2](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_http-v1.1.1...lambda_http-v1.1.2) - 2026-03-19 + +### Other + +- updated the following local packages: lambda_runtime_api_client, lambda_runtime + +## [1.1.1](https://github.com/aws/aws-lambda-rust-runtime/compare/v1.0.2...lambda_http-v1.1.1) - 2026-03-11 + +### Added + +- *(lambda-managed-instances)* Lambda Managed Instances support via `concurrency-tokio` feature flag with `run_concurrent()` and `BoxCloneService` for concurrent streaming responses ([#1067](https://github.com/aws/aws-lambda-rust-runtime/pull/1067)) +- Add builder pattern support for event response types ([#1090](https://github.com/aws/aws-lambda-rust-runtime/pull/1090)) + +### Changed + +- MSRV updated from 1.82.0 to 1.84.0, enabled MSRV-aware resolver ([#1078](https://github.com/aws/aws-lambda-rust-runtime/pull/1078)) + +### Other + +- *(lambda-managed-instances)* add `tokio_unstable` to known cfgs to avoid linter warns ([#1095](https://github.com/aws/aws-lambda-rust-runtime/pull/1095)) +- *(lambda-managed-instances)* warn on `run()` with `AWS_LAMBDA_MAX_CONCURRENCY`, rename feature `experimental-concurrency` to `concurrency-tokio` ([#1095](https://github.com/aws/aws-lambda-rust-runtime/pull/1095)) +- *(lambda-managed-instances)* verify request-ID isolation in concurrent exec ([#1086](https://github.com/aws/aws-lambda-rust-runtime/pull/1086)) +- Introducing Harness Testing ([#1103](https://github.com/aws/aws-lambda-rust-runtime/pull/1103)) +- disable default features of lambda-runtime ([#1093](https://github.com/aws/aws-lambda-rust-runtime/pull/1093)) +- *(deps)* update axum-extra requirement from 0.10.2 to 0.12.5 ([#1079](https://github.com/aws/aws-lambda-rust-runtime/pull/1079)) + +## [1.1.0-rc1](https://github.com/aws/aws-lambda-rust-runtime/compare/v1.0.2...lambda_http-v1.1.0-rc1) - 2026-02-04 + +### Added + +- *(lambda-managed-instances)* Lambda Managed Instances support via `concurrency-tokio` feature flag with `run_concurrent()` and `BoxCloneService` for concurrent streaming responses ([#1067](https://github.com/aws/aws-lambda-rust-runtime/pull/1067)) + +### Changed + +- MSRV updated from 1.82.0 to 1.84.0, enabled MSRV-aware resolver ([#1078](https://github.com/aws/aws-lambda-rust-runtime/pull/1078)) diff --git a/lambda-http/Cargo.toml b/lambda-http/Cargo.toml index 387a5e2d7..245c126b0 100644 --- a/lambda-http/Cargo.toml +++ b/lambda-http/Cargo.toml @@ -1,49 +1,71 @@ [package] name = "lambda_http" -version = "0.7.1" +version = "1.3.2" authors = [ "David Calavera ", - "Harold Sun " + "Harold Sun ", ] edition = "2021" +rust-version = "1.84.0" description = "Application Load Balancer and API Gateway event types for AWS Lambda" keywords = ["AWS", "Lambda", "APIGateway", "ALB", "API"] license = "Apache-2.0" -homepage = "https://github.com/awslabs/aws-lambda-rust-runtime" -repository = "https://github.com/awslabs/aws-lambda-rust-runtime" +homepage = "https://github.com/aws/aws-lambda-rust-runtime" +repository = "https://github.com/aws/aws-lambda-rust-runtime" documentation = "https://docs.rs/lambda_runtime" categories = ["web-programming::http-server"] readme = "README.md" [features] -default = ["apigw_rest", "apigw_http", "apigw_websockets", "alb"] +default = ["apigw_rest", "apigw_http", "apigw_websockets", "alb", "vpc_lattice", "tracing"] apigw_rest = [] apigw_http = [] apigw_websockets = [] alb = [] +vpc_lattice = [] +pass_through = [] +builders = ["aws_lambda_events/builders"] +catch-all-fields = ["aws_lambda_events/catch-all-fields"] +tracing = ["lambda_runtime/tracing"] # enables access to the Tracing utilities +opentelemetry = ["lambda_runtime/opentelemetry"] # enables access to the OpenTelemetry layers and utilities +anyhow = ["lambda_runtime/anyhow"] # enables From for Diagnostic for anyhow error types, see README.md for more info +eyre = ["lambda_runtime/eyre"] # enables From for Diagnostic for eyre error types, see README.md for more info +miette = ["lambda_runtime/miette"] # enables From for Diagnostic for miette error types, see README.md for more info +concurrency-tokio = ["lambda_runtime/concurrency-tokio"] [dependencies] -base64 = "0.13.0" -bytes = "1" -http = "0.2" -http-body = "0.4" -hyper = "0.14.20" -lambda_runtime = { path = "../lambda-runtime", version = "0.7" } -serde = { version = "^1", features = ["derive"] } -serde_json = "^1" -serde_urlencoded = "0.7.0" -mime = "0.3.16" -encoding_rs = "0.8.31" -url = "2.2.2" -percent-encoding = "2.2.0" +bytes = { workspace = true } +encoding_rs = "0.8" +futures-util = { workspace = true } +http = { workspace = true } +http-body = { workspace = true } +http-body-util = { workspace = true } +hyper = { workspace = true } +lambda_runtime = { version = "1.5.0", path = "../lambda-runtime", default-features = false} +mime = "0.3" +percent-encoding = "2.2" +pin-project-lite = { workspace = true } +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0", features = ["raw_value"] } +serde_urlencoded = "0.7" +tokio-stream = "0.1.2" +url = "2.2" [dependencies.aws_lambda_events] -version = "^0.7.2" +path = "../lambda-events" +version = "1.2" default-features = false -features = ["alb", "apigw"] +features = ["alb", "apigw", "vpc_lattice"] [dev-dependencies] +axum-core = "0.5.4" + +axum-extra = { version = "0.12.5", features = ["query"] } +lambda_runtime_api_client = { version = "1.1.1", path = "../lambda-runtime-api-client" } + log = "^0.4" maplit = "1.0" tokio = { version = "1.0", features = ["macros"] } +[package.metadata.docs.rs] +all-features = true diff --git a/lambda-http/README.md b/lambda-http/README.md index 20e670ebc..b31a8c8de 100644 --- a/lambda-http/README.md +++ b/lambda-http/README.md @@ -1,53 +1,60 @@ # lambda-http for AWS Lambda in Rust -[![Docs](https://docs.rs/lambda_http/badge.svg)](https://docs.rs/lambda_http) +[![Docs](https://docs.rs/lambda_http/badge.svg)](https://docs.rs/lambda_http) -**`lambda-http`** is an abstraction that takes payloads from different services and turns them into http objects, making it easy to write API Gateway proxy event focused Lambda functions in Rust. +**`lambda-http`** is an abstraction that takes HTTP-shaped payloads from different AWS services and turns them +into standard `http` objects, making it easy to write Lambda functions in Rust that serve HTTP traffic +regardless of the upstream trigger. lambda-http handler is made of: -* Request - Represents an HTTP request -* IntoResponse - Future that will convert an [`IntoResponse`] into an actual [`LambdaResponse`] + +* `Request` - Represents an HTTP request +* `IntoResponse` - Future that will convert an [`IntoResponse`] into an actual [`LambdaResponse`] We are able to handle requests from: + * [API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html) REST, HTTP and WebSockets API lambda integrations * AWS [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html) * AWS [Lambda function URLs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html) +* AWS [VPC Lattice](https://docs.aws.amazon.com/vpc-lattice/latest/ug/lambda-functions.html) -Thanks to the Request type we can seemsly handle proxy integrations without the worry to specify the specific service type. +Thanks to the `Request` type we can seamlessly handle all of these triggers without having to write service-specific code. -There is also an Extentions for `lambda_http::Request` structs that provide access to [API gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format) and [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html) features. +There is also an extension for `lambda_http::Request` structs that provides access to [API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format), [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html), and [VPC Lattice](https://docs.aws.amazon.com/vpc-lattice/latest/ug/lambda-functions.html) features. For example some handy extensions: -* query_string_parameters - Return pre-parsed http query string parameters, parameters provided after the `?` portion of a url associated with the request -* path_parameters - Return pre-extracted path parameters, parameter provided in url placeholders `/foo/{bar}/baz/{boom}` associated with the request -* payload - Return the Result of a payload parsed into a serde Deserializeable type +* `query_string_parameters` - Return pre-parsed http query string parameters, parameters provided after the `?` portion of a url associated with the request +* `path_parameters` - Return pre-extracted path parameters, parameter provided in url placeholders `/foo/{bar}/baz/{qux}` associated with the request +* `lambda_context` - Return the Lambda context for the invocation; see the [runtime docs](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html#runtimes-api-next) +* `request_context` - Return the ALB, API Gateway, or VPC Lattice request context +* payload - Return the Result of a payload parsed into a type that implements `serde::Deserialize` + +See the `lambda_http::RequestPayloadExt` and `lambda_http::RequestExt` traits for more extensions. ## Examples -Here you will find a few examples to handle basic scenarions: +Here you will find a few examples to handle basic scenarios: -* Reading a JSON from a body and deserialise into a structure -* Reading querystring parameters +* Reading a JSON from a body and deserialize into a structure +* Reading query string parameters * Lambda Request Authorizer -* Passing the Lambda execution context initialisation to the handler +* Passing the Lambda execution context initialization to the handler -### Reading a JSON from a body and deserialise into a structure +### Reading a JSON from a body and deserialize into a structure -The code below creates a simple API Gateway proxy (HTTP, REST) that accept in input a JSON payload. +The code below creates a simple API Gateway proxy (HTTP, REST) that accepts in input a JSON payload. ```rust -use http::Response; -use lambda_http::{run, http::StatusCode, service_fn, Error, IntoResponse, Request, RequestExt}; +use lambda_http::{run, http::{StatusCode, Response}, service_fn, Error, IntoResponse, Request, RequestPayloadExt}; use serde::{Deserialize, Serialize}; use serde_json::json; #[tokio::main] async fn main() -> Result<(), Error> { tracing_subscriber::fmt() - .with_ansi(false) .without_time() - .with_max_level(tracing_subscriber::filter::LevelFilter::INFO) + .with_max_level(tracing::Level::INFO) .init(); run(service_fn(function_handler)).await @@ -61,7 +68,7 @@ pub async fn function_handler(event: Request) -> Result Result<(), Error> { tracing_subscriber::fmt() - .with_ansi(false) .without_time() - .with_max_level(tracing_subscriber::filter::LevelFilter::INFO) + .with_max_level(tracing::Level::INFO) .init(); run(service_fn(function_handler)).await } pub async fn function_handler(event: Request) -> Result { - let name = event.query_string_parameters() - .first("name") + let name = event.query_string_parameters_ref() + .and_then(|params| params.first("name")) .unwrap_or_else(|| "stranger") .to_string(); @@ -117,7 +122,6 @@ pub async fn function_handler(event: Request) -> Result Result<(), Error> { tracing_subscriber::fmt() - .with_ansi(false) .without_time() - .with_max_level(tracing_subscriber::filter::LevelFilter::INFO) + .with_max_level(tracing::Level::INFO) .init(); run(service_fn(function_handler)).await @@ -151,8 +154,8 @@ pub async fn function_handler(event: LambdaEvent Result<(), Error> { tracing_subscriber::fmt() - .with_ansi(false) .without_time() - .with_max_level(tracing_subscriber::filter::LevelFilter::INFO) + .with_max_level(tracing::Level::INFO) .init(); let config = aws_config::from_env() @@ -206,8 +207,8 @@ async fn main() -> Result<(), Error> { pub async fn function_handler(dynamodb_client: &aws_sdk_dynamodb::Client, event: Request) -> Result { let table = std::env::var("TABLE_NAME").expect("TABLE_NAME must be set"); - let name = event.query_string_parameters() - .first("name") + let name = event.query_string_parameters_ref() + .and_then(|params| params.first("name")) .unwrap_or_else(|| "stranger") .to_string(); @@ -230,4 +231,35 @@ pub async fn function_handler(dynamodb_client: &aws_sdk_dynamodb::Client, event: Ok(response) } -``` \ No newline at end of file +``` + +## Integration with API Gateway stages + +When you integrate HTTP Lambda functions with API Gateway stages, the path received in the request will include the stage as the first segment, for example `/production/api/v1`, where `production` is the API Gateway stage. + +If you don't want to receive the stage as part of the path, you can set the environment variable `AWS_LAMBDA_HTTP_IGNORE_STAGE_IN_PATH` to `true`, either in your Lambda function configuration, or inside the `main` Rust function. Following the previous example, when this environment variable is present, the path that the function receives is `/api/v1`, eliminating the stage from the first segment. + +## Feature flags + +`lambda_http` is a wrapper for HTTP events coming from four different AWS services: Application Load Balancer (ALB), API Gateway, Lambda Function URLs, and VPC Lattice. API Gateway can send events from three different endpoint types: REST APIs, HTTP APIs, and WebSockets. `lambda_http` transforms events from all these sources into native `http::Request` objects, so you can incorporate Rust HTTP semantics into your Lambda functions. + +By default, `lambda_http` compiles your function to support any of those services. This increases the compile time of your function because we have to generate code for all the sources. In reality, you'll usually put a Lambda function only behind one of those sources. You can choose which source to generate code for with feature flags. + +The available feature flags for `lambda_http` are the following: + +- `alb`: for events coming from [Amazon Elastic Load Balancer](https://aws.amazon.com/elasticloadbalancing/). +- `apigw_rest`: for events coming from [Amazon API Gateway REST APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html). +- `apigw_http`: for events coming from [Amazon API Gateway HTTP APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html) and [AWS Lambda Function URLs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html). +- `apigw_websockets`: for events coming from [Amazon API Gateway WebSockets](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api.html). +- `vpc_lattice`: for events coming from [AWS VPC Lattice](https://docs.aws.amazon.com/vpc-lattice/latest/ug/lambda-functions.html). +- `catch-all-fields`: enables catch-all fields in event types to preserve unknown JSON fields during deserialization. +- `builders`: enables builder pattern support for request/response types in `aws_lambda_events`, useful for constructing test fixtures. + +If you only want to support one of these sources, you can disable the default features, and enable only the source that you care about in your package's `Cargo.toml` file. Substitute the dependency line for `lambda_http` for the snippet below, changing the feature that you want to enable: + +```toml +[dependencies.lambda_http] +version = "0.5.3" +default-features = false +features = ["apigw_rest"] +``` diff --git a/lambda-http/src/deserializer.rs b/lambda-http/src/deserializer.rs new file mode 100644 index 000000000..776151995 --- /dev/null +++ b/lambda-http/src/deserializer.rs @@ -0,0 +1,190 @@ +use crate::request::LambdaRequest; +#[cfg(feature = "alb")] +use aws_lambda_events::alb::AlbTargetGroupRequest; +#[cfg(feature = "apigw_rest")] +use aws_lambda_events::apigw::ApiGatewayProxyRequest; +#[cfg(feature = "apigw_http")] +use aws_lambda_events::apigw::ApiGatewayV2httpRequest; +#[cfg(feature = "apigw_websockets")] +use aws_lambda_events::apigw::ApiGatewayWebsocketProxyRequest; +#[cfg(feature = "vpc_lattice")] +use aws_lambda_events::vpc_lattice::VpcLatticeRequestV2; + +use serde::{de::Error, Deserialize}; +use serde_json::value::RawValue; + +const ERROR_CONTEXT: &str = "this function expects a JSON payload from Amazon API Gateway, Amazon Elastic Load Balancer, or AWS Lambda Function URLs, but the data doesn't match any of those services' events"; + +#[cfg(feature = "pass_through")] +const PASS_THROUGH_ENABLED: bool = true; + +impl<'de> Deserialize<'de> for LambdaRequest { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let raw_value: Box = Box::::deserialize(deserializer)?; + let data = raw_value.get(); + + #[cfg(feature = "apigw_rest")] + if let Ok(res) = serde_json::from_str::(data) { + return Ok(LambdaRequest::ApiGatewayV1(res)); + } + #[cfg(feature = "apigw_http")] + if let Ok(res) = serde_json::from_str::(data) { + return Ok(LambdaRequest::ApiGatewayV2(res)); + } + #[cfg(feature = "alb")] + if let Ok(res) = serde_json::from_str::(data) { + return Ok(LambdaRequest::Alb(res)); + } + #[cfg(feature = "apigw_websockets")] + if let Ok(res) = serde_json::from_str::(data) { + return Ok(LambdaRequest::WebSocket(res)); + } + #[cfg(feature = "vpc_lattice")] + if let Ok(res) = serde_json::from_str::(data) { + return Ok(LambdaRequest::VpcLatticeV2(res)); + } + #[cfg(feature = "pass_through")] + if PASS_THROUGH_ENABLED { + return Ok(LambdaRequest::PassThrough(data.to_string())); + } + + Err(Error::custom(ERROR_CONTEXT)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_deserialize_apigw_rest() { + let data = include_bytes!("../../lambda-events/src/fixtures/example-apigw-request.json"); + + let req: LambdaRequest = serde_json::from_slice(data).expect("failed to deserialize apigw rest data"); + match req { + LambdaRequest::ApiGatewayV1(req) => { + assert_eq!("12345678912", req.request_context.account_id.unwrap()); + } + other => panic!("unexpected request variant: {other:?}"), + } + } + + #[test] + fn test_deserialize_apigw_http() { + let data = include_bytes!("../../lambda-events/src/fixtures/example-apigw-v2-request-iam.json"); + + let req: LambdaRequest = serde_json::from_slice(data).expect("failed to deserialize apigw http data"); + match req { + LambdaRequest::ApiGatewayV2(req) => { + assert_eq!("123456789012", req.request_context.account_id.unwrap()); + } + other => panic!("unexpected request variant: {other:?}"), + } + } + + #[test] + fn test_deserialize_sam_rest() { + let data = include_bytes!("../../lambda-events/src/fixtures/example-apigw-sam-rest-request.json"); + + let req: LambdaRequest = serde_json::from_slice(data).expect("failed to deserialize SAM rest data"); + match req { + LambdaRequest::ApiGatewayV1(req) => { + assert_eq!("123456789012", req.request_context.account_id.unwrap()); + } + other => panic!("unexpected request variant: {other:?}"), + } + } + + #[test] + fn test_deserialize_sam_http() { + let data = include_bytes!("../../lambda-events/src/fixtures/example-apigw-sam-http-request.json"); + + let req: LambdaRequest = serde_json::from_slice(data).expect("failed to deserialize SAM http data"); + match req { + LambdaRequest::ApiGatewayV2(req) => { + assert_eq!("123456789012", req.request_context.account_id.unwrap()); + } + other => panic!("unexpected request variant: {other:?}"), + } + } + + #[test] + fn test_deserialize_alb() { + let data = include_bytes!( + "../../lambda-events/src/fixtures/example-alb-lambda-target-request-multivalue-headers.json" + ); + + let req: LambdaRequest = serde_json::from_slice(data).expect("failed to deserialize alb rest data"); + match req { + LambdaRequest::Alb(req) => { + assert_eq!( + "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/lambda-target/abcdefgh", + req.request_context.elb.target_group_arn.unwrap() + ); + } + other => panic!("unexpected request variant: {other:?}"), + } + } + + #[test] + fn test_deserialize_apigw_websocket() { + let data = + include_bytes!("../../lambda-events/src/fixtures/example-apigw-websocket-request-without-method.json"); + + let req: LambdaRequest = serde_json::from_slice(data).expect("failed to deserialize apigw websocket data"); + match req { + LambdaRequest::WebSocket(req) => { + assert_eq!("CONNECT", req.request_context.event_type.unwrap()); + } + other => panic!("unexpected request variant: {other:?}"), + } + } + + #[test] + fn test_deserialize_vpc_lattice() { + let data = include_bytes!("../../lambda-events/src/fixtures/example-vpc-lattice-v2-request.json"); + + let req: LambdaRequest = serde_json::from_slice(data).expect("failed to deserialize vpc lattice data"); + match req { + LambdaRequest::VpcLatticeV2(req) => { + assert_eq!( + "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + req.request_context.target_group_arn + ); + } + other => panic!("unexpected request variant: {other:?}"), + } + } + + #[test] + #[cfg(feature = "pass_through")] + fn test_deserialize_bedrock_agent() { + let data = include_bytes!("../../lambda-events/src/fixtures/example-bedrock-agent-runtime-event.json"); + + let req: LambdaRequest = + serde_json::from_slice(data).expect("failed to deserialize bedrock agent request data"); + match req { + LambdaRequest::PassThrough(req) => { + assert_eq!(String::from_utf8_lossy(data), req); + } + other => panic!("unexpected request variant: {:?}", other), + } + } + + #[test] + #[cfg(feature = "pass_through")] + fn test_deserialize_sqs() { + let data = include_bytes!("../../lambda-events/src/fixtures/example-sqs-event.json"); + + let req: LambdaRequest = serde_json::from_slice(data).expect("failed to deserialize sqs event data"); + match req { + LambdaRequest::PassThrough(req) => { + assert_eq!(String::from_utf8_lossy(data), req); + } + other => panic!("unexpected request variant: {:?}", other), + } + } +} diff --git a/lambda-http/src/ext.rs b/lambda-http/src/ext.rs deleted file mode 100644 index 2056d48a4..000000000 --- a/lambda-http/src/ext.rs +++ /dev/null @@ -1,443 +0,0 @@ -//! Extension methods for `http::Request` types - -use crate::{request::RequestContext, Body}; -use aws_lambda_events::query_map::QueryMap; -use lambda_runtime::Context; -use serde::{de::value::Error as SerdeError, Deserialize}; -use std::{error::Error, fmt}; - -/// ALB/API gateway pre-parsed http query string parameters -pub(crate) struct QueryStringParameters(pub(crate) QueryMap); - -/// API gateway pre-extracted url path parameters -/// -/// These will always be empty for ALB requests -pub(crate) struct PathParameters(pub(crate) QueryMap); - -/// API gateway configured -/// [stage variables](https://docs.aws.amazon.com/apigateway/latest/developerguide/stage-variables.html) -/// -/// These will always be empty for ALB requests -pub(crate) struct StageVariables(pub(crate) QueryMap); - -/// ALB/API gateway raw http path without any stage information -pub(crate) struct RawHttpPath(pub(crate) String); - -/// Request payload deserialization errors -/// -/// Returned by [`RequestExt#payload()`](trait.RequestExt.html#tymethod.payload) -#[derive(Debug)] -pub enum PayloadError { - /// Returned when `application/json` bodies fail to deserialize a payload - Json(serde_json::Error), - /// Returned when `application/x-www-form-urlencoded` bodies fail to deserialize a payload - WwwFormUrlEncoded(SerdeError), -} - -impl fmt::Display for PayloadError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - PayloadError::Json(json) => writeln!(f, "failed to parse payload from application/json {}", json), - PayloadError::WwwFormUrlEncoded(form) => writeln!( - f, - "failed to parse payload from application/x-www-form-urlencoded {}", - form - ), - } - } -} - -impl Error for PayloadError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - match self { - PayloadError::Json(json) => Some(json), - PayloadError::WwwFormUrlEncoded(form) => Some(form), - } - } -} - -/// Extentions for `lambda_http::Request` structs that -/// provide access to [API gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format) -/// and [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html) -/// features. -/// -/// # Examples -/// -/// A request's body can be deserialized if its correctly encoded as per -/// the request's `Content-Type` header. The two supported content types are -/// `application/x-www-form-urlencoded` and `application/json`. -/// -/// The following handler will work an http request body of `x=1&y=2` -/// as well as `{"x":1, "y":2}` respectively. -/// -/// ```rust,no_run -/// use lambda_http::{service_fn, Error, Context, Body, IntoResponse, Request, Response, RequestExt}; -/// use serde::Deserialize; -/// -/// #[derive(Debug,Deserialize,Default)] -/// struct Args { -/// #[serde(default)] -/// x: usize, -/// #[serde(default)] -/// y: usize -/// } -/// -/// #[tokio::main] -/// async fn main() -> Result<(), Error> { -/// lambda_http::run(service_fn(add)).await?; -/// Ok(()) -/// } -/// -/// async fn add( -/// request: Request -/// ) -> Result, Error> { -/// let args: Args = request.payload() -/// .unwrap_or_else(|_parse_err| None) -/// .unwrap_or_default(); -/// Ok( -/// Response::new( -/// format!( -/// "{} + {} = {}", -/// args.x, -/// args.y, -/// args.x + args.y -/// ).into() -/// ) -/// ) -/// } -/// ``` -pub trait RequestExt { - /// Return the raw http path for a request without any stage information. - fn raw_http_path(&self) -> String; - - /// Configures instance with the raw http path. - fn with_raw_http_path(self, path: &str) -> Self; - - /// Return pre-parsed http query string parameters, parameters - /// provided after the `?` portion of a url, - /// associated with the API gateway request. - /// - /// The yielded value represents both single and multi-valued - /// parameters alike. When multiple query string parameters with the same - /// name are expected, `query_string_parameters().get_all("many")` to retrieve them all. - /// - /// No query parameters - /// will yield an empty `QueryMap`. - fn query_string_parameters(&self) -> QueryMap; - - /// Configures instance with query string parameters - /// - /// This is intended for use in mock testing contexts. - fn with_query_string_parameters(self, parameters: Q) -> Self - where - Q: Into; - - /// Return pre-extracted path parameters, parameter provided in url placeholders - /// `/foo/{bar}/baz/{boom}`, - /// associated with the API gateway request. No path parameters - /// will yield an empty `QueryMap` - /// - /// These will always be empty for ALB triggered requests - fn path_parameters(&self) -> QueryMap; - - /// Configures instance with path parameters - /// - /// This is intended for use in mock testing contexts. - fn with_path_parameters

(self, parameters: P) -> Self - where - P: Into; - - /// Return [stage variables](https://docs.aws.amazon.com/apigateway/latest/developerguide/stage-variables.html) - /// associated with the API gateway request. No stage parameters - /// will yield an empty `QueryMap` - /// - /// These will always be empty for ALB triggered requests - fn stage_variables(&self) -> QueryMap; - - /// Configures instance with stage variables under #[cfg(test)] configurations - /// - /// This is intended for use in mock testing contexts. - #[cfg(test)] - fn with_stage_variables(self, variables: V) -> Self - where - V: Into; - - /// Return request context data associated with the ALB or API gateway request - fn request_context(&self) -> RequestContext; - - /// Configures instance with request context - /// - /// This is intended for use in mock testing contexts. - fn with_request_context(self, context: RequestContext) -> Self; - - /// Return the Result of a payload parsed into a serde Deserializeable - /// type - /// - /// Currently only `application/x-www-form-urlencoded` - /// and `application/json` flavors of content type - /// are supported - /// - /// A [PayloadError](enum.PayloadError.html) will be returned for undeserializable - /// payloads. If no body is provided, `Ok(None)` will be returned. - fn payload(&self) -> Result, PayloadError> - where - for<'de> D: Deserialize<'de>; - - /// Return the Lambda function context associated with the request - fn lambda_context(&self) -> Context; - - /// Configures instance with lambda context - fn with_lambda_context(self, context: Context) -> Self; -} - -impl RequestExt for http::Request { - fn raw_http_path(&self) -> String { - self.extensions() - .get::() - .map(|ext| ext.0.clone()) - .unwrap_or_default() - } - - fn with_raw_http_path(self, path: &str) -> Self { - let mut s = self; - s.extensions_mut().insert(RawHttpPath(path.into())); - s - } - - fn query_string_parameters(&self) -> QueryMap { - self.extensions() - .get::() - .map(|ext| ext.0.clone()) - .unwrap_or_default() - } - - fn with_query_string_parameters(self, parameters: Q) -> Self - where - Q: Into, - { - let mut s = self; - s.extensions_mut().insert(QueryStringParameters(parameters.into())); - s - } - - fn path_parameters(&self) -> QueryMap { - self.extensions() - .get::() - .map(|ext| ext.0.clone()) - .unwrap_or_default() - } - - fn with_path_parameters

(self, parameters: P) -> Self - where - P: Into, - { - let mut s = self; - s.extensions_mut().insert(PathParameters(parameters.into())); - s - } - - fn stage_variables(&self) -> QueryMap { - self.extensions() - .get::() - .map(|ext| ext.0.clone()) - .unwrap_or_default() - } - - #[cfg(test)] - fn with_stage_variables(self, variables: V) -> Self - where - V: Into, - { - let mut s = self; - s.extensions_mut().insert(StageVariables(variables.into())); - s - } - - fn request_context(&self) -> RequestContext { - self.extensions() - .get::() - .cloned() - .expect("Request did not contain a request context") - } - - fn with_request_context(self, context: RequestContext) -> Self { - let mut s = self; - s.extensions_mut().insert(context); - s - } - - fn lambda_context(&self) -> Context { - self.extensions() - .get::() - .cloned() - .expect("Request did not contain a lambda context") - } - - fn with_lambda_context(self, context: Context) -> Self { - let mut s = self; - s.extensions_mut().insert(context); - s - } - - fn payload(&self) -> Result, PayloadError> - where - for<'de> D: Deserialize<'de>, - { - self.headers() - .get(http::header::CONTENT_TYPE) - .map(|ct| match ct.to_str() { - Ok(content_type) => { - if content_type.starts_with("application/x-www-form-urlencoded") { - return serde_urlencoded::from_bytes::(self.body().as_ref()) - .map_err(PayloadError::WwwFormUrlEncoded) - .map(Some); - } else if content_type.starts_with("application/json") { - return serde_json::from_slice::(self.body().as_ref()) - .map_err(PayloadError::Json) - .map(Some); - } - - Ok(None) - } - _ => Ok(None), - }) - .unwrap_or_else(|| Ok(None)) - } -} - -#[cfg(test)] -mod tests { - use crate::{Body, Request, RequestExt}; - use serde::Deserialize; - - #[test] - fn requests_can_mock_query_string_parameters_ext() { - let mocked = hashmap! { - "foo".into() => vec!["bar".into()] - }; - let request = Request::default().with_query_string_parameters(mocked.clone()); - assert_eq!(request.query_string_parameters(), mocked.into()); - } - - #[test] - fn requests_can_mock_path_parameters_ext() { - let mocked = hashmap! { - "foo".into() => vec!["bar".into()] - }; - let request = Request::default().with_path_parameters(mocked.clone()); - assert_eq!(request.path_parameters(), mocked.into()); - } - - #[test] - fn requests_can_mock_stage_variables_ext() { - let mocked = hashmap! { - "foo".into() => vec!["bar".into()] - }; - let request = Request::default().with_stage_variables(mocked.clone()); - assert_eq!(request.stage_variables(), mocked.into()); - } - - #[test] - fn requests_have_form_post_parsable_payloads() { - #[derive(Deserialize, Eq, PartialEq, Debug)] - struct Payload { - foo: String, - baz: usize, - } - let request = http::Request::builder() - .header("Content-Type", "application/x-www-form-urlencoded") - .body(Body::from("foo=bar&baz=2")) - .expect("failed to build request"); - let payload: Option = request.payload().unwrap_or_default(); - assert_eq!( - payload, - Some(Payload { - foo: "bar".into(), - baz: 2 - }) - ); - } - - #[test] - fn requests_have_json_parseable_payloads() { - #[derive(Deserialize, Eq, PartialEq, Debug)] - struct Payload { - foo: String, - baz: usize, - } - let request = http::Request::builder() - .header("Content-Type", "application/json") - .body(Body::from(r#"{"foo":"bar", "baz": 2}"#)) - .expect("failed to build request"); - let payload: Option = request.payload().unwrap_or_default(); - assert_eq!( - payload, - Some(Payload { - foo: "bar".into(), - baz: 2 - }) - ); - } - - #[test] - fn requests_match_form_post_content_type_with_charset() { - #[derive(Deserialize, Eq, PartialEq, Debug)] - struct Payload { - foo: String, - baz: usize, - } - let request = http::Request::builder() - .header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") - .body(Body::from("foo=bar&baz=2")) - .expect("failed to build request"); - let payload: Option = request.payload().unwrap_or_default(); - assert_eq!( - payload, - Some(Payload { - foo: "bar".into(), - baz: 2 - }) - ); - } - - #[test] - fn requests_match_json_content_type_with_charset() { - #[derive(Deserialize, Eq, PartialEq, Debug)] - struct Payload { - foo: String, - baz: usize, - } - let request = http::Request::builder() - .header("Content-Type", "application/json; charset=UTF-8") - .body(Body::from(r#"{"foo":"bar", "baz": 2}"#)) - .expect("failed to build request"); - let payload: Option = request.payload().unwrap_or_default(); - assert_eq!( - payload, - Some(Payload { - foo: "bar".into(), - baz: 2 - }) - ); - } - - #[test] - fn requests_omiting_content_types_do_not_support_parseable_payloads() { - #[derive(Deserialize, Eq, PartialEq, Debug)] - struct Payload { - foo: String, - baz: usize, - } - let request = http::Request::builder() - .body(Body::from(r#"{"foo":"bar", "baz": 2}"#)) - .expect("failed to bulid request"); - let payload: Option = request.payload().unwrap_or_default(); - assert_eq!(payload, None); - } - - #[test] - fn requests_can_mock_raw_http_path_ext() { - let request = Request::default().with_raw_http_path("/raw-path"); - assert_eq!("/raw-path", request.raw_http_path().as_str()); - } -} diff --git a/lambda-http/src/ext/extensions.rs b/lambda-http/src/ext/extensions.rs new file mode 100644 index 000000000..65bf8ac07 --- /dev/null +++ b/lambda-http/src/ext/extensions.rs @@ -0,0 +1,631 @@ +//! Extension methods for `http::Extensions` and `http::Request` types + +use aws_lambda_events::query_map::QueryMap; +use http::request::Parts; +use lambda_runtime::Context; + +use crate::request::RequestContext; + +/// ALB/API gateway pre-parsed http query string parameters +#[non_exhaustive] +#[derive(Clone)] +pub(crate) struct QueryStringParameters(pub(crate) QueryMap); + +/// API gateway pre-extracted url path parameters +/// +/// These will always be empty for ALB requests +#[non_exhaustive] +#[derive(Clone)] +pub(crate) struct PathParameters(pub(crate) QueryMap); + +/// API gateway configured +/// [stage variables](https://docs.aws.amazon.com/apigateway/latest/developerguide/stage-variables.html) +/// +/// These will always be empty for ALB requests +#[non_exhaustive] +#[derive(Clone)] +pub(crate) struct StageVariables(pub(crate) QueryMap); + +/// ALB/API gateway raw http path without any stage information +#[non_exhaustive] +#[derive(Clone)] +pub(crate) struct RawHttpPath(pub(crate) String); + +/// Extensions for [`lambda_http::Request`], `http::request::Parts`, and `http::Extensions` structs +/// that provide access to +/// [API gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format) +/// and [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html) +/// features. +/// +/// [`lambda_http::Request`]: crate::Request +pub trait RequestExt { + /// Return the raw http path for a request without any stage information. + fn raw_http_path(&self) -> &str; + + /// Configures instance with the raw http path. + fn with_raw_http_path(self, path: S) -> Self + where + S: Into; + + /// Return pre-parsed HTTP query string parameters, parameters + /// provided after the `?` portion of a URL, + /// associated with the API gateway request. + /// + /// The yielded value represents both single and multi-valued + /// parameters alike. When multiple query string parameters with the same + /// name are expected, use `query_string_parameters().all("many")` to + /// retrieve them all. + /// + /// Having no query parameters will yield an empty `QueryMap`. + fn query_string_parameters(&self) -> QueryMap; + + /// Return pre-parsed HTTP query string parameters, parameters + /// provided after the `?` portion of a URL, + /// associated with the API gateway request. + /// + /// The yielded value represents both single and multi-valued + /// parameters alike. When multiple query string parameters with the same + /// name are expected, use + /// `query_string_parameters_ref().and_then(|params| params.all("many"))` to + /// retrieve them all. + /// + /// Having no query parameters will yield `None`. + fn query_string_parameters_ref(&self) -> Option<&QueryMap>; + + /// Configures instance with query string parameters + /// + /// This is intended for use in mock testing contexts. + fn with_query_string_parameters(self, parameters: Q) -> Self + where + Q: Into; + + /// Return pre-extracted path parameters, parameter provided in URL placeholders + /// `/foo/{bar}/baz/{qux}`, + /// associated with the API gateway request. Having no path parameters + /// will yield an empty `QueryMap`. + /// + /// These will always be empty for ALB triggered requests. + fn path_parameters(&self) -> QueryMap; + + /// Return pre-extracted path parameters, parameter provided in URL placeholders + /// `/foo/{bar}/baz/{qux}`, + /// associated with the API gateway request. Having no path parameters + /// will yield `None`. + /// + /// These will always be `None` for ALB triggered requests. + fn path_parameters_ref(&self) -> Option<&QueryMap>; + + /// Configures instance with path parameters + /// + /// This is intended for use in mock testing contexts. + fn with_path_parameters

(self, parameters: P) -> Self + where + P: Into; + + /// Return [stage variables](https://docs.aws.amazon.com/apigateway/latest/developerguide/stage-variables.html) + /// associated with the API gateway request. Having no stage parameters + /// will yield an empty `QueryMap`. + /// + /// These will always be empty for ALB triggered requests. + fn stage_variables(&self) -> QueryMap; + + /// Return [stage variables](https://docs.aws.amazon.com/apigateway/latest/developerguide/stage-variables.html) + /// associated with the API gateway request. Having no stage parameters + /// will yield `None`. + /// + /// These will always be `None` for ALB triggered requests. + fn stage_variables_ref(&self) -> Option<&QueryMap>; + + /// Configures instance with stage variables + /// + /// This is intended for use in mock testing contexts. + fn with_stage_variables(self, variables: V) -> Self + where + V: Into; + + /// Return request context data associated with the ALB or + /// API gateway request + fn request_context(&self) -> RequestContext; + + /// Return a reference to the request context data associated with the ALB or + /// API gateway request + fn request_context_ref(&self) -> Option<&RequestContext>; + + /// Configures instance with request context + /// + /// This is intended for use in mock testing contexts. + fn with_request_context(self, context: RequestContext) -> Self; + + /// Return Lambda function context data associated with the + /// request + fn lambda_context(&self) -> Context; + + /// Return a reference to the Lambda function context data associated with the + /// request + fn lambda_context_ref(&self) -> Option<&Context>; + + /// Configures instance with lambda context + fn with_lambda_context(self, context: Context) -> Self; +} + +impl RequestExt for http::Extensions { + fn raw_http_path(&self) -> &str { + self.get::() + .map(|RawHttpPath(path)| path.as_str()) + .unwrap_or_default() + } + + fn with_raw_http_path(self, path: S) -> Self + where + S: Into, + { + let mut s = self; + s.insert(RawHttpPath(path.into())); + s + } + + fn query_string_parameters(&self) -> QueryMap { + self.query_string_parameters_ref().cloned().unwrap_or_default() + } + + fn query_string_parameters_ref(&self) -> Option<&QueryMap> { + self.get::().and_then( + |QueryStringParameters(params)| { + if params.is_empty() { + None + } else { + Some(params) + } + }, + ) + } + + fn with_query_string_parameters(self, parameters: Q) -> Self + where + Q: Into, + { + let mut s = self; + s.insert(QueryStringParameters(parameters.into())); + s + } + + fn path_parameters(&self) -> QueryMap { + self.path_parameters_ref().cloned().unwrap_or_default() + } + + fn path_parameters_ref(&self) -> Option<&QueryMap> { + self.get::().and_then( + |PathParameters(params)| { + if params.is_empty() { + None + } else { + Some(params) + } + }, + ) + } + + fn with_path_parameters

(self, parameters: P) -> Self + where + P: Into, + { + let mut s = self; + s.insert(PathParameters(parameters.into())); + s + } + + fn stage_variables(&self) -> QueryMap { + self.stage_variables_ref().cloned().unwrap_or_default() + } + + fn stage_variables_ref(&self) -> Option<&QueryMap> { + self.get::() + .and_then(|StageVariables(vars)| if vars.is_empty() { None } else { Some(vars) }) + } + + fn with_stage_variables(self, variables: V) -> Self + where + V: Into, + { + let mut s = self; + s.insert(StageVariables(variables.into())); + s + } + + fn request_context(&self) -> RequestContext { + self.request_context_ref() + .cloned() + .expect("Request did not contain a request context") + } + + fn request_context_ref(&self) -> Option<&RequestContext> { + self.get::() + } + + fn with_request_context(self, context: RequestContext) -> Self { + let mut s = self; + s.insert(context); + s + } + + fn lambda_context(&self) -> Context { + self.lambda_context_ref() + .cloned() + .expect("Request did not contain a lambda context") + } + + fn lambda_context_ref(&self) -> Option<&Context> { + self.get::() + } + + fn with_lambda_context(self, context: Context) -> Self { + let mut s = self; + s.insert(context); + s + } +} + +impl RequestExt for Parts { + fn raw_http_path(&self) -> &str { + self.extensions.raw_http_path() + } + + fn with_raw_http_path(self, path: S) -> Self + where + S: Into, + { + let mut s = self; + s.extensions = s.extensions.with_raw_http_path(path); + + s + } + + fn query_string_parameters(&self) -> QueryMap { + self.extensions.query_string_parameters() + } + + fn query_string_parameters_ref(&self) -> Option<&QueryMap> { + self.extensions.query_string_parameters_ref() + } + + fn with_query_string_parameters(self, parameters: Q) -> Self + where + Q: Into, + { + let mut s = self; + s.extensions = s.extensions.with_query_string_parameters(parameters); + + s + } + + fn path_parameters(&self) -> QueryMap { + self.extensions.path_parameters() + } + + fn path_parameters_ref(&self) -> Option<&QueryMap> { + self.extensions.path_parameters_ref() + } + + fn with_path_parameters

(self, parameters: P) -> Self + where + P: Into, + { + let mut s = self; + s.extensions = s.extensions.with_path_parameters(parameters); + + s + } + + fn stage_variables(&self) -> QueryMap { + self.extensions.stage_variables() + } + + fn stage_variables_ref(&self) -> Option<&QueryMap> { + self.extensions.stage_variables_ref() + } + + fn with_stage_variables(self, variables: V) -> Self + where + V: Into, + { + let mut s = self; + s.extensions = s.extensions.with_stage_variables(variables); + + s + } + + fn request_context(&self) -> RequestContext { + self.extensions.request_context() + } + + fn request_context_ref(&self) -> Option<&RequestContext> { + self.extensions.request_context_ref() + } + + fn with_request_context(self, context: RequestContext) -> Self { + let mut s = self; + s.extensions = s.extensions.with_request_context(context); + + s + } + + fn lambda_context(&self) -> Context { + self.extensions.lambda_context() + } + + fn lambda_context_ref(&self) -> Option<&Context> { + self.extensions.lambda_context_ref() + } + + fn with_lambda_context(self, context: Context) -> Self { + let mut s = self; + s.extensions = s.extensions.with_lambda_context(context); + + s + } +} + +fn map_req_ext(req: http::Request, f: F) -> http::Request +where + F: FnOnce(http::Extensions) -> http::Extensions, +{ + let (mut parts, body) = req.into_parts(); + parts.extensions = (f)(parts.extensions); + + http::Request::from_parts(parts, body) +} + +impl RequestExt for http::Request { + fn raw_http_path(&self) -> &str { + self.extensions().raw_http_path() + } + + fn with_raw_http_path(self, path: S) -> Self + where + S: Into, + { + map_req_ext(self, |ext| ext.with_raw_http_path(path)) + } + + fn query_string_parameters(&self) -> QueryMap { + self.extensions().query_string_parameters() + } + + fn query_string_parameters_ref(&self) -> Option<&QueryMap> { + self.extensions().query_string_parameters_ref() + } + + fn with_query_string_parameters(self, parameters: Q) -> Self + where + Q: Into, + { + map_req_ext(self, |ext| ext.with_query_string_parameters(parameters)) + } + + fn path_parameters(&self) -> QueryMap { + self.extensions().path_parameters() + } + + fn path_parameters_ref(&self) -> Option<&QueryMap> { + self.extensions().path_parameters_ref() + } + + fn with_path_parameters

(self, parameters: P) -> Self + where + P: Into, + { + map_req_ext(self, |ext| ext.with_path_parameters(parameters)) + } + + fn stage_variables(&self) -> QueryMap { + self.extensions().stage_variables() + } + + fn stage_variables_ref(&self) -> Option<&QueryMap> { + self.extensions().stage_variables_ref() + } + + fn with_stage_variables(self, variables: V) -> Self + where + V: Into, + { + map_req_ext(self, |ext| ext.with_stage_variables(variables)) + } + + fn request_context(&self) -> RequestContext { + self.extensions().request_context() + } + + fn request_context_ref(&self) -> Option<&RequestContext> { + self.extensions().request_context_ref() + } + + fn with_request_context(self, context: RequestContext) -> Self { + map_req_ext(self, |ext| ext.with_request_context(context)) + } + + fn lambda_context(&self) -> Context { + self.extensions().lambda_context() + } + + fn lambda_context_ref(&self) -> Option<&Context> { + self.extensions().lambda_context_ref() + } + + fn with_lambda_context(self, context: Context) -> Self { + map_req_ext(self, |ext| ext.with_lambda_context(context)) + } +} + +#[cfg(test)] +mod tests { + use aws_lambda_events::query_map::QueryMap; + use http::Extensions; + + use crate::Request; + + use super::RequestExt; + + #[test] + fn extensions_can_mock_query_string_parameters_ext() { + let ext = Extensions::default(); + assert_eq!(ext.query_string_parameters_ref(), None); + assert_eq!(ext.query_string_parameters(), QueryMap::default()); + + let mocked: QueryMap = hashmap! { + "foo".into() => vec!["bar".into()] + } + .into(); + + let ext = ext.with_query_string_parameters(mocked.clone()); + assert_eq!(ext.query_string_parameters_ref(), Some(&mocked)); + assert_eq!(ext.query_string_parameters(), mocked); + } + + #[test] + fn parts_can_mock_query_string_parameters_ext() { + let (parts, _) = Request::default().into_parts(); + assert_eq!(parts.query_string_parameters_ref(), None); + assert_eq!(parts.query_string_parameters(), QueryMap::default()); + + let mocked: QueryMap = hashmap! { + "foo".into() => vec!["bar".into()] + } + .into(); + + let parts = parts.with_query_string_parameters(mocked.clone()); + assert_eq!(parts.query_string_parameters_ref(), Some(&mocked)); + assert_eq!(parts.query_string_parameters(), mocked); + } + + #[test] + fn requests_can_mock_query_string_parameters_ext() { + let request = Request::default(); + assert_eq!(request.query_string_parameters_ref(), None); + assert_eq!(request.query_string_parameters(), QueryMap::default()); + + let mocked: QueryMap = hashmap! { + "foo".into() => vec!["bar".into()] + } + .into(); + + let request = request.with_query_string_parameters(mocked.clone()); + assert_eq!(request.query_string_parameters_ref(), Some(&mocked)); + assert_eq!(request.query_string_parameters(), mocked); + } + + #[test] + fn extensions_can_mock_path_parameters_ext() { + let ext = Extensions::default(); + assert_eq!(ext.path_parameters_ref(), None); + assert_eq!(ext.path_parameters(), QueryMap::default()); + + let mocked: QueryMap = hashmap! { + "foo".into() => vec!["bar".into()] + } + .into(); + + let ext = ext.with_path_parameters(mocked.clone()); + assert_eq!(ext.path_parameters_ref(), Some(&mocked)); + assert_eq!(ext.path_parameters(), mocked); + } + + #[test] + fn parts_can_mock_path_parameters_ext() { + let (parts, _) = Request::default().into_parts(); + assert_eq!(parts.path_parameters_ref(), None); + assert_eq!(parts.path_parameters(), QueryMap::default()); + + let mocked: QueryMap = hashmap! { + "foo".into() => vec!["bar".into()] + } + .into(); + + let parts = parts.with_path_parameters(mocked.clone()); + assert_eq!(parts.path_parameters_ref(), Some(&mocked)); + assert_eq!(parts.path_parameters(), mocked); + } + + #[test] + fn requests_can_mock_path_parameters_ext() { + let request = Request::default(); + assert_eq!(request.path_parameters_ref(), None); + assert_eq!(request.path_parameters(), QueryMap::default()); + + let mocked: QueryMap = hashmap! { + "foo".into() => vec!["bar".into()] + } + .into(); + + let request = request.with_path_parameters(mocked.clone()); + assert_eq!(request.path_parameters_ref(), Some(&mocked)); + assert_eq!(request.path_parameters(), mocked); + } + + #[test] + fn extensions_can_mock_stage_variables_ext() { + let ext = Extensions::default(); + assert_eq!(ext.stage_variables_ref(), None); + assert_eq!(ext.stage_variables(), QueryMap::default()); + + let mocked: QueryMap = hashmap! { + "foo".into() => vec!["bar".into()] + } + .into(); + + let ext = ext.with_stage_variables(mocked.clone()); + assert_eq!(ext.stage_variables_ref(), Some(&mocked)); + assert_eq!(ext.stage_variables(), mocked); + } + + #[test] + fn parts_can_mock_stage_variables_ext() { + let (parts, _) = Request::default().into_parts(); + assert_eq!(parts.stage_variables_ref(), None); + assert_eq!(parts.stage_variables(), QueryMap::default()); + + let mocked: QueryMap = hashmap! { + "foo".into() => vec!["bar".into()] + } + .into(); + + let parts = parts.with_stage_variables(mocked.clone()); + assert_eq!(parts.stage_variables_ref(), Some(&mocked)); + assert_eq!(parts.stage_variables(), mocked); + } + + #[test] + fn requests_can_mock_stage_variables_ext() { + let request = Request::default(); + assert_eq!(request.stage_variables_ref(), None); + assert_eq!(request.stage_variables(), QueryMap::default()); + + let mocked: QueryMap = hashmap! { + "foo".into() => vec!["bar".into()] + } + .into(); + + let request = request.with_stage_variables(mocked.clone()); + assert_eq!(request.stage_variables_ref(), Some(&mocked)); + assert_eq!(request.stage_variables(), mocked); + } + + #[test] + fn extensions_can_mock_raw_http_path_ext() { + let ext = Extensions::default().with_raw_http_path("/raw-path"); + assert_eq!("/raw-path", ext.raw_http_path()); + } + + #[test] + fn parts_can_mock_raw_http_path_ext() { + let (parts, _) = Request::default().into_parts(); + let parts = parts.with_raw_http_path("/raw-path"); + assert_eq!("/raw-path", parts.raw_http_path()); + } + + #[test] + fn requests_can_mock_raw_http_path_ext() { + let request = Request::default().with_raw_http_path("/raw-path"); + assert_eq!("/raw-path", request.raw_http_path()); + } +} diff --git a/lambda-http/src/ext/mod.rs b/lambda-http/src/ext/mod.rs new file mode 100644 index 000000000..81c64daa3 --- /dev/null +++ b/lambda-http/src/ext/mod.rs @@ -0,0 +1,7 @@ +//! Extension methods for `Request` types + +pub mod extensions; +pub mod request; + +pub use extensions::RequestExt; +pub use request::{PayloadError, RequestPayloadExt}; diff --git a/lambda-http/src/ext/request.rs b/lambda-http/src/ext/request.rs new file mode 100644 index 000000000..38e45afa1 --- /dev/null +++ b/lambda-http/src/ext/request.rs @@ -0,0 +1,614 @@ +//! Extension methods for `Request` types + +use std::{error::Error, fmt}; + +use serde::{ + de::{value::Error as SerdeError, DeserializeOwned}, + Deserialize, +}; + +use crate::Body; + +/// Request payload deserialization errors +/// +/// Returned by [`RequestPayloadExt::payload()`] +#[non_exhaustive] +#[derive(Debug)] +pub enum PayloadError { + /// Returned when `application/json` bodies fail to deserialize a payload + Json(serde_json::Error), + /// Returned when `application/x-www-form-urlencoded` bodies fail to deserialize a payload + WwwFormUrlEncoded(SerdeError), +} + +/// Indicates a problem processing a JSON payload. +#[non_exhaustive] +#[derive(Debug)] +pub enum JsonPayloadError { + /// Problem deserializing a JSON payload. + Parsing(serde_json::Error), +} + +/// Indicates a problem processing an x-www-form-urlencoded payload. +#[non_exhaustive] +#[derive(Debug)] +pub enum FormUrlEncodedPayloadError { + /// Problem deserializing an x-www-form-urlencoded payload. + Parsing(SerdeError), +} + +impl From for PayloadError { + fn from(err: JsonPayloadError) -> Self { + match err { + JsonPayloadError::Parsing(inner_err) => PayloadError::Json(inner_err), + } + } +} + +impl From for PayloadError { + fn from(err: FormUrlEncodedPayloadError) -> Self { + match err { + FormUrlEncodedPayloadError::Parsing(inner_err) => PayloadError::WwwFormUrlEncoded(inner_err), + } + } +} + +impl fmt::Display for PayloadError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PayloadError::Json(json) => writeln!(f, "failed to parse payload from application/json {json}"), + PayloadError::WwwFormUrlEncoded(form) => writeln!( + f, + "failed to parse payload from application/x-www-form-urlencoded {form}" + ), + } + } +} + +impl Error for PayloadError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + PayloadError::Json(json) => Some(json), + PayloadError::WwwFormUrlEncoded(form) => Some(form), + } + } +} + +/// Extends `http::Request` with payload deserialization helpers. +pub trait RequestPayloadExt { + /// Return the result of a payload parsed into a type that implements [`serde::Deserialize`] + /// + /// Currently only `application/x-www-form-urlencoded` + /// and `application/json` flavors of content type + /// are supported + /// + /// A [`PayloadError`] will be returned for undeserializable payloads. + /// If no body is provided, the content-type header is missing, + /// or the content-type header is unsupported, then `Ok(None)` will + /// be returned. Note that a blank body (e.g. an empty string) is treated + /// like a present payload by some deserializers and may result in an error. + /// + /// ### Examples + /// + /// A request's body can be deserialized if its correctly encoded as per + /// the request's `Content-Type` header. The two supported content types are + /// `application/x-www-form-urlencoded` and `application/json`. + /// + /// The following handler will work an http request body of `x=1&y=2` + /// as well as `{"x":1, "y":2}` respectively. + /// + /// ```rust,no_run + /// use lambda_http::{ + /// service_fn, Body, Context, Error, IntoResponse, Request, RequestPayloadExt, Response, + /// }; + /// use serde::Deserialize; + /// + /// #[derive(Debug, Default, Deserialize)] + /// struct Args { + /// #[serde(default)] + /// x: usize, + /// #[serde(default)] + /// y: usize + /// } + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Error> { + /// lambda_http::run(service_fn(add)).await?; + /// Ok(()) + /// } + /// + /// async fn add( + /// request: Request + /// ) -> Result, Error> { + /// let args: Args = request.payload() + /// .unwrap_or_else(|_parse_err| None) + /// .unwrap_or_default(); + /// Ok( + /// Response::new( + /// format!( + /// "{} + {} = {}", + /// args.x, + /// args.y, + /// args.x + args.y + /// ).into() + /// ) + /// ) + /// } + fn payload(&self) -> Result, PayloadError> + where + D: DeserializeOwned; + + /// Attempts to deserialize the request payload as JSON. When there is no payload, + /// `Ok(None)` is returned. + /// + /// ### Errors + /// + /// If a present payload is not a valid JSON payload matching the annotated type, + /// a [`JsonPayloadError`] is returned. + /// + /// ### Examples + /// + /// #### 1. Parsing a JSONString. + /// ```ignore + /// let req = http::Request::builder() + /// .body(Body::from("\"I am a JSON string\"")) + /// .expect("failed to build request"); + /// match req.json::() { + /// Ok(Some(json)) => assert_eq!(json, "I am a JSON string"), + /// Ok(None) => panic!("payload is missing."), + /// Err(err) => panic!("error processing json: {err:?}"), + /// } + /// ``` + /// + /// #### 2. Parsing a JSONObject. + /// ```ignore + /// #[derive(Deserialize, Eq, PartialEq, Debug)] + /// struct Person { + /// name: String, + /// age: u8, + /// } + /// + /// let req = http::Request::builder() + /// .body(Body::from(r#"{"name": "Adam", "age": 23}"#)) + /// .expect("failed to build request"); + /// + /// match req.json::() { + /// Ok(Some(person)) => assert_eq!( + /// person, + /// Person { + /// name: "Adam".to_string(), + /// age: 23 + /// } + /// ), + /// Ok(None) => panic!("payload is missing"), + /// Err(JsonPayloadError::Parsing(err)) => { + /// if err.is_data() { + /// panic!("payload does not match Person schema: {err:?}") + /// } + /// if err.is_syntax() { + /// panic!("payload is invalid json: {err:?}") + /// } + /// panic!("failed to parse json: {err:?}") + /// } + /// } + /// ``` + fn json(&self) -> Result, JsonPayloadError> + where + D: DeserializeOwned; + + /// Attempts to deserialize the request payload as an application/x-www-form-urlencoded + /// content type. When there is no payload, `Ok(None)` is returned. + /// + /// ### Errors + /// + /// If a present payload is not a valid application/x-www-form-urlencoded payload + /// matching the annotated type, a [`FormUrlEncodedPayloadError`] is returned. + /// + /// ### Examples + /// ```ignore + /// let req = http::Request::builder() + /// .body(Body::from("name=Adam&age=23")) + /// .expect("failed to build request"); + /// match req.form_url_encoded::() { + /// Ok(Some(person)) => assert_eq!( + /// person, + /// Person { + /// name: "Adam".to_string(), + /// age: 23 + /// } + /// ), + /// Ok(None) => panic!("payload is missing."), + /// Err(err) => panic!("error processing payload: {err:?}"), + /// } + /// ``` + fn form_url_encoded(&self) -> Result, FormUrlEncodedPayloadError> + where + D: DeserializeOwned; +} + +impl RequestPayloadExt for http::Request { + fn payload(&self) -> Result, PayloadError> + where + for<'de> D: Deserialize<'de>, + { + self.headers() + .get(http::header::CONTENT_TYPE) + .map(|ct| match ct.to_str() { + Ok(content_type) => { + if content_type.starts_with("application/x-www-form-urlencoded") { + return self.form_url_encoded().map_err(PayloadError::from); + } else if content_type.starts_with("application/json") { + return self.json().map_err(PayloadError::from); + } + Ok(None) + } + _ => Ok(None), + }) + .unwrap_or_else(|| Ok(None)) + } + + fn json(&self) -> Result, JsonPayloadError> + where + D: DeserializeOwned, + { + if self.body().is_empty() { + return Ok(None); + } + serde_json::from_slice::(self.body().as_ref()) + .map(Some) + .map_err(JsonPayloadError::Parsing) + } + + fn form_url_encoded(&self) -> Result, FormUrlEncodedPayloadError> + where + D: DeserializeOwned, + { + if self.body().is_empty() { + return Ok(None); + } + serde_urlencoded::from_bytes::(self.body().as_ref()) + .map(Some) + .map_err(FormUrlEncodedPayloadError::Parsing) + } +} + +#[cfg(test)] +mod tests { + use serde::Deserialize; + + use super::{FormUrlEncodedPayloadError, JsonPayloadError, RequestPayloadExt}; + + use crate::Body; + + #[derive(Deserialize, Eq, PartialEq, Debug)] + struct Payload { + foo: String, + baz: usize, + } + + fn get_test_payload_as_json_body() -> Body { + Body::from(r#"{"foo":"bar", "baz": 2}"#) + } + + fn assert_eq_test_payload(payload: Option) { + assert_eq!( + payload, + Some(Payload { + foo: "bar".into(), + baz: 2 + }) + ); + } + + #[test] + fn requests_have_form_post_parsable_payloads() { + let request = http::Request::builder() + .header("Content-Type", "application/x-www-form-urlencoded") + .body(Body::from("foo=bar&baz=2")) + .expect("failed to build request"); + let payload: Option = request.payload().unwrap_or_default(); + assert_eq!( + payload, + Some(Payload { + foo: "bar".into(), + baz: 2 + }) + ); + } + + #[test] + fn requests_have_json_parsable_payloads() { + let request = http::Request::builder() + .header("Content-Type", "application/json") + .body(get_test_payload_as_json_body()) + .expect("failed to build request"); + let payload: Option = request.payload().unwrap_or_default(); + assert_eq_test_payload(payload) + } + + #[test] + fn requests_match_form_post_content_type_with_charset() { + let request = http::Request::builder() + .header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") + .body(Body::from("foo=bar&baz=2")) + .expect("failed to build request"); + let payload: Option = request.payload().unwrap_or_default(); + assert_eq!( + payload, + Some(Payload { + foo: "bar".into(), + baz: 2 + }) + ); + } + + #[test] + fn requests_match_json_content_type_with_charset() { + let request = http::Request::builder() + .header("Content-Type", "application/json; charset=UTF-8") + .body(Body::from(r#"{"foo":"bar", "baz": 2}"#)) + .expect("failed to build request"); + let payload: Option = request.payload().unwrap_or_default(); + assert_eq!( + payload, + Some(Payload { + foo: "bar".into(), + baz: 2 + }) + ); + } + + #[test] + fn requests_omitting_content_types_do_not_support_parsable_payloads() { + let request = http::Request::builder() + .body(Body::from(r#"{"foo":"bar", "baz": 2}"#)) + .expect("failed to build request"); + let payload: Option = request.payload().unwrap_or_default(); + assert_eq!(payload, None); + } + + #[test] + fn requests_omitting_body_returns_none() { + let request = http::Request::builder() + .body(Body::Empty) + .expect("failed to build request"); + let payload: Option = request.payload().unwrap(); + assert_eq!(payload, None) + } + + #[test] + fn requests_with_json_content_type_hdr_omitting_body_returns_none() { + let request = http::Request::builder() + .header("Content-Type", "application/json; charset=UTF-8") + .body(Body::Empty) + .expect("failed to build request"); + let payload: Option = request.payload().unwrap(); + assert_eq!(payload, None) + } + + #[test] + fn requests_with_formurlencoded_content_type_hdr_omitting_body_returns_none() { + let request = http::Request::builder() + .header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") + .body(Body::Empty) + .expect("failed to build request"); + let payload: Option = request.payload().unwrap(); + assert_eq!(payload, None) + } + + #[derive(Deserialize, Eq, PartialEq, Debug)] + struct Person { + name: String, + age: u8, + } + + #[test] + fn json_fn_parses_json_strings() { + let req = http::Request::builder() + .body(Body::from("\"I am a JSON string\"")) + .expect("failed to build request"); + match req.json::() { + Ok(Some(json)) => assert_eq!(json, "I am a JSON string"), + Ok(None) => panic!("payload is missing."), + Err(err) => panic!("error processing json: {err:?}"), + } + } + + #[test] + fn json_fn_parses_objects() { + let req = http::Request::builder() + .body(Body::from(r#"{"name": "Adam", "age": 23}"#)) + .expect("failed to build request"); + + match req.json::() { + Ok(Some(person)) => assert_eq!( + person, + Person { + name: "Adam".to_string(), + age: 23 + } + ), + Ok(None) => panic!("request data missing"), + Err(JsonPayloadError::Parsing(err)) => { + if err.is_data() { + panic!("payload does not match Person: {err:?}") + } + if err.is_syntax() { + panic!("invalid json: {err:?}") + } + panic!("failed to parse json: {err:?}") + } + } + } + + #[test] + fn json_fn_parses_list_of_objects() { + let req = http::Request::builder() + .body(Body::from( + r#"[{"name": "Adam", "age": 23}, {"name": "Sarah", "age": 47}]"#, + )) + .expect("failed to build request"); + let expected_result = vec![ + Person { + name: "Adam".to_string(), + age: 23, + }, + Person { + name: "Sarah".to_string(), + age: 47, + }, + ]; + let result: Vec = req.json().expect("invalid payload").expect("missing payload"); + assert_eq!(result, expected_result); + } + + #[test] + fn json_fn_parses_nested_objects() { + #[derive(Deserialize, Eq, PartialEq, Debug)] + struct Pet { + name: String, + owner: Person, + } + + let req = http::Request::builder() + .body(Body::from( + r#"{"name": "Gumball", "owner": {"name": "Adam", "age": 23}}"#, + )) + .expect("failed to build request"); + + let expected_result = Pet { + name: "Gumball".to_string(), + owner: Person { + name: "Adam".to_string(), + age: 23, + }, + }; + let result: Pet = req.json().expect("invalid payload").expect("missing payload"); + assert_eq!(result, expected_result); + } + + #[test] + fn json_fn_accepts_request_with_content_type_header() { + let request = http::Request::builder() + .header("Content-Type", "application/json") + .body(get_test_payload_as_json_body()) + .expect("failed to build request"); + let payload: Option = request.json().unwrap(); + assert_eq_test_payload(payload) + } + + #[test] + fn json_fn_accepts_request_without_content_type_header() { + let request = http::Request::builder() + .body(get_test_payload_as_json_body()) + .expect("failed to build request"); + let payload: Option = request.json().expect("failed to parse json"); + assert_eq_test_payload(payload) + } + + #[test] + fn json_fn_given_nonjson_payload_returns_syntax_error() { + let request = http::Request::builder() + .body(Body::Text(String::from("Not a JSON"))) + .expect("failed to build request"); + let payload = request.json::(); + assert!(payload.is_err()); + + if let Err(JsonPayloadError::Parsing(err)) = payload { + assert!(err.is_syntax()) + } else { + panic!( + "{}", + format!("payload should have caused a parsing error. instead, it was {payload:?}") + ); + } + } + + #[test] + fn json_fn_given_unexpected_payload_shape_returns_data_error() { + let request = http::Request::builder() + .body(Body::from(r#"{"foo":"bar", "baz": "!SHOULD BE A NUMBER!"}"#)) + .expect("failed to build request"); + let result = request.json::(); + + if let Err(JsonPayloadError::Parsing(err)) = result { + assert!(err.is_data()) + } else { + panic!( + "{}", + format!("payload should have caused a parsing error. instead, it was {result:?}") + ); + } + } + + #[test] + fn json_fn_given_empty_payload_returns_none() { + let empty_request = http::Request::default(); + let payload: Option = empty_request.json().expect("failed to parse json"); + assert_eq!(payload, None) + } + + #[test] + fn form_url_encoded_fn_parses_forms() { + let req = http::Request::builder() + .body(Body::from("name=Adam&age=23")) + .expect("failed to build request"); + match req.form_url_encoded::() { + Ok(Some(person)) => assert_eq!( + person, + Person { + name: "Adam".to_string(), + age: 23 + } + ), + Ok(None) => panic!("payload is missing."), + Err(err) => panic!("error processing payload: {err:?}"), + } + } + + #[test] + fn form_url_encoded_fn_accepts_request_with_content_type_header() { + let request = http::Request::builder() + .header("Content-Type", "application/x-www-form-urlencoded") + .body(Body::from("foo=bar&baz=2")) + .expect("failed to build request"); + let payload: Option = request.form_url_encoded().unwrap(); + assert_eq_test_payload(payload); + } + + #[test] + fn form_url_encoded_fn_accepts_request_without_content_type_header() { + let request = http::Request::builder() + .body(Body::from("foo=bar&baz=2")) + .expect("failed to build request"); + let payload: Option = request.form_url_encoded().expect("failed to parse form"); + assert_eq_test_payload(payload); + } + + #[test] + fn form_url_encoded_fn_given_non_form_urlencoded_payload_errors() { + let request = http::Request::builder() + .body(Body::Text(String::from("Not a url-encoded form"))) + .expect("failed to build request"); + let payload = request.form_url_encoded::(); + assert!(payload.is_err()); + assert!(matches!(payload, Err(FormUrlEncodedPayloadError::Parsing(_)))); + } + + #[test] + fn form_url_encoded_fn_given_unexpected_payload_shape_errors() { + let request = http::Request::builder() + .body(Body::from("foo=bar&baz=SHOULD_BE_A_NUMBER")) + .expect("failed to build request"); + let result = request.form_url_encoded::(); + assert!(result.is_err()); + assert!(matches!(result, Err(FormUrlEncodedPayloadError::Parsing(_)))); + } + + #[test] + fn form_url_encoded_fn_given_empty_payload_returns_none() { + let empty_request = http::Request::default(); + let payload: Option = empty_request.form_url_encoded().expect("failed to parse form"); + assert_eq!(payload, None); + } +} diff --git a/lambda-http/src/lib.rs b/lambda-http/src/lib.rs index ebb3e0c54..caea86181 100644 --- a/lambda-http/src/lib.rs +++ b/lambda-http/src/lib.rs @@ -1,7 +1,11 @@ #![warn(missing_docs, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg))] //#![deny(warnings)] //! Enriches the `lambda` crate with [`http`](https://github.com/hyperium/http) -//! types targeting AWS [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html), [API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html) REST and HTTP API lambda integrations. +//! types targeting AWS +//! * [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html) +//! * [API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html) REST and HTTP API lambda integrations +//! * [VPC Lattice](https://docs.aws.amazon.com/vpc-lattice/latest/ug/lambda-functions.html) //! //! This crate abstracts over all of these trigger events using standard [`http`](https://github.com/hyperium/http) types minimizing the mental overhead //! of understanding the nuances and variation between trigger details allowing you to focus more on your application while also giving you to the maximum flexibility to @@ -32,11 +36,11 @@ //! //! ## Leveraging trigger provided data //! -//! You can also access information provided directly from the underlying trigger events, like query string parameters, -//! or Lambda function context, with the [`RequestExt`](trait.RequestExt.html) trait. +//! You can also access information provided directly from the underlying trigger events, +//! like query string parameters, or Lambda function context, with the [`RequestExt`] trait. //! //! ```rust,no_run -//! use lambda_http::{service_fn, Error, IntoResponse, Request, RequestExt}; +//! use lambda_http::{service_fn, Error, RequestExt, IntoResponse, Request}; //! //! #[tokio::main] //! async fn main() -> Result<(), Error> { @@ -47,13 +51,13 @@ //! async fn hello( //! request: Request //! ) -> Result { -//! let _context = request.lambda_context(); +//! let _context = request.lambda_context_ref(); //! //! Ok(format!( //! "hello {}", //! request -//! .query_string_parameters() -//! .first("name") +//! .query_string_parameters_ref() +//! .and_then(|params| params.first("name")) //! .unwrap_or_else(|| "stranger") //! )) //! } @@ -65,15 +69,23 @@ extern crate maplit; pub use http::{self, Response}; -use lambda_runtime::LambdaEvent; -pub use lambda_runtime::{self, service_fn, tower, Context, Error, Service}; +/// Utilities to initialize and use `tracing` and `tracing-subscriber` in Lambda Functions. +#[cfg(feature = "tracing")] +#[cfg_attr(docsrs, doc(cfg(feature = "tracing")))] +pub use lambda_runtime::tracing; +use lambda_runtime::Diagnostic; +pub use lambda_runtime::{self, service_fn, tower, BoxFuture, Context, Error, LambdaEvent, Service, SnapStartResource}; use request::RequestFuture; use response::ResponseFuture; +mod deserializer; pub mod ext; pub mod request; mod response; -pub use crate::{ext::RequestExt, response::IntoResponse}; +pub use crate::{ + ext::{RequestExt, RequestPayloadExt}, + response::IntoResponse, +}; use crate::{ request::{LambdaRequest, RequestOrigin}, response::LambdaResponse, @@ -92,19 +104,25 @@ use std::{ task::{Context as TaskContext, Poll}, }; +mod streaming; +pub use streaming::{run_with_streaming_response, streaming_runtime, StreamAdapter}; +#[cfg(feature = "concurrency-tokio")] +pub use streaming::{run_with_streaming_response_concurrent, streaming_runtime_concurrent}; + /// Type alias for `http::Request`s with a fixed [`Body`](enum.Body.html) type pub type Request = http::Request; /// Future that will convert an [`IntoResponse`] into an actual [`LambdaResponse`] /// /// This is used by the `Adapter` wrapper and is completely internal to the `lambda_http::run` function. +#[non_exhaustive] #[doc(hidden)] pub enum TransformResponse<'a, R, E> { Request(RequestOrigin, RequestFuture<'a, R, E>), Response(RequestOrigin, ResponseFuture), } -impl<'a, R, E> Future for TransformResponse<'a, R, E> +impl Future for TransformResponse<'_, R, E> where R: IntoResponse, { @@ -131,12 +149,25 @@ where /// Wraps a `Service` in a `Service>` /// /// This is completely internal to the `lambda_http::run` function. +#[non_exhaustive] #[doc(hidden)] pub struct Adapter<'a, R, S> { service: S, _phantom_data: PhantomData<&'a R>, } +impl<'a, R, S> Clone for Adapter<'a, R, S> +where + S: Clone, +{ + fn clone(&self) -> Self { + Self { + service: self.service.clone(), + _phantom_data: PhantomData, + } + } +} + impl<'a, R, S, E> From for Adapter<'a, R, S> where S: Service, @@ -166,9 +197,11 @@ where } fn call(&mut self, req: LambdaEvent) -> Self::Future { - let request_origin = req.payload.request_origin(); - let event: Request = req.payload.into(); - let fut = Box::pin(self.service.call(event.with_lambda_context(req.context))); + let LambdaEvent { payload, context } = req; + let request_origin = payload.request_origin(); + let mut event: Request = payload.into(); + update_xray_trace_id_header(event.headers_mut(), &context); + let fut = Box::pin(self.service.call(event.with_lambda_context(context))); TransformResponse::Request(request_origin, fut) } @@ -178,17 +211,162 @@ where /// Runtime APIs](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html). /// /// This takes care of transforming the LambdaEvent into a [`Request`] and then -/// converting the result into a [`LambdaResponse`]. +/// converting the result into a `LambdaResponse`. +/// +/// # Managed concurrency +/// If `AWS_LAMBDA_MAX_CONCURRENCY` is set, a warning is logged. +/// If your handler can satisfy `Clone + Send + 'static`, +/// prefer [`run_concurrent`] (requires the `concurrency-tokio` feature), +/// which honors managed concurrency and falls back to sequential behavior when +/// unset. +/// +/// # Panics +/// +/// This function panics if required Lambda environment variables are missing +/// (`AWS_LAMBDA_FUNCTION_NAME`, `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`, +/// `AWS_LAMBDA_FUNCTION_VERSION`, `AWS_LAMBDA_RUNTIME_API`). pub async fn run<'a, R, S, E>(handler: S) -> Result<(), Error> where S: Service, S::Future: Send + 'a, R: IntoResponse, - E: std::fmt::Debug + std::fmt::Display, + E: std::fmt::Debug + Into, { lambda_runtime::run(Adapter::from(handler)).await } +/// Starts the Lambda Rust runtime and begins polling for events on the [Lambda +/// Runtime APIs](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html). +/// +/// This takes care of transforming the LambdaEvent into a [`Request`] and then +/// converting the result into a `LambdaResponse`. +/// +/// # Managed concurrency +/// +/// When `AWS_LAMBDA_MAX_CONCURRENCY` is set to a value greater than 1, this +/// function spawns multiple tokio worker tasks to handle concurrent invocations. +/// When the environment variable is unset or `<= 1`, it falls back to +/// sequential behavior, so the same handler can run on both classic Lambda +/// and Lambda Managed Instances. +/// +/// # Panics +/// +/// This function panics if: +/// - Called outside of a Tokio runtime with `AWS_LAMBDA_MAX_CONCURRENCY > 1` +/// - Required Lambda environment variables are missing (`AWS_LAMBDA_FUNCTION_NAME`, +/// `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`, `AWS_LAMBDA_FUNCTION_VERSION`, +/// `AWS_LAMBDA_RUNTIME_API`) +#[cfg(feature = "concurrency-tokio")] +#[cfg_attr(docsrs, doc(cfg(feature = "concurrency-tokio")))] +pub async fn run_concurrent(handler: S) -> Result<(), Error> +where + S: Service + Clone + Send + 'static, + S::Future: Send + 'static, + R: IntoResponse + Send + Sync + 'static, + E: std::fmt::Debug + Into + Send + 'static, +{ + lambda_runtime::run_concurrent(Adapter::from(handler)).await +} + +/// Returns a configured [`Runtime`](lambda_runtime::Runtime) wrapping the given +/// handler, without starting the event loop. +/// +/// Use this when you need to register +/// [`SnapStartResource`]s for the SnapStart +/// snapshot/restore lifecycle. For the common case where no custom hooks are +/// needed, use [`run()`] instead — SnapStart support works automatically. +/// +/// # Example +/// +/// ```no_run +/// use lambda_http::{service_fn, BoxFuture, Error, Request, SnapStartResource}; +/// use std::sync::Arc; +/// +/// struct Pool; +/// impl SnapStartResource for Pool { +/// fn before_snapshot(&self) -> BoxFuture<'_, Result<(), Error>> { +/// Box::pin(async move { Ok(()) }) +/// } +/// fn after_restore(&self) -> BoxFuture<'_, Result<(), Error>> { +/// Box::pin(async move { Ok(()) }) +/// } +/// } +/// +/// #[tokio::main] +/// async fn main() -> Result<(), Error> { +/// let pool = Arc::new(Pool); +/// let runtime = lambda_http::runtime(service_fn(handler)) +/// .register_snapstart_resource(pool.clone()); +/// +/// runtime.run().await?; +/// Ok(()) +/// } +/// +/// async fn handler(_req: Request) -> Result<&'static str, std::convert::Infallible> { +/// Ok("hello") +/// } +/// ``` +/// +/// # Panics +/// +/// This function panics if required Lambda environment variables are missing +/// (`AWS_LAMBDA_FUNCTION_NAME`, `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`, +/// `AWS_LAMBDA_FUNCTION_VERSION`, `AWS_LAMBDA_RUNTIME_API`). +pub fn runtime( + handler: S, +) -> lambda_runtime::Runtime< + impl lambda_runtime::Service, +> +where + S: Service + Send + 'static, + S::Future: Send + 'static, + R: IntoResponse + Send + Sync + 'static, + E: std::fmt::Debug + Into + Send + 'static, +{ + lambda_runtime::Runtime::new(Adapter::from(handler)) +} + +/// Returns a configured [`Runtime`](lambda_runtime::Runtime) wrapping the given +/// handler for concurrent execution, without starting the event loop. +/// +/// This is the concurrent variant of [`runtime()`]. Use it when you need SnapStart +/// hooks AND your handler supports concurrent invocations. +/// +/// # Panics +/// +/// This function panics if required Lambda environment variables are missing. +#[cfg(feature = "concurrency-tokio")] +#[cfg_attr(docsrs, doc(cfg(feature = "concurrency-tokio")))] +pub fn runtime_concurrent( + handler: S, +) -> lambda_runtime::Runtime< + impl lambda_runtime::Service< + lambda_runtime::LambdaInvocation, + Response = (), + Error = lambda_runtime::Error, + Future: Send, + > + Clone + + Send + + 'static, +> +where + S: Service + Clone + Send + 'static, + S::Future: Send + 'static, + R: IntoResponse + Send + Sync + 'static, + E: std::fmt::Debug + Into + Send + 'static, +{ + lambda_runtime::Runtime::new(Adapter::from(handler)) +} + +// In concurrent mode we must use the per-request context. +fn update_xray_trace_id_header(headers: &mut http::HeaderMap, context: &Context) { + if let Some(trace_id) = context.xray_trace_id.as_deref() { + if let Ok(header_value) = http::HeaderValue::from_str(trace_id) { + headers.insert(http::header::HeaderName::from_static("x-amzn-trace-id"), header_value); + } + } +} + #[cfg(test)] mod test_adapter { use std::task::{Context, Poll}; @@ -221,7 +399,7 @@ mod test_adapter { fn call(&mut self, event: LambdaEvent) -> Self::Future { // Log the request - println!("Lambda event: {:#?}", event); + println!("Lambda event: {event:#?}"); self.inner.call(event) } @@ -243,4 +421,35 @@ mod test_adapter { .service_fn(|_event: Request| async move { Response::builder().status(StatusCode::OK).body(Body::Empty) }) .boxed(); } + + async fn http_handler(_req: Request) -> Result<&'static str, std::convert::Infallible> { + Ok("hello") + } + + /// `runtime()` accepts a (non-`Clone`) handler for the sequential path. This + /// is a compile-time check that the helper's bounds are satisfiable. + #[test] + fn runtime_helper_accepts_handler() { + let _ = || { + let _runtime = crate::runtime(crate::service_fn(http_handler)); + }; + } + + /// `runtime_concurrent()` requires a `Clone` handler (Lambda Managed + /// Instances). `service_fn` over an `async fn` is `Clone`, so this compiles; + /// it guards against the `Clone` bound regressing on the concurrent helper. + /// + /// Crucially, this also calls `.run_concurrent()` on the result: that method + /// requires the wrapped service to be `Clone + Send + 'static` with a `Send` + /// future, so this compiling proves the helper's return type exposes those + /// bounds (without them, the chained call would not type-check). + #[cfg(feature = "concurrency-tokio")] + #[test] + fn runtime_concurrent_helper_exposes_lmi_bounds() { + let _ = || async { + let _ = crate::runtime_concurrent(crate::service_fn(http_handler)) + .run_concurrent() + .await; + }; + } } diff --git a/lambda-http/src/request.rs b/lambda-http/src/request.rs index 08a907448..e5f784769 100644 --- a/lambda-http/src/request.rs +++ b/lambda-http/src/request.rs @@ -1,37 +1,51 @@ -//! ALB and API Gateway request adaptations +//! ALB and API Gateway and VPC Lattice request adaptations //! -//! Typically these are exposed via the `request_context` -//! request extension method provided by [lambda_http::RequestExt](../trait.RequestExt.html) +//! Typically, these are exposed via the [`request_context()`] or [`request_context_ref()`] +//! request extension methods provided by the [`RequestExt`] trait. //! +//! [`request_context()`]: crate::RequestExt::request_context() +//! [`request_context_ref()`]: crate::RequestExt::request_context_ref() +//! [`RequestExt`]: crate::RequestExt #[cfg(any(feature = "apigw_rest", feature = "apigw_http", feature = "apigw_websockets"))] -use crate::ext::{PathParameters, StageVariables}; -use crate::ext::{QueryStringParameters, RawHttpPath}; +use crate::ext::extensions::{PathParameters, StageVariables}; +#[cfg(any( + feature = "apigw_rest", + feature = "apigw_http", + feature = "alb", + feature = "apigw_websockets", + feature = "vpc_lattice" +))] +use crate::ext::extensions::{QueryStringParameters, RawHttpPath}; #[cfg(feature = "alb")] use aws_lambda_events::alb::{AlbTargetGroupRequest, AlbTargetGroupRequestContext}; +#[cfg(any(feature = "apigw_rest", feature = "apigw_http", feature = "apigw_websockets"))] +use aws_lambda_events::apigw::ApiGatewayRequestAuthorizer; #[cfg(feature = "apigw_rest")] use aws_lambda_events::apigw::{ApiGatewayProxyRequest, ApiGatewayProxyRequestContext}; #[cfg(feature = "apigw_http")] use aws_lambda_events::apigw::{ApiGatewayV2httpRequest, ApiGatewayV2httpRequestContext}; #[cfg(feature = "apigw_websockets")] use aws_lambda_events::apigw::{ApiGatewayWebsocketProxyRequest, ApiGatewayWebsocketProxyRequestContext}; +#[cfg(feature = "vpc_lattice")] +use aws_lambda_events::vpc_lattice::{VpcLatticeRequestV2, VpcLatticeRequestV2Context}; + use aws_lambda_events::{encodings::Body, query_map::QueryMap}; -use http::header::HeaderName; -use http::{HeaderMap, HeaderValue}; -use serde::Deserialize; +use http::{header::HeaderName, HeaderMap, HeaderValue}; + +use serde::{Deserialize, Serialize}; use serde_json::error::Error as JsonError; -use std::future::Future; -use std::pin::Pin; -use std::{env, io::Read, mem}; + +use std::{env, future::Future, io::Read, pin::Pin}; use url::Url; /// Internal representation of an Lambda http event from -/// ALB, API Gateway REST and HTTP API proxy event perspectives +/// ALB, VPC Lattice Lambda, API Gateway REST and HTTP API proxy event perspectives /// /// This is not intended to be a type consumed by crate users directly. The order /// of the variants are notable. Serde will try to deserialize in this order. +#[non_exhaustive] #[doc(hidden)] -#[derive(Deserialize, Debug)] -#[serde(untagged)] +#[derive(Debug)] pub enum LambdaRequest { #[cfg(feature = "apigw_rest")] ApiGatewayV1(ApiGatewayProxyRequest), @@ -41,6 +55,10 @@ pub enum LambdaRequest { Alb(AlbTargetGroupRequest), #[cfg(feature = "apigw_websockets")] WebSocket(ApiGatewayWebsocketProxyRequest), + #[cfg(feature = "vpc_lattice")] + VpcLatticeV2(VpcLatticeRequestV2), + #[cfg(feature = "pass_through")] + PassThrough(String), } impl LambdaRequest { @@ -57,6 +75,18 @@ impl LambdaRequest { LambdaRequest::Alb { .. } => RequestOrigin::Alb, #[cfg(feature = "apigw_websockets")] LambdaRequest::WebSocket { .. } => RequestOrigin::WebSocket, + #[cfg(feature = "vpc_lattice")] + LambdaRequest::VpcLatticeV2 { .. } => RequestOrigin::VpcLatticeV2, + #[cfg(feature = "pass_through")] + LambdaRequest::PassThrough { .. } => RequestOrigin::PassThrough, + #[cfg(not(any( + feature = "apigw_rest", + feature = "apigw_http", + feature = "alb", + feature = "apigw_websockets", + feature = "vpc_lattice", + )))] + _ => compile_error!("Either feature `apigw_rest`, `apigw_http`, `alb`, `apigw_websockets` or `vpc_lattice` must be enabled for the `lambda-http` crate."), } } } @@ -65,6 +95,7 @@ impl LambdaRequest { pub type RequestFuture<'a, R, E> = Pin> + Send + 'a>>; /// Represents the origin from which the lambda was requested from. +#[non_exhaustive] #[doc(hidden)] #[derive(Debug, Clone)] pub enum RequestOrigin { @@ -80,6 +111,12 @@ pub enum RequestOrigin { /// API Gateway WebSocket #[cfg(feature = "apigw_websockets")] WebSocket, + /// VPC Lattice origin + #[cfg(feature = "vpc_lattice")] + VpcLatticeV2, + /// PassThrough request origin + #[cfg(feature = "pass_through")] + PassThrough, } #[cfg(feature = "apigw_http")] @@ -96,7 +133,7 @@ fn into_api_gateway_v2_request(ag: ApiGatewayV2httpRequest) -> http::Request http::Request http::Request http::Request http::Request { let http_method = ag.http_method; @@ -187,9 +218,8 @@ fn into_proxy_request(ag: ApiGatewayProxyRequest) -> http::Request { // multi-value_headers our cannoncial source of request headers let mut headers = ag.multi_value_headers; headers.extend(ag.headers); - update_xray_trace_id_header(&mut headers); - let base64 = ag.is_base64_encoded.unwrap_or_default(); + let base64 = ag.is_base64_encoded; let mut req = builder .body( ag.body @@ -199,8 +229,8 @@ fn into_proxy_request(ag: ApiGatewayProxyRequest) -> http::Request { .expect("failed to build request"); // no builder method that sets headers in batch - let _ = mem::replace(req.headers_mut(), headers); - let _ = mem::replace(req.method_mut(), http_method); + let _ = std::mem::replace(req.headers_mut(), headers); + let _ = std::mem::replace(req.method_mut(), http_method); req } @@ -238,7 +268,6 @@ fn into_alb_request(alb: AlbTargetGroupRequest) -> http::Request { // multi-value_headers our cannoncial source of request headers let mut headers = alb.multi_value_headers; headers.extend(alb.headers); - update_xray_trace_id_header(&mut headers); let base64 = alb.is_base64_encoded; @@ -251,13 +280,13 @@ fn into_alb_request(alb: AlbTargetGroupRequest) -> http::Request { .expect("failed to build request"); // no builder method that sets headers in batch - let _ = mem::replace(req.headers_mut(), headers); - let _ = mem::replace(req.method_mut(), http_method); + let _ = std::mem::replace(req.headers_mut(), headers); + let _ = std::mem::replace(req.method_mut(), http_method); req } -#[cfg(feature = "alb")] +#[cfg(any(feature = "alb", feature = "vpc_lattice"))] fn decode_query_map(query_map: QueryMap) -> QueryMap { use std::str::FromStr; @@ -274,7 +303,8 @@ fn into_websocket_request(ag: ApiGatewayWebsocketProxyRequest) -> http::Request< .get(http::header::HOST) .and_then(|s| s.to_str().ok()) .or(ag.request_context.domain_name.as_deref()); - let path = apigw_path_with_stage(&ag.request_context.stage, &ag.path.unwrap_or_default()); + let raw_path = ag.path.unwrap_or_default(); + let path = apigw_path_with_stage(&ag.request_context.stage, &raw_path); let builder = http::Request::builder() .uri(build_request_uri( @@ -283,6 +313,7 @@ fn into_websocket_request(ag: ApiGatewayWebsocketProxyRequest) -> http::Request< host, Some((&ag.multi_value_query_string_parameters, &ag.query_string_parameters)), )) + .extension(RawHttpPath(raw_path)) // multi-valued query string parameters are always a super // set of singly valued query string parameters, // when present, multi-valued query string parameters are preferred @@ -298,12 +329,11 @@ fn into_websocket_request(ag: ApiGatewayWebsocketProxyRequest) -> http::Request< .extension(RequestContext::WebSocket(ag.request_context)); // merge headers into multi_value_headers and make - // multi-value_headers our cannoncial source of request headers + // multi-value_headers our canonical source of request headers let mut headers = ag.multi_value_headers; headers.extend(ag.headers); - update_xray_trace_id_header(&mut headers); - let base64 = ag.is_base64_encoded.unwrap_or_default(); + let base64 = ag.is_base64_encoded; let mut req = builder .body( ag.body @@ -313,24 +343,90 @@ fn into_websocket_request(ag: ApiGatewayWebsocketProxyRequest) -> http::Request< .expect("failed to build request"); // no builder method that sets headers in batch - let _ = mem::replace(req.headers_mut(), headers); - let _ = mem::replace(req.method_mut(), http_method.unwrap_or(http::Method::GET)); + let _ = std::mem::replace(req.headers_mut(), headers); + let _ = std::mem::replace(req.method_mut(), http_method.unwrap_or(http::Method::GET)); + + req +} + +#[cfg(feature = "vpc_lattice")] +fn into_vpc_lattice_request(vlr: VpcLatticeRequestV2) -> http::Request { + let http_method = vlr.method; + let host = vlr.headers.get(http::header::HOST).and_then(|s| s.to_str().ok()); + let raw_path = vlr.path.unwrap_or_default(); + + let query_string_parameters = decode_query_map(vlr.query_string_parameters); + + let builder = http::Request::builder() + .uri(build_request_uri( + &raw_path, + &vlr.headers, + host, + Some((&query_string_parameters, &query_string_parameters)), + )) + .extension(RawHttpPath(raw_path)) + .extension(QueryStringParameters(query_string_parameters)) + .extension(RequestContext::VpcLattice(vlr.request_context)); + + let base64 = vlr.is_base64_encoded; + + let mut req = builder + .body( + vlr.body + .as_deref() + .map_or_else(Body::default, |b| Body::from_maybe_encoded(base64, b)), + ) + .expect("failed to build request"); + + // no builder method that sets headers in batch + let _ = std::mem::replace(req.headers_mut(), vlr.headers); + let _ = std::mem::replace(req.method_mut(), http_method.unwrap_or(http::Method::GET)); req } +#[cfg(feature = "pass_through")] +fn into_pass_through_request(data: String) -> http::Request { + let mut builder = http::Request::builder(); + + let headers = builder.headers_mut().unwrap(); + headers.insert("Content-Type", "application/json".parse().unwrap()); + + let raw_path = "/events"; + + builder + .method(http::Method::POST) + .uri(raw_path) + .extension(RawHttpPath(raw_path.to_string())) + .extension(RequestContext::PassThrough) + .body(Body::from(data)) + .expect("failed to build request") +} + #[cfg(any(feature = "apigw_rest", feature = "apigw_http", feature = "apigw_websockets"))] fn apigw_path_with_stage(stage: &Option, path: &str) -> String { - match stage { - None => path.into(), - Some(stage) if stage == "$default" => path.into(), - Some(stage) => format!("/{}{}", stage, path), + if env::var("AWS_LAMBDA_HTTP_IGNORE_STAGE_IN_PATH").is_ok() { + return path.into(); + } + + let stage = match stage { + None => return path.into(), + Some(stage) if stage == "$default" => return path.into(), + Some(stage) => stage, + }; + + let prefix = format!("/{stage}/"); + if path.starts_with(&prefix) { + path.into() + } else { + format!("/{stage}{path}") } } /// Event request context as an enumeration of request contexts /// for both ALB and API Gateway and HTTP API events -#[derive(Deserialize, Debug, Clone)] +#[non_exhaustive] +#[derive(Deserialize, Debug, Clone, Serialize)] #[serde(untagged)] pub enum RequestContext { /// API Gateway proxy request context @@ -345,6 +441,12 @@ pub enum RequestContext { /// WebSocket request context #[cfg(feature = "apigw_websockets")] WebSocket(ApiGatewayWebsocketProxyRequestContext), + /// VPC Lattice request context + #[cfg(feature = "vpc_lattice")] + VpcLattice(VpcLatticeRequestV2Context), + /// Custom request context + #[cfg(feature = "pass_through")] + PassThrough, } /// Converts LambdaRequest types into `http::Request` types @@ -359,6 +461,27 @@ impl From for http::Request { LambdaRequest::Alb(alb) => into_alb_request(alb), #[cfg(feature = "apigw_websockets")] LambdaRequest::WebSocket(ag) => into_websocket_request(ag), + #[cfg(feature = "vpc_lattice")] + LambdaRequest::VpcLatticeV2(vpclat) => into_vpc_lattice_request(vpclat), + #[cfg(feature = "pass_through")] + LambdaRequest::PassThrough(data) => into_pass_through_request(data), + } + } +} + +impl RequestContext { + /// Returns the Api Gateway Authorizer information for a request. + #[cfg(any(feature = "apigw_rest", feature = "apigw_http", feature = "apigw_websockets"))] + pub fn authorizer(&self) -> Option<&ApiGatewayRequestAuthorizer> { + match self { + #[cfg(feature = "apigw_rest")] + Self::ApiGatewayV1(ag) => Some(&ag.authorizer), + #[cfg(feature = "apigw_http")] + Self::ApiGatewayV2(ag) => ag.authorizer.as_ref(), + #[cfg(feature = "apigw_websockets")] + Self::WebSocket(ag) => Some(&ag.authorizer), + #[cfg(any(feature = "alb", feature = "pass_through", feature = "vpc_lattice"))] + _ => None, } } } @@ -418,7 +541,7 @@ fn build_request_uri( ) -> String { let mut url = match host { None => { - let rel_url = Url::parse(&format!("http://localhost{}", path)).unwrap(); + let rel_url = Url::parse(&format!("http://localhost{path}")).unwrap(); rel_url.path().to_string() } Some(host) => { @@ -426,7 +549,7 @@ fn build_request_uri( .get(x_forwarded_proto()) .and_then(|s| s.to_str().ok()) .unwrap_or("https"); - let url = format!("{}://{}{}", scheme, host, path); + let url = format!("{scheme}://{host}{path}"); Url::parse(&url).unwrap().to_string() } }; @@ -447,7 +570,7 @@ fn build_request_uri( #[cfg(test)] mod tests { use super::*; - use crate::RequestExt; + use crate::ext::RequestExt; use std::fs::File; #[test] @@ -456,7 +579,7 @@ mod tests { // https://docs.aws.amazon.com/lambda/latest/dg/eventsources.html#eventsources-api-gateway-request // note: file paths are relative to the directory of the crate at runtime let result = from_reader(File::open("tests/data/apigw_proxy_request.json").expect("expected file")); - assert!(result.is_ok(), "event was not parsed as expected {:?}", result); + assert!(result.is_ok(), "event was not parsed as expected {result:?}"); } #[test] @@ -467,20 +590,17 @@ mod tests { let result = from_str(input); assert!( result.is_ok(), - "event was not parsed as expected {:?} given {}", - result, - input + "event was not parsed as expected {result:?} given {input}" ); let req = result.expect("failed to parse request"); assert_eq!(req.method(), "GET"); assert_eq!(req.uri(), "https://xxx.execute-api.us-east-1.amazonaws.com/"); // Ensure this is an APIGWv2 request - let req_context = req.request_context(); + let req_context = req.request_context_ref().expect("Request is missing RequestContext"); assert!( - matches!(req_context, RequestContext::ApiGatewayV2(_)), - "expected ApiGatewayV2 context, got {:?}", - req_context + matches!(req_context, &RequestContext::ApiGatewayV2(_)), + "expected ApiGatewayV2 context, got {req_context:?}" ); } @@ -492,9 +612,7 @@ mod tests { let result = from_str(input); assert!( result.is_ok(), - "event was not parsed as expected {:?} given {}", - result, - input + "event was not parsed as expected {result:?} given {input}" ); let req = result.expect("failed to parse request"); let cookie_header = req @@ -505,15 +623,17 @@ mod tests { assert_eq!(req.method(), "POST"); assert_eq!(req.uri(), "https://id.execute-api.us-east-1.amazonaws.com/my/path?parameter1=value1¶meter1=value2¶meter2=value"); - assert_eq!(cookie_header, Ok("cookie1=value1;cookie2=value2")); + assert_eq!(cookie_header, Ok("cookie1=value1; cookie2=value2")); // Ensure this is an APIGWv2 request - let req_context = req.request_context(); + let req_context = req.request_context_ref().expect("Request is missing RequestContext"); assert!( - matches!(req_context, RequestContext::ApiGatewayV2(_)), - "expected ApiGatewayV2 context, got {:?}", - req_context + matches!(req_context, &RequestContext::ApiGatewayV2(_)), + "expected ApiGatewayV2 context, got {req_context:?}" ); + + let (parts, _) = req.into_parts(); + assert_eq!("https://id.execute-api.us-east-1.amazonaws.com/my/path?parameter1=value1¶meter1=value2¶meter2=value", parts.uri.to_string()); } #[test] @@ -525,23 +645,53 @@ mod tests { let result = from_str(input); assert!( result.is_ok(), - "event was not parsed as expected {:?} given {}", - result, - input + "event was not parsed as expected {result:?} given {input}" ); let req = result.expect("failed to parse request"); assert_eq!(req.method(), "GET"); assert_eq!( req.uri(), - "https://wt6mne2s9k.execute-api.us-west-2.amazonaws.com/test/test/hello?name=me" + "https://wt6mne2s9k.execute-api.us-west-2.amazonaws.com/test/hello?name=me" ); // Ensure this is an APIGW request - let req_context = req.request_context(); + let req_context = req.request_context_ref().expect("Request is missing RequestContext"); + assert!( + matches!(req_context, &RequestContext::ApiGatewayV1(_)), + "expected ApiGateway context, got {req_context:?}" + ); + } + + #[test] + fn deserializes_lambda_function_url_request_events() { + // from the docs + // https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads + let input = include_str!("../tests/data/lambda_function_url_request.json"); + let result = from_str(input); assert!( - matches!(req_context, RequestContext::ApiGatewayV1(_)), - "expected ApiGateway context, got {:?}", - req_context + result.is_ok(), + "event was not parsed as expected {result:?} given {input}" + ); + let req = result.expect("failed to parse request"); + let cookie_header = req + .headers() + .get_all(http::header::COOKIE) + .iter() + .map(|v| v.to_str().unwrap().to_string()) + .reduce(|acc, nxt| [acc, nxt].join(";")); + + assert_eq!(req.method(), "GET"); + assert_eq!( + req.uri(), + "https://id.lambda-url.eu-west-2.on.aws/my/path?parameter1=value1¶meter1=value2¶meter2=value" + ); + assert_eq!(cookie_header, Some("test=hi".to_string())); + + // Ensure this is an APIGWv2 request (Lambda Function URL requests confirm to API GW v2 Request format) + let req_context = req.request_context_ref().expect("Request is missing RequestContext"); + assert!( + matches!(req_context, &RequestContext::ApiGatewayV2(_)), + "expected ApiGatewayV2 context, got {req_context:?}" ); } @@ -553,9 +703,7 @@ mod tests { let result = from_str(input); assert!( result.is_ok(), - "event was not parsed as expected {:?} given {}", - result, - input + "event was not parsed as expected {result:?} given {input}" ); let req = result.expect("failed to parse request"); assert_eq!(req.method(), "GET"); @@ -565,11 +713,10 @@ mod tests { ); // Ensure this is an ALB request - let req_context = req.request_context(); + let req_context = req.request_context_ref().expect("Request is missing RequestContext"); assert!( - matches!(req_context, RequestContext::Alb(_)), - "expected Alb context, got {:?}", - req_context + matches!(req_context, &RequestContext::Alb(_)), + "expected Alb context, got {req_context:?}" ); } @@ -581,9 +728,7 @@ mod tests { let result = from_str(input); assert!( result.is_ok(), - "event was not parsed as expected {:?} given {}", - result, - input + "event was not parsed as expected {result:?} given {input}" ); let req = result.expect("failed to parse request"); assert_eq!(req.method(), "GET"); @@ -593,11 +738,10 @@ mod tests { ); // Ensure this is an ALB request - let req_context = req.request_context(); + let req_context = req.request_context_ref().expect("Request is missing RequestContext"); assert!( - matches!(req_context, RequestContext::Alb(_)), - "expected Alb context, got {:?}", - req_context + matches!(req_context, &RequestContext::Alb(_)), + "expected Alb context, got {req_context:?}" ); } @@ -609,19 +753,26 @@ mod tests { let result = from_str(input); assert!( result.is_ok(), - "event is was not parsed as expected {:?} given {}", - result, - input + "event is was not parsed as expected {result:?} given {input}" ); let request = result.expect("failed to parse request"); - assert!(!request.query_string_parameters().is_empty()); - - // test RequestExt#query_string_parameters does the right thing - assert_eq!( - request.query_string_parameters().all("multivalueName"), - Some(vec!["you", "me"]) - ); + assert!(!request + .query_string_parameters_ref() + .expect("Request is missing query parameters") + .is_empty()); + + // test RequestExt#query_string_parameters_ref does the right thing + let params = request.query_string_parameters(); + assert_eq!(Some(vec!["you", "me"]), params.all("multiValueName")); + assert_eq!(Some(vec!["me"]), params.all("name")); + + let query = request.uri().query().unwrap(); + assert!(query.contains("name=me")); + assert!(query.contains("multiValueName=you&multiValueName=me")); + let (parts, _) = request.into_parts(); + assert!(parts.uri.to_string().contains("name=me")); + assert!(parts.uri.to_string().contains("multiValueName=you&multiValueName=me")); } #[test] @@ -632,18 +783,22 @@ mod tests { let result = from_str(input); assert!( result.is_ok(), - "event is was not parsed as expected {:?} given {}", - result, - input + "event is was not parsed as expected {result:?} given {input}" ); let request = result.expect("failed to parse request"); - assert!(!request.query_string_parameters().is_empty()); - - // test RequestExt#query_string_parameters does the right thing - assert_eq!( - request.query_string_parameters().all("myKey"), - Some(vec!["val1", "val2"]) - ); + assert!(!request + .query_string_parameters_ref() + .expect("Request is missing query parameters") + .is_empty()); + + // test RequestExt#query_string_parameters_ref does the right thing + let params = request.query_string_parameters(); + assert_eq!(Some(vec!["val1", "val2"]), params.all("myKey")); + assert_eq!(Some(vec!["val3", "val4"]), params.all("myOtherKey")); + + let query = request.uri().query().unwrap(); + assert!(query.contains("myKey=val1&myKey=val2")); + assert!(query.contains("myOtherKey=val3&myOtherKey=val4")); } #[test] @@ -654,20 +809,210 @@ mod tests { let result = from_str(input); assert!( result.is_ok(), - "event is was not parsed as expected {:?} given {}", - result, - input + "event is was not parsed as expected {result:?} given {input}" ); let request = result.expect("failed to parse request"); - assert!(!request.query_string_parameters().is_empty()); + assert!(!request + .query_string_parameters_ref() + .expect("Request is missing query parameters") + .is_empty()); - // test RequestExt#query_string_parameters does the right thing + // test RequestExt#query_string_parameters_ref does the right thing assert_eq!( - request.query_string_parameters().all("myKey"), + request + .query_string_parameters_ref() + .and_then(|params| params.all("myKey")), Some(vec!["?showAll=true", "?showAll=false"]) ); } + #[test] + #[cfg(feature = "vpc_lattice")] + fn deserializes_vpc_lattice_basic() { + let input = include_str!("../tests/data/vpc_lattice_v2_request.json"); + let result = from_str(input); + assert!( + result.is_ok(), + "event is was not parsed as expected {result:?} given {input}" + ); + let request = result.expect("failed to parse request"); + assert_eq!(request.method(), "GET"); + + let body_str = match request.body() { + Body::Text(s) => s.as_str(), + _ => "", + }; + + assert_eq!(body_str, "All is good"); + + let uri = request.uri().to_string(); + assert!(uri.starts_with("/health?"), "unexpected uri: {uri}"); + assert!(uri.contains("multi=a"), "unexpected uri: {uri}"); + assert!(uri.contains("multi=DEF"), "unexpected uri: {uri}"); + assert!(uri.contains("multi=g"), "unexpected uri: {uri}"); + assert!(uri.contains("state=prod"), "unexpected uri: {uri}"); + + // Ensure this is an VPC Lattice request + let req_context = request + .request_context_ref() + .expect("Request is missing RequestContext"); + assert!( + matches!(req_context, &RequestContext::VpcLattice(_)), + "expected Vpc lattice context, got {req_context:?}" + ); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn deserializes_vpc_lattice_basic_base64() { + let input = include_str!("../tests/data/vpc_lattice_v2_request_base64.json"); + let result = from_str(input); + assert!( + result.is_ok(), + "event is was not parsed as expected {result:?} given {input}" + ); + let request = result.expect("failed to parse request"); + assert_eq!(request.method(), "GET"); + + let body_array = match request.body() { + Body::Binary(s) => s.as_slice(), + _ => &[], + }; + + assert_eq!(body_array, *b"All is good"); + + // URI should have been built from host header, query and protocol etc + let uri = request.uri(); + + assert!(uri.to_string().starts_with("https://www.site.com/health?")); + assert!(uri.to_string().contains("multi=a&multi=DEF&multi=g")); + assert!(uri.to_string().contains("state=prod")); + + // Ensure this is an VPC Lattice request + let req_context = request + .request_context_ref() + .expect("Request is missing RequestContext"); + assert!( + matches!(req_context, &RequestContext::VpcLattice(_)), + "expected Vpc lattice context, got {req_context:?}" + ); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn deserializes_vpc_lattice_headers() { + let input = include_str!("../tests/data/vpc_lattice_v2_request.json"); + let result = from_str(input); + assert!( + result.is_ok(), + "event is was not parsed as expected {result:?} given {input}" + ); + let request = result.expect("failed to parse request"); + + // decoding multi value headers + let multi_headers_as_big_string = request + .headers() + .get_all("multi") + .iter() + .map(|v| v.to_str().unwrap().to_string()) + .reduce(|acc, nxt| [acc, nxt].join(";")); + + assert_eq!(multi_headers_as_big_string, Some("x;y".to_string())); + + // decoding regular headers + let basic_headers_as_big_string = request + .headers() + .get_all("user-agent") + .iter() + .map(|v| v.to_str().unwrap().to_string()) + .reduce(|acc, nxt| [acc, nxt].join(";")); + + assert_eq!(basic_headers_as_big_string, Some("curl/7.68.0".to_string())); + + // Ensure this is an VPC Lattice request + let req_context = request + .request_context_ref() + .expect("Request is missing RequestContext"); + assert!( + matches!(req_context, &RequestContext::VpcLattice(_)), + "expected Vpc lattice context, got {req_context:?}" + ); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn deserializes_vpc_lattice_multi_value_querys() { + let input = include_str!("../tests/data/vpc_lattice_v2_request.json"); + let result = from_str(input); + assert!( + result.is_ok(), + "event is was not parsed as expected {result:?} given {input}" + ); + let request = result.expect("failed to parse request"); + assert!(!request + .query_string_parameters_ref() + .expect("Request is missing query parameters") + .is_empty()); + + let params = request.query_string_parameters(); + assert_eq!(Some(vec!["prod"]), params.all("state")); + assert_eq!(Some(vec!["a", "DEF", "g"]), params.all("multi")); + + let query = request.uri().query().unwrap(); + assert!(query.contains("multi=a&multi=DEF&multi=g")); + assert!(query.contains("state=prod")); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn deserializes_vpc_lattice_encoded_query_parameters() { + let input = include_str!("../tests/data/vpc_lattice_v2_request_encoded_query.json"); + let result = from_str(input); + assert!( + result.is_ok(), + "event is was not parsed as expected {result:?} given {input}" + ); + let request = result.expect("failed to parse request"); + + let params = request.query_string_parameters(); + // percent-encoded values should be decoded + assert_eq!(Some(vec!["?showAll=true"]), params.all("filter")); + assert_eq!(Some(vec!["hello world"]), params.all("q")); + + let query = request.uri().query().unwrap(); + assert!(query.contains("filter="), "unexpected uri query: {query}"); + assert!(query.contains("q="), "unexpected uri query: {query}"); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn deserializes_vpc_lattice_no_body() { + let input = r#"{ + "version": "2.0", + "path": "/ping", + "method": "GET", + "headers": {"accept": ["*/*"]}, + "queryStringParameters": {}, + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:us-east-1:123456789012:servicenetwork/sn-abc", + "serviceArn": "arn:aws:vpc-lattice:us-east-1:123456789012:service/svc-abc", + "targetGroupArn": "arn:aws:vpc-lattice:us-east-1:123456789012:targetgroup/tg-abc", + "region": "us-east-1", + "timeEpoch": "1724875399456789" + } + }"#; + let result = from_str(input); + assert!(result.is_ok(), "event was not parsed as expected {result:?}"); + let request = result.expect("failed to parse request"); + assert_eq!(request.method(), "GET"); + assert!( + matches!(request.body(), Body::Empty), + "expected empty body, got {:?}", + request.body() + ); + } + #[test] fn deserialize_apigw_http_sam_local() { // manually generated from AWS SAM CLI @@ -682,9 +1027,7 @@ mod tests { let result = from_str(input); assert!( result.is_ok(), - "event was not parsed as expected {:?} given {}", - result, - input + "event was not parsed as expected {result:?} given {input}" ); let req = result.expect("failed to parse request"); assert_eq!(req.method(), "GET"); @@ -698,13 +1041,11 @@ mod tests { let result = from_str(input); assert!( result.is_ok(), - "event was not parsed as expected {:?} given {}", - result, - input + "event was not parsed as expected {result:?} given {input}" ); let req = result.expect("failed to parse request"); assert_eq!(req.method(), "GET"); - assert_eq!(req.uri(), "/test/test/hello?name=me"); + assert_eq!(req.uri(), "/test/hello?name=me"); } #[test] @@ -714,9 +1055,7 @@ mod tests { let result = from_str(input); assert!( result.is_ok(), - "event was not parsed as expected {:?} given {}", - result, - input + "event was not parsed as expected {result:?} given {input}" ); let req = result.expect("failed to parse request"); assert_eq!(req.method(), "GET"); @@ -730,9 +1069,7 @@ mod tests { let result = from_str(input); assert!( result.is_ok(), - "event was not parsed as expected {:?} given {}", - result, - input + "event was not parsed as expected {result:?} given {input}" ); let req = result.expect("failed to parse request"); assert_eq!(req.uri(), "https://id.execute-api.us-east-1.amazonaws.com/my/path-with%20space?parameter1=value1¶meter1=value2¶meter2=value"); @@ -743,4 +1080,169 @@ mod tests { let url = build_request_uri("/path with spaces/and multiple segments", &HeaderMap::new(), None, None); assert_eq!("/path%20with%20spaces/and%20multiple%20segments", url); } + + #[test] + fn deserializes_apigw_http_request_with_stage_in_path() { + let input = include_str!("../tests/data/apigw_v2_proxy_request_with_stage_in_path.json"); + let result = from_str(input); + assert!( + result.is_ok(), + "event was not parsed as expected {result:?} given {input}" + ); + let req = result.expect("failed to parse request"); + assert_eq!("/Prod/my/path", req.uri().path()); + assert_eq!("/Prod/my/path", req.raw_http_path()); + } + + #[test] + fn test_apigw_path_with_stage() { + assert_eq!("/path", apigw_path_with_stage(&None, "/path")); + assert_eq!("/path", apigw_path_with_stage(&Some("$default".into()), "/path")); + assert_eq!("/Prod/path", apigw_path_with_stage(&Some("Prod".into()), "/Prod/path")); + assert_eq!("/Prod/path", apigw_path_with_stage(&Some("Prod".into()), "/path")); + } + + #[tokio::test] + #[cfg(feature = "apigw_rest")] + async fn test_axum_query_extractor_apigw_rest() { + use axum_core::extract::FromRequestParts; + use axum_extra::extract::Query; + // from docs + // https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format + let input = include_str!("../tests/data/apigw_multi_value_proxy_request.json"); + let request = from_str(input).expect("failed to parse request"); + let (mut parts, _) = request.into_parts(); + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct Params { + name: Vec, + multi_value_name: Vec, + } + struct State; + + let query = Query::::from_request_parts(&mut parts, &State).await.unwrap(); + assert_eq!(vec!["me"], query.0.name); + assert_eq!(vec!["you", "me"], query.0.multi_value_name); + } + + #[tokio::test] + #[cfg(feature = "apigw_http")] + async fn test_axum_query_extractor_apigw_http() { + use axum_core::extract::FromRequestParts; + use axum_extra::extract::Query; + let input = include_str!("../tests/data/apigw_v2_proxy_request.json"); + let request = from_str(input).expect("failed to parse request"); + let (mut parts, _) = request.into_parts(); + + #[derive(Deserialize)] + struct Params { + parameter1: Vec, + parameter2: Vec, + } + struct State; + + let query = Query::::from_request_parts(&mut parts, &State).await.unwrap(); + assert_eq!(vec!["value1", "value2"], query.0.parameter1); + assert_eq!(vec!["value"], query.0.parameter2); + } + + #[tokio::test] + #[cfg(feature = "alb")] + async fn test_axum_query_extractor_alb() { + use axum_core::extract::FromRequestParts; + use axum_extra::extract::Query; + let input = include_str!("../tests/data/alb_multi_value_request.json"); + let request = from_str(input).expect("failed to parse request"); + let (mut parts, _) = request.into_parts(); + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct Params { + my_key: Vec, + my_other_key: Vec, + } + struct State; + + let query = Query::::from_request_parts(&mut parts, &State).await.unwrap(); + assert_eq!(vec!["val1", "val2"], query.0.my_key); + assert_eq!(vec!["val3", "val4"], query.0.my_other_key); + } + + #[test] + #[cfg(feature = "apigw_rest")] + fn deserializes_request_authorizer() { + let input = include_str!("../../lambda-events/src/fixtures/example-apigw-request.json"); + let result = from_str(input); + assert!( + result.is_ok(), + "event was not parsed as expected {result:?} given {input}" + ); + let req = result.expect("failed to parse request"); + + let req_context = req.request_context_ref().expect("Request is missing RequestContext"); + let authorizer = req_context.authorizer().expect("authorizer is missing"); + assert_eq!(Some("admin"), authorizer.fields.get("principalId").unwrap().as_str()); + } + + #[test] + #[cfg(all(feature = "apigw_http", feature = "vpc_lattice"))] + fn vpc_lattice_event_does_not_match_apigw_v2() { + let data = include_bytes!("../../lambda-events/src/fixtures/example-vpc-lattice-v2-request.json"); + let result = serde_json::from_slice::(data); + assert!(result.is_err(), "VPC Lattice event should not deserialize as APIGW V2"); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn vpc_lattice_method_none_defaults_to_get() { + let input = r#"{ + "version": "2.0", + "path": "/ping", + "headers": {"accept": ["*/*"]}, + "queryStringParameters": {}, + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:us-east-1:123456789012:servicenetwork/sn-abc", + "serviceArn": "arn:aws:vpc-lattice:us-east-1:123456789012:service/svc-abc", + "targetGroupArn": "arn:aws:vpc-lattice:us-east-1:123456789012:targetgroup/tg-abc", + "region": "us-east-1", + "timeEpoch": "1724875399456789" + } + }"#; + let result = from_str(input); + assert!(result.is_ok(), "event was not parsed as expected {result:?}"); + let request = result.expect("failed to parse request"); + assert_eq!(request.method(), "GET"); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn vpc_lattice_post_with_body() { + let input = r#"{ + "version": "2.0", + "path": "/submit", + "method": "POST", + "headers": {"content-type": ["application/json"]}, + "queryStringParameters": {}, + "body": "{\"key\":\"value\"}", + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:us-east-1:123456789012:servicenetwork/sn-abc", + "serviceArn": "arn:aws:vpc-lattice:us-east-1:123456789012:service/svc-abc", + "targetGroupArn": "arn:aws:vpc-lattice:us-east-1:123456789012:targetgroup/tg-abc", + "region": "us-east-1", + "timeEpoch": "1724875399456789" + } + }"#; + let result = from_str(input); + assert!(result.is_ok(), "event was not parsed as expected {result:?}"); + let request = result.expect("failed to parse request"); + assert_eq!(request.method(), "POST"); + let body_str = match request.body() { + Body::Text(s) => s.as_str(), + other => panic!("expected text body, got {other:?}"), + }; + assert_eq!(body_str, r#"{"key":"value"}"#); + } } diff --git a/lambda-http/src/response.rs b/lambda-http/src/response.rs index b174f98ed..da3873225 100644 --- a/lambda-http/src/response.rs +++ b/lambda-http/src/response.rs @@ -8,17 +8,23 @@ use aws_lambda_events::apigw::ApiGatewayProxyResponse; #[cfg(feature = "apigw_http")] use aws_lambda_events::apigw::ApiGatewayV2httpResponse; use aws_lambda_events::encodings::Body; +#[cfg(feature = "vpc_lattice")] +use aws_lambda_events::vpc_lattice::VpcLatticeResponse; use encoding_rs::Encoding; -use http::header::CONTENT_ENCODING; -use http::HeaderMap; -use http::{header::CONTENT_TYPE, Response}; +use http::{ + header::{CONTENT_ENCODING, CONTENT_TYPE}, + HeaderMap, Response, StatusCode, +}; use http_body::Body as HttpBody; -use hyper::body::to_bytes; +use http_body_util::BodyExt; use mime::{Mime, CHARSET}; use serde::Serialize; -use std::borrow::Cow; -use std::future::ready; -use std::{fmt, future::Future, pin::Pin}; +use std::{ + borrow::Cow, + fmt, + future::{ready, Future}, + pin::Pin, +}; const X_LAMBDA_HTTP_CONTENT_ENCODING: &str = "x-lambda-http-content-encoding"; @@ -36,6 +42,7 @@ const TEXT_ENCODING_PREFIXES: [&str; 5] = [ const TEXT_ENCODING_SUFFIXES: [&str; 3] = ["+xml", "+yaml", "+json"]; /// Representation of Lambda response +#[non_exhaustive] #[doc(hidden)] #[derive(Serialize, Debug)] #[serde(untagged)] @@ -46,6 +53,10 @@ pub enum LambdaResponse { ApiGatewayV2(ApiGatewayV2httpResponse), #[cfg(feature = "alb")] Alb(AlbTargetGroupResponse), + #[cfg(feature = "vpc_lattice")] + VpcLattice(VpcLatticeResponse), + #[cfg(feature = "pass_through")] + PassThrough(serde_json::Value), } /// Transformation from http type to internal type @@ -56,6 +67,7 @@ impl LambdaResponse { Body::Empty => (false, None), b @ Body::Text(_) => (false, Some(b)), b @ Body::Binary(_) => (true, Some(b)), + _ => (false, None), }; let headers = parts.headers; @@ -63,12 +75,22 @@ impl LambdaResponse { match request_origin { #[cfg(feature = "apigw_rest")] - RequestOrigin::ApiGatewayV1 => LambdaResponse::ApiGatewayV1(ApiGatewayProxyResponse { - body, - status_code: status_code as i64, - is_base64_encoded: Some(is_base64_encoded), - headers: headers.clone(), - multi_value_headers: headers, + RequestOrigin::ApiGatewayV1 => LambdaResponse::ApiGatewayV1({ + let mut response = ApiGatewayProxyResponse::default(); + + response.body = body; + response.is_base64_encoded = is_base64_encoded; + response.status_code = status_code as i64; + // Explicitly empty, as API gateway v1 will merge "headers" and + // "multi_value_headers" fields together resulting in duplicate response headers. + response.headers = HeaderMap::new(); + response.multi_value_headers = headers; + // Today, this implementation doesn't provide any additional fields + #[cfg(feature = "catch-all-fields")] + { + response.other = Default::default(); + } + response }), #[cfg(feature = "apigw_http")] RequestOrigin::ApiGatewayV2 => { @@ -79,41 +101,101 @@ impl LambdaResponse { let cookies = headers .get_all(SET_COOKIE) .iter() - .cloned() .map(|v| v.to_str().ok().unwrap_or_default().to_string()) .collect(); headers.remove(SET_COOKIE); - LambdaResponse::ApiGatewayV2(ApiGatewayV2httpResponse { - body, - status_code: status_code as i64, - is_base64_encoded: Some(is_base64_encoded), - cookies, - headers: headers.clone(), - multi_value_headers: headers, + LambdaResponse::ApiGatewayV2({ + let mut response = ApiGatewayV2httpResponse::default(); + response.body = body; + response.is_base64_encoded = is_base64_encoded; + response.status_code = status_code as i64; + response.cookies = cookies; + // API gateway v2 doesn't have "multi_value_headers" field. Duplicate headers + // are combined with commas and included in the headers field. + response.headers = headers; + response.multi_value_headers = HeaderMap::new(); + // Today, this implementation doesn't provide any additional fields + #[cfg(feature = "catch-all-fields")] + { + response.other = Default::default(); + } + response }) } #[cfg(feature = "alb")] - RequestOrigin::Alb => LambdaResponse::Alb(AlbTargetGroupResponse { - body, - status_code: status_code as i64, - is_base64_encoded, - headers: headers.clone(), - multi_value_headers: headers, - status_description: Some(format!( + RequestOrigin::Alb => LambdaResponse::Alb({ + let mut response = AlbTargetGroupResponse::default(); + + response.body = body; + response.is_base64_encoded = is_base64_encoded; + response.status_code = status_code as i64; + // ALB responses are used for ALB integration, which can be configured to use + // either "headers" or "multi_value_headers" field. We need to return both fields + // to ensure both configuration work correctly. + response.headers = headers.clone(); + response.multi_value_headers = headers; + response.status_description = Some(format!( "{} {}", status_code, parts.status.canonical_reason().unwrap_or_default() - )), + )); + // Today, this implementation doesn't provide any additional fields + #[cfg(feature = "catch-all-fields")] + { + response.other = Default::default(); + } + response }), #[cfg(feature = "apigw_websockets")] - RequestOrigin::WebSocket => LambdaResponse::ApiGatewayV1(ApiGatewayProxyResponse { - body, - status_code: status_code as i64, - is_base64_encoded: Some(is_base64_encoded), - headers: headers.clone(), - multi_value_headers: headers, + RequestOrigin::WebSocket => LambdaResponse::ApiGatewayV1({ + let mut response = ApiGatewayProxyResponse::default(); + response.body = body; + response.is_base64_encoded = is_base64_encoded; + response.status_code = status_code as i64; + // Explicitly empty, as API gateway v1 will merge "headers" and + // "multi_value_headers" fields together resulting in duplicate response headers. + response.headers = HeaderMap::new(); + response.multi_value_headers = headers; + // Today, this implementation doesn't provide any additional fields + #[cfg(feature = "catch-all-fields")] + { + response.other = Default::default(); + } + response + }), + #[cfg(feature = "vpc_lattice")] + RequestOrigin::VpcLatticeV2 => LambdaResponse::VpcLattice({ + let mut response = VpcLatticeResponse::default(); + + response.body = body; + response.is_base64_encoded = is_base64_encoded; + response.status_code = status_code; + response.headers = headers; + // Today, this implementation doesn't provide any additional fields + #[cfg(feature = "catch-all-fields")] + { + response.other = Default::default(); + } + response }), + #[cfg(feature = "pass_through")] + RequestOrigin::PassThrough => { + match body { + // text body must be a valid json string + Some(Body::Text(body)) => {LambdaResponse::PassThrough(serde_json::from_str(&body).unwrap_or_default())}, + // binary body and other cases return Value::Null + _ => LambdaResponse::PassThrough(serde_json::Value::Null), + } + } + #[cfg(not(any( + feature = "apigw_rest", + feature = "apigw_http", + feature = "alb", + feature = "apigw_websockets", + feature = "vpc_lattice" + )))] + _ => compile_error!("Either feature `apigw_rest`, `apigw_http`, `alb`, `apigw_websockets` or `vpc_lattice` must be enabled for the `lambda-http` crate."), } } } @@ -122,7 +204,7 @@ impl LambdaResponse { /// /// Types that implement this trait can be used as return types for handler functions. pub trait IntoResponse { - /// Transform into a Response Future + /// Transform into a `Response` Future fn into_response(self) -> ResponseFuture; } @@ -179,6 +261,71 @@ impl IntoResponse for serde_json::Value { } } +impl IntoResponse for (StatusCode, String) { + fn into_response(self) -> ResponseFuture { + let (status, body) = self; + Box::pin(ready( + Response::builder() + .status(status) + .body(Body::from(body)) + .expect("unable to build http::Response"), + )) + } +} + +impl IntoResponse for (StatusCode, &str) { + fn into_response(self) -> ResponseFuture { + let (status, body) = self; + Box::pin(ready( + Response::builder() + .status(status) + .body(Body::from(body)) + .expect("unable to build http::Response"), + )) + } +} + +impl IntoResponse for (StatusCode, &[u8]) { + fn into_response(self) -> ResponseFuture { + let (status, body) = self; + Box::pin(ready( + Response::builder() + .status(status) + .body(Body::from(body)) + .expect("unable to build http::Response"), + )) + } +} + +impl IntoResponse for (StatusCode, Vec) { + fn into_response(self) -> ResponseFuture { + let (status, body) = self; + Box::pin(ready( + Response::builder() + .status(status) + .body(Body::from(body)) + .expect("unable to build http::Response"), + )) + } +} + +impl IntoResponse for (StatusCode, serde_json::Value) { + fn into_response(self) -> ResponseFuture { + let (status, body) = self; + Box::pin(async move { + Response::builder() + .status(status) + .header(CONTENT_TYPE, "application/json") + .body( + serde_json::to_string(&body) + .expect("unable to serialize serde_json::Value") + .into(), + ) + .expect("unable to build http::Response") + }) + } +} + pub type ResponseFuture = Pin> + Send>>; pub trait ConvertBody { @@ -210,7 +357,9 @@ where } for suffix in TEXT_ENCODING_SUFFIXES { - if content_type.ends_with(suffix) { + let mut parts = content_type.trim().split(';'); + let mime_type = parts.next().unwrap_or_default(); + if mime_type.ends_with(suffix) { return convert_to_text(self, content_type); } } @@ -231,7 +380,15 @@ where B::Data: Send, B::Error: fmt::Debug, { - Box::pin(async move { Body::from(to_bytes(body).await.expect("unable to read bytes from body").to_vec()) }) + Box::pin(async move { + Body::from( + body.collect() + .await + .expect("unable to read bytes from body") + .to_bytes() + .to_vec(), + ) + }) } fn convert_to_text(body: B, content_type: &str) -> BodyFuture @@ -252,7 +409,7 @@ where // assumes utf-8 Box::pin(async move { - let bytes = to_bytes(body).await.expect("unable to read bytes from body"); + let bytes = body.collect().await.expect("unable to read bytes from body").to_bytes(); let (content, _, _) = encoding.decode(&bytes); match content { @@ -269,9 +426,9 @@ mod tests { use super::{Body, IntoResponse, LambdaResponse, RequestOrigin, X_LAMBDA_HTTP_CONTENT_ENCODING}; use http::{ header::{CONTENT_ENCODING, CONTENT_TYPE}, - Response, + Response, StatusCode, }; - use hyper::Body as HyperBody; + use lambda_runtime_api_client::body::Body as HyperBody; use serde_json::{self, json}; const SVG_LOGO: &str = include_str!("../tests/data/svg_logo.svg"); @@ -310,6 +467,54 @@ mod tests { } } + #[tokio::test] + async fn json_with_status_code_into_response() { + let response = (StatusCode::CREATED, json!({ "hello": "lambda"})).into_response().await; + match response.body() { + Body::Text(json) => assert_eq!(json, r#"{"hello":"lambda"}"#), + _ => panic!("invalid body"), + } + match response.status() { + StatusCode::CREATED => (), + _ => panic!("invalid status code"), + } + + assert_eq!( + response + .headers() + .get(CONTENT_TYPE) + .map(|h| h.to_str().expect("invalid header")), + Some("application/json") + ) + } + + #[tokio::test] + async fn text_with_status_code_into_response() { + let response = (StatusCode::CREATED, "text").into_response().await; + + match response.status() { + StatusCode::CREATED => (), + _ => panic!("invalid status code"), + } + match response.body() { + Body::Text(text) => assert_eq!(text, "text"), + _ => panic!("invalid body"), + } + } + + #[tokio::test] + async fn bytes_with_status_code_into_response() { + let response = (StatusCode::CREATED, "text".as_bytes()).into_response().await; + match response.status() { + StatusCode::CREATED => (), + _ => panic!("invalid status code"), + } + match response.body() { + Body::Binary(data) => assert_eq!(data, "text".as_bytes()), + _ => panic!("invalid body"), + } + } + #[tokio::test] async fn content_encoding_header() { // Drive the implementation by using `hyper::Body` instead of @@ -324,7 +529,7 @@ mod tests { let json = serde_json::to_string(&response).expect("failed to serialize to json"); assert_eq!( json, - r#"{"statusCode":200,"headers":{"content-encoding":"gzip"},"multiValueHeaders":{"content-encoding":["gzip"]},"body":"MDAwMDAw","isBase64Encoded":true,"cookies":[]}"# + r#"{"statusCode":200,"headers":{"content-encoding":"gzip"},"multiValueHeaders":{},"body":"MDAwMDAw","isBase64Encoded":true,"cookies":[]}"# ) } @@ -342,7 +547,7 @@ mod tests { let json = serde_json::to_string(&response).expect("failed to serialize to json"); assert_eq!( json, - r#"{"statusCode":200,"headers":{"content-type":"application/json"},"multiValueHeaders":{"content-type":["application/json"]},"body":"000000","isBase64Encoded":false,"cookies":[]}"# + r#"{"statusCode":200,"headers":{"content-type":"application/json"},"multiValueHeaders":{},"body":"000000","isBase64Encoded":false,"cookies":[]}"# ) } @@ -360,7 +565,25 @@ mod tests { let json = serde_json::to_string(&response).expect("failed to serialize to json"); assert_eq!( json, - r#"{"statusCode":200,"headers":{"content-type":"application/json; charset=utf-16"},"multiValueHeaders":{"content-type":["application/json; charset=utf-16"]},"body":"〰〰〰","isBase64Encoded":false,"cookies":[]}"# + r#"{"statusCode":200,"headers":{"content-type":"application/json; charset=utf-16"},"multiValueHeaders":{},"body":"〰〰〰","isBase64Encoded":false,"cookies":[]}"# + ) + } + + #[tokio::test] + async fn charset_content_type_header_suffix() { + // Drive the implementation by using `hyper::Body` instead of + // of `aws_lambda_events::encodings::Body` + let response = Response::builder() + .header(CONTENT_TYPE, "application/graphql-response+json; charset=utf-16") + .body(HyperBody::from("000000".as_bytes())) + .expect("unable to build http::Response"); + let response = response.into_response().await; + let response = LambdaResponse::from_response(&RequestOrigin::ApiGatewayV2, response); + + let json = serde_json::to_string(&response).expect("failed to serialize to json"); + assert_eq!( + json, + r#"{"statusCode":200,"headers":{"content-type":"application/graphql-response+json; charset=utf-16"},"multiValueHeaders":{},"body":"〰〰〰","isBase64Encoded":false,"cookies":[]}"# ) } @@ -381,6 +604,56 @@ mod tests { ) } + #[test] + #[cfg(feature = "vpc_lattice")] + fn serialize_vpc_lattice_response_text_body() { + let res = LambdaResponse::from_response( + &RequestOrigin::VpcLatticeV2, + Response::builder() + .status(200) + .header("content-type", "text/plain") + .body(Body::from("hello")) + .expect("failed to create response"), + ); + let json = serde_json::to_string(&res).expect("failed to serialize to json"); + assert_eq!( + json, + r#"{"isBase64Encoded":false,"statusCode":200,"headers":{"content-type":"text/plain"},"body":"hello"}"# + ); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn serialize_vpc_lattice_response_binary_body() { + let res = LambdaResponse::from_response( + &RequestOrigin::VpcLatticeV2, + Response::builder() + .status(200) + .body(Body::from(b"hello".as_slice())) + .expect("failed to create response"), + ); + let json = serde_json::to_string(&res).expect("failed to serialize to json"); + assert_eq!(json, r#"{"isBase64Encoded":true,"statusCode":200,"body":"aGVsbG8="}"#); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn serialize_vpc_lattice_response_non_200_status() { + let res = LambdaResponse::from_response( + &RequestOrigin::VpcLatticeV2, + Response::builder() + .status(404) + .header("x-request-id", "abc-123") + .body(Body::from(())) + .expect("failed to create response"), + ); + let json = serde_json::to_string(&res).expect("failed to serialize to json"); + assert_eq!( + json, + r#"{"isBase64Encoded":false,"statusCode":404,"headers":{"x-request-id":"abc-123"},"body":null}"# + ); + } + #[test] fn serialize_multi_value_headers() { let res = LambdaResponse::from_response( @@ -394,7 +667,7 @@ mod tests { let json = serde_json::to_string(&res).expect("failed to serialize to json"); assert_eq!( json, - r#"{"statusCode":200,"headers":{"multi":"a"},"multiValueHeaders":{"multi":["a","b"]},"isBase64Encoded":false}"# + r#"{"statusCode":200,"headers":{},"multiValueHeaders":{"multi":["a","b"]},"isBase64Encoded":false}"# ) } diff --git a/lambda-http/src/streaming.rs b/lambda-http/src/streaming.rs new file mode 100644 index 000000000..361a925e1 --- /dev/null +++ b/lambda-http/src/streaming.rs @@ -0,0 +1,361 @@ +use crate::{http::header::SET_COOKIE, request::LambdaRequest, update_xray_trace_id_header, Request, RequestExt}; +use bytes::Bytes; +use core::{ + fmt::Debug, + pin::Pin, + task::{Context, Poll}, +}; +use futures_util::{Stream, TryFutureExt}; +pub use http::{self, Response}; +use http_body::Body; +use lambda_runtime::{ + tower::{ + util::{MapRequest, MapResponse}, + ServiceBuilder, ServiceExt, + }, + Diagnostic, +}; +pub use lambda_runtime::{Error, LambdaEvent, MetadataPrelude, Service, StreamResponse}; +use std::{future::Future, marker::PhantomData}; + +/// An adapter that lifts a standard [`Service`] into a +/// [`Service>`] which produces streaming Lambda HTTP +/// responses. +#[non_exhaustive] +pub struct StreamAdapter<'a, S, B> { + service: S, + _phantom_data: PhantomData<&'a B>, +} + +impl<'a, S, B> Clone for StreamAdapter<'a, S, B> +where + S: Clone, +{ + fn clone(&self) -> Self { + Self { + service: self.service.clone(), + _phantom_data: PhantomData, + } + } +} + +impl<'a, S, B, E> From for StreamAdapter<'a, S, B> +where + S: Service, Error = E>, + S::Future: Send + 'a, + B: Body + Unpin + Send + 'static, + B::Data: Into + Send, + B::Error: Into + Send + Debug, +{ + fn from(service: S) -> Self { + StreamAdapter { + service, + _phantom_data: PhantomData, + } + } +} + +impl<'a, S, B, E> Service> for StreamAdapter<'a, S, B> +where + S: Service, Error = E>, + S::Future: Send + 'a, + B: Body + Unpin + Send + 'static, + B::Data: Into + Send, + B::Error: Into + Send + Debug, +{ + type Response = StreamResponse>; + type Error = E; + type Future = Pin> + Send + 'a>>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.service.poll_ready(cx) + } + + fn call(&mut self, req: LambdaEvent) -> Self::Future { + let LambdaEvent { payload, context } = req; + let mut event: Request = payload.into(); + update_xray_trace_id_header(event.headers_mut(), &context); + Box::pin( + self.service + .call(event.with_lambda_context(context)) + .map_ok(into_stream_response), + ) + } +} + +/// Builds a streaming-aware Tower service from a `Service` **without** +/// boxing its future (no heap allocation / vtable). +/// +/// Transforms `LambdaEvent` into `Request` with Lambda context +/// and wraps `Response` into `StreamResponse>`. +/// +/// Used internally by [`run_with_streaming_response`]; not part of the public +/// API. +#[allow(clippy::type_complexity)] +fn into_stream_service<'a, S, B, E>( + handler: S, +) -> MapResponse< + MapRequest) -> Request>, + impl FnOnce(Response) -> StreamResponse> + Clone, +> +where + S: Service, Error = E>, + S::Future: Send + 'a, + E: Debug + Into, + B: Body + Unpin + Send + 'static, + B::Data: Into + Send, + B::Error: Into + Send + Debug, +{ + ServiceBuilder::new() + .map_request(event_to_request as fn(LambdaEvent) -> Request) + .service(handler) + .map_response(into_stream_response) +} + +/// Builds a streaming-aware Tower service from a `Service` that can be +/// cloned and sent across tasks. This is used by the concurrent HTTP entrypoint. +#[cfg(feature = "concurrency-tokio")] +type EventToRequest = fn(LambdaEvent) -> Request; + +#[cfg(feature = "concurrency-tokio")] +#[allow(clippy::type_complexity)] +fn into_stream_service_cloneable( + handler: S, +) -> MapResponse, impl FnOnce(Response) -> StreamResponse> + Clone> +where + S: Service, Error = E> + Clone + Send + 'static, + S::Future: Send + 'static, + E: Debug + Into + Send + 'static, + B: Body + Unpin + Send + 'static, + B::Data: Into + Send, + B::Error: Into + Send + Debug, +{ + ServiceBuilder::new() + .map_request(event_to_request as EventToRequest) + .service(handler) + .map_response(into_stream_response) +} + +/// Converts an `http::Response` into a streaming Lambda response. +fn into_stream_response(res: Response) -> StreamResponse> +where + B: Body + Unpin + Send + 'static, + B::Data: Into + Send, + B::Error: Into + Send + Debug, +{ + let (parts, body) = res.into_parts(); + + let mut headers = parts.headers; + let cookies = headers + .get_all(SET_COOKIE) + .iter() + .map(|c| String::from_utf8_lossy(c.as_bytes()).to_string()) + .collect::>(); + headers.remove(SET_COOKIE); + + StreamResponse { + metadata_prelude: MetadataPrelude { + headers, + status_code: parts.status, + cookies, + }, + stream: BodyStream { body }, + } +} + +fn event_to_request(req: LambdaEvent) -> Request { + let LambdaEvent { payload, context } = req; + let mut event: Request = payload.into(); + update_xray_trace_id_header(event.headers_mut(), &context); + event.with_lambda_context(context) +} + +/// Runs the Lambda runtime with a handler that returns **streaming** HTTP +/// responses. +/// +/// See the [AWS docs for response streaming]. +/// +/// # Managed concurrency +/// If `AWS_LAMBDA_MAX_CONCURRENCY` is set, this function returns an error because +/// it does not enable concurrent polling. Use [`run_with_streaming_response_concurrent`] +/// (requires the `concurrency-tokio` feature) instead. +/// +/// [AWS docs for response streaming]: +/// https://docs.aws.amazon.com/lambda/latest/dg/configuration-response-streaming.html +/// +/// # Panics +/// +/// This function panics if required Lambda environment variables are missing +/// (`AWS_LAMBDA_FUNCTION_NAME`, `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`, +/// `AWS_LAMBDA_FUNCTION_VERSION`, `AWS_LAMBDA_RUNTIME_API`). +pub async fn run_with_streaming_response<'a, S, B, E>(handler: S) -> Result<(), Error> +where + S: Service, Error = E>, + S::Future: Send + 'a, + E: Debug + Into, + B: Body + Unpin + Send + 'static, + B::Data: Into + Send, + B::Error: Into + Send + Debug, +{ + lambda_runtime::run(into_stream_service(handler)).await +} + +/// Runs the Lambda runtime with a handler that returns **streaming** HTTP +/// responses, in a mode that is compatible with Lambda Managed Instances. +/// +/// When `AWS_LAMBDA_MAX_CONCURRENCY` is set to a value greater than 1, this +/// spawns multiple tokio worker tasks to handle concurrent invocations. When the +/// environment variable is unset or `<= 1`, it falls back to sequential +/// behavior, so the same handler can run on both classic Lambda and Lambda +/// Managed Instances. +/// +/// # Panics +/// +/// This function panics if: +/// - Called outside of a Tokio runtime +/// - Required Lambda environment variables are missing (`AWS_LAMBDA_FUNCTION_NAME`, +/// `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`, `AWS_LAMBDA_FUNCTION_VERSION`, +/// `AWS_LAMBDA_RUNTIME_API`) +#[cfg(feature = "concurrency-tokio")] +#[cfg_attr(docsrs, doc(cfg(feature = "concurrency-tokio")))] +pub async fn run_with_streaming_response_concurrent(handler: S) -> Result<(), Error> +where + S: Service, Error = E> + Clone + Send + 'static, + S::Future: Send + 'static, + E: Debug + Into + Send + 'static, + B: Body + Unpin + Send + 'static, + B::Data: Into + Send, + B::Error: Into + Send + Debug, +{ + lambda_runtime::run_concurrent(into_stream_service_cloneable(handler)).await +} + +/// Returns a configured [`Runtime`](lambda_runtime::Runtime) wrapping the given +/// streaming handler, without starting the event loop. +/// +/// Use this when you need access to the [`Runtime`](lambda_runtime::Runtime) for +/// SnapStart lifecycle hooks with a streaming response handler. +/// +/// # Panics +/// +/// This function panics if required Lambda environment variables are missing. +pub fn streaming_runtime( + handler: S, +) -> lambda_runtime::Runtime< + impl lambda_runtime::Service, +> +where + S: Service, Error = E> + Send + 'static, + S::Future: Send + 'static, + E: Debug + Into + Send + 'static, + B: Body + Unpin + Send + 'static, + B::Data: Into + Send, + B::Error: Into + Send + Debug, +{ + lambda_runtime::Runtime::new(into_stream_service(handler)) +} + +/// Returns a configured [`Runtime`](lambda_runtime::Runtime) wrapping the given +/// streaming handler for concurrent execution, without starting the event loop. +/// +/// This is the concurrent variant of [`streaming_runtime()`]. +/// +/// # Panics +/// +/// This function panics if required Lambda environment variables are missing. +#[cfg(feature = "concurrency-tokio")] +#[cfg_attr(docsrs, doc(cfg(feature = "concurrency-tokio")))] +pub fn streaming_runtime_concurrent( + handler: S, +) -> lambda_runtime::Runtime< + impl lambda_runtime::Service< + lambda_runtime::LambdaInvocation, + Response = (), + Error = lambda_runtime::Error, + Future: Send, + > + Clone + + Send + + 'static, +> +where + S: Service, Error = E> + Clone + Send + 'static, + S::Future: Send + 'static, + E: Debug + Into + Send + 'static, + B: Body + Unpin + Send + 'static, + B::Data: Into + Send, + B::Error: Into + Send + Debug, +{ + lambda_runtime::Runtime::new(into_stream_service_cloneable(handler)) +} + +pin_project_lite::pin_project! { +#[non_exhaustive] +pub struct BodyStream { + #[pin] + pub(crate) body: B, +} +} + +impl Stream for BodyStream +where + B: Body + Unpin + Send + 'static, + B::Data: Into + Send, + B::Error: Into + Send + Debug, +{ + type Item = Result; + + #[inline] + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match futures_util::ready!(self.as_mut().project().body.poll_frame(cx)?) { + Some(frame) => match frame.into_data() { + Ok(data) => Poll::Ready(Some(Ok(data))), + Err(_frame) => Poll::Ready(None), + }, + None => Poll::Ready(None), + } + } +} + +#[cfg(test)] +mod test_stream_adapter { + use super::*; + + use crate::Body; + use http::StatusCode; + + // A middleware that logs requests before forwarding them to another service + struct LogService { + inner: S, + } + + impl Service> for LogService + where + S: Service>, + { + type Response = S::Response; + type Error = S::Error; + type Future = S::Future; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, event: LambdaEvent) -> Self::Future { + println!("Lambda event: {event:#?}"); + self.inner.call(event) + } + } + + #[test] + fn stream_adapter_is_boxable() { + // Works with a concrete service stack (no boxing) + let svc = ServiceBuilder::new() + .layer_fn(|service| LogService { inner: service }) + .layer_fn(StreamAdapter::from) + .service_fn( + |_req: Request| async move { http::Response::builder().status(StatusCode::OK).body(Body::Empty) }, + ); + // Also works when the stack is boxed (type-erased) + let _boxed_svc = svc.boxed(); + } +} diff --git a/lambda-http/tests/data/alb_multi_value_request.json b/lambda-http/tests/data/alb_multi_value_request.json index 10cf1155e..49edfd236 100644 --- a/lambda-http/tests/data/alb_multi_value_request.json +++ b/lambda-http/tests/data/alb_multi_value_request.json @@ -1,37 +1,70 @@ { - "requestContext": { - "elb": { - "targetGroupArn": "arn:aws:elasticloadbalancing:region:123456789012:targetgroup/my-target-group/6d0ecf831eec9f09" - } - }, - "httpMethod": "GET", - "path": "/", - "queryStringParameters": { "myKey": "val2" }, - "multiValueQueryStringParameters": { "myKey": ["val1", "val2"] }, - "headers": { - "accept": "text/html,application/xhtml+xml", - "accept-language": "en-US,en;q=0.8", - "content-type": "text/plain", - "cookie": "name1=value1", - "host": "lambda-846800462-us-east-2.elb.amazonaws.com", - "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6)", - "x-amzn-trace-id": "Root=1-5bdb40ca-556d8b0c50dc66f0511bf520", - "x-forwarded-for": "72.21.198.66", - "x-forwarded-port": "443", - "x-forwarded-proto": "https" - }, - "multiValueHeaders": { - "accept": ["text/html,application/xhtml+xml"], - "accept-language": ["en-US,en;q=0.8"], - "content-type": ["text/plain"], - "cookie": ["name1=value1", "name2=value2"], - "host": ["lambda-846800462-us-east-2.elb.amazonaws.com"], - "user-agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6)"], - "x-amzn-trace-id": ["Root=1-5bdb40ca-556d8b0c50dc66f0511bf520"], - "x-forwarded-for": ["72.21.198.66"], - "x-forwarded-port": ["443"], - "x-forwarded-proto": ["https"] - }, - "isBase64Encoded": false, - "body": "request_body" + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:region:123456789012:targetgroup/my-target-group/6d0ecf831eec9f09" + } + }, + "httpMethod": "GET", + "path": "/", + "queryStringParameters": { + "myKey": "val2", + "myOtherKey": "val3" + }, + "multiValueQueryStringParameters": { + "myKey": [ + "val1", + "val2" + ], + "myOtherKey": [ + "val3", + "val4" + ] + }, + "headers": { + "accept": "text/html,application/xhtml+xml", + "accept-language": "en-US,en;q=0.8", + "content-type": "text/plain", + "cookie": "name1=value1", + "host": "lambda-846800462-us-east-2.elb.amazonaws.com", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6)", + "x-amzn-trace-id": "Root=1-5bdb40ca-556d8b0c50dc66f0511bf520", + "x-forwarded-for": "72.21.198.66", + "x-forwarded-port": "443", + "x-forwarded-proto": "https" + }, + "multiValueHeaders": { + "accept": [ + "text/html,application/xhtml+xml" + ], + "accept-language": [ + "en-US,en;q=0.8" + ], + "content-type": [ + "text/plain" + ], + "cookie": [ + "name1=value1", + "name2=value2" + ], + "host": [ + "lambda-846800462-us-east-2.elb.amazonaws.com" + ], + "user-agent": [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6)" + ], + "x-amzn-trace-id": [ + "Root=1-5bdb40ca-556d8b0c50dc66f0511bf520" + ], + "x-forwarded-for": [ + "72.21.198.66" + ], + "x-forwarded-port": [ + "443" + ], + "x-forwarded-proto": [ + "https" + ] + }, + "isBase64Encoded": false, + "body": "request_body" } \ No newline at end of file diff --git a/lambda-http/tests/data/apigw_multi_value_proxy_request.json b/lambda-http/tests/data/apigw_multi_value_proxy_request.json index 8f84aeb9b..280f49cb2 100644 --- a/lambda-http/tests/data/apigw_multi_value_proxy_request.json +++ b/lambda-http/tests/data/apigw_multi_value_proxy_request.json @@ -23,74 +23,74 @@ "X-Forwarded-Port": "443", "X-Forwarded-Proto": "https" }, - "multiValueHeaders":{ - "Accept":[ + "multiValueHeaders": { + "Accept": [ "*/*" ], - "Accept-Encoding":[ + "Accept-Encoding": [ "gzip, deflate" ], - "cache-control":[ + "cache-control": [ "no-cache" ], - "CloudFront-Forwarded-Proto":[ + "CloudFront-Forwarded-Proto": [ "https" ], - "CloudFront-Is-Desktop-Viewer":[ + "CloudFront-Is-Desktop-Viewer": [ "true" ], - "CloudFront-Is-Mobile-Viewer":[ + "CloudFront-Is-Mobile-Viewer": [ "false" ], - "CloudFront-Is-SmartTV-Viewer":[ + "CloudFront-Is-SmartTV-Viewer": [ "false" ], - "CloudFront-Is-Tablet-Viewer":[ + "CloudFront-Is-Tablet-Viewer": [ "false" ], - "CloudFront-Viewer-Country":[ + "CloudFront-Viewer-Country": [ "US" ], - "Content-Type":[ + "Content-Type": [ "application/json" ], - "headerName":[ + "headerName": [ "headerValue" ], - "Host":[ + "Host": [ "gy415nuibc.execute-api.us-east-1.amazonaws.com" ], - "Postman-Token":[ + "Postman-Token": [ "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f" ], - "User-Agent":[ + "User-Agent": [ "PostmanRuntime/2.4.5" ], - "Via":[ + "Via": [ "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)" ], - "X-Amz-Cf-Id":[ + "X-Amz-Cf-Id": [ "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==" ], - "X-Forwarded-For":[ + "X-Forwarded-For": [ "54.240.196.186, 54.182.214.83" ], - "X-Forwarded-Port":[ + "X-Forwarded-Port": [ "443" ], - "X-Forwarded-Proto":[ + "X-Forwarded-Proto": [ "https" ] }, "queryStringParameters": { "name": "me", - "multivalueName": "me" + "multiValueName": "me" }, - "multiValueQueryStringParameters":{ - "name":[ + "multiValueQueryStringParameters": { + "name": [ "me" ], - "multivalueName":[ + "multiValueName": [ "you", "me" ] diff --git a/lambda-http/tests/data/apigw_no_host.json b/lambda-http/tests/data/apigw_no_host.json index 3143c81b6..78a40dee6 100644 --- a/lambda-http/tests/data/apigw_no_host.json +++ b/lambda-http/tests/data/apigw_no_host.json @@ -1,5 +1,5 @@ { - "path": "/test/hello", + "path": "/hello", "headers": { "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Encoding": "gzip, deflate, lzma, sdch, br", diff --git a/lambda-http/tests/data/apigw_proxy_request.json b/lambda-http/tests/data/apigw_proxy_request.json index 3b7cc9d23..61183846c 100644 --- a/lambda-http/tests/data/apigw_proxy_request.json +++ b/lambda-http/tests/data/apigw_proxy_request.json @@ -1,5 +1,5 @@ { - "path": "/test/hello", + "path": "/hello", "headers": { "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Encoding": "gzip, deflate, lzma, sdch, br", diff --git a/lambda-http/tests/data/apigw_v2_proxy_request_with_stage_in_path.json b/lambda-http/tests/data/apigw_v2_proxy_request_with_stage_in_path.json new file mode 100644 index 000000000..86e173c6a --- /dev/null +++ b/lambda-http/tests/data/apigw_v2_proxy_request_with_stage_in_path.json @@ -0,0 +1,57 @@ +{ + "version": "2.0", + "routeKey": "Prod", + "rawPath": "/Prod/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1=value1", + "cookie2=value2" + ], + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "authorizer": { + "jwt": { + "claims": { + "claim1": "value1", + "claim2": "value2" + }, + "scopes": [ + "scope1", + "scope2" + ] + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/Prod/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "IP", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "Prod", + "stage": "Prod", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "body": "Hello from Lambda", + "pathParameters": { + "parameter1": "value1" + }, + "isBase64Encoded": false, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + } +} \ No newline at end of file diff --git a/lambda-http/tests/data/lambda_function_url_request.json b/lambda-http/tests/data/lambda_function_url_request.json new file mode 100644 index 000000000..588ae4d18 --- /dev/null +++ b/lambda-http/tests/data/lambda_function_url_request.json @@ -0,0 +1,40 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "test=hi" + ], + "headers": { + "x-amzn-tls-cipher-suite": "ECDHE-RSA-AES128-GCM-SHA256", + "x-amzn-tls-version": "TLSv1.2", + "x-amzn-trace-id": "Root=1-5eb33c07-de25b420912dee103a5db434", + "cookie": "test=hi", + "x-forwarded-proto": "https", + "host": "id.lambda-url.eu-west-2.on.aws", + "x-forwarded-port": "443", + "x-forwarded-for": "65.78.31.245", + "accept": "*/*", + "user-agent": "curl/7.68.0" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "xxx", + "domainName": "id.lambda-url.eu-west-2.on.aws", + "domainPrefix": "id", + "http": { + "method": "GET", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "65.78.31.245", + "userAgent": "curl/7.68.0" + }, + "requestId": "MIZRNhJtIAMEMDw=", + "routeKey": "$default", + "stage": "$default", + "time": "11/Jan/2023:11:45:34 +0000", + "timeEpoch": 1673437534837 + }, + "isBase64Encoded": false +} \ No newline at end of file diff --git a/lambda-http/tests/data/vpc_lattice_v2_request.json b/lambda-http/tests/data/vpc_lattice_v2_request.json new file mode 100644 index 000000000..2ac3920f9 --- /dev/null +++ b/lambda-http/tests/data/vpc_lattice_v2_request.json @@ -0,0 +1,30 @@ +{ + "version": "2.0", + "path": "/health", + "method": "GET", + "headers": { + "accept": ["*/*"], + "user-agent": ["curl/7.68.0"], + "x-forwarded-for": ["10.0.2.100"], + "multi": ["x", "y"] + }, + "queryStringParameters": { + "state": ["prod"], + "multi": ["a", "DEF", "g"] + }, + "body": "All is good", + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", + "type": "AWS_IAM", + "principal": "arn:aws:iam::123456789012:role/service-role/HealthChecker", + "principalOrgID": "o-50dc6c495c0c9188" + }, + "region": "ap-southeast-2", + "timeEpoch": "1724875399456789" + } +} \ No newline at end of file diff --git a/lambda-http/tests/data/vpc_lattice_v2_request_base64.json b/lambda-http/tests/data/vpc_lattice_v2_request_base64.json new file mode 100644 index 000000000..c622a775f --- /dev/null +++ b/lambda-http/tests/data/vpc_lattice_v2_request_base64.json @@ -0,0 +1,31 @@ +{ + "version": "2.0", + "path": "/health", + "method": "GET", + "headers": { + "host": "www.site.com", + "accept": ["*/*"], + "user-agent": ["curl/7.68.0"], + "x-forwarded-for": ["10.0.2.100"], + "multi": ["x", "y"] + }, + "queryStringParameters": { + "state": ["prod"], + "multi": ["a", "DEF", "g"] + }, + "body": "QWxsIGlzIGdvb2Q=", + "isBase64Encoded": true, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", + "type": "AWS_IAM", + "principal": "arn:aws:iam::123456789012:role/service-role/HealthChecker", + "principalOrgID": "o-50dc6c495c0c9188" + }, + "region": "ap-southeast-2", + "timeEpoch": "1724875399456789" + } +} \ No newline at end of file diff --git a/lambda-http/tests/data/vpc_lattice_v2_request_encoded_query.json b/lambda-http/tests/data/vpc_lattice_v2_request_encoded_query.json new file mode 100644 index 000000000..a848638e3 --- /dev/null +++ b/lambda-http/tests/data/vpc_lattice_v2_request_encoded_query.json @@ -0,0 +1,21 @@ +{ + "version": "2.0", + "path": "/search", + "method": "GET", + "headers": { + "host": ["api.example.com"], + "accept": ["*/*"] + }, + "queryStringParameters": { + "filter": ["%3FshowAll%3Dtrue"], + "q": ["hello%20world"] + }, + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "region": "ap-southeast-2", + "timeEpoch": "1724875399456789" + } +} \ No newline at end of file diff --git a/lambda-integration-tests/Cargo.toml b/lambda-integration-tests/Cargo.toml index 116b29867..e2497333e 100644 --- a/lambda-integration-tests/Cargo.toml +++ b/lambda-integration-tests/Cargo.toml @@ -1,22 +1,38 @@ [package] -name = "lambda_integration_tests" -version = "0.5.0" -authors = ["Nicolas Moutschen "] -edition = "2018" +name = "lambda-integration-tests" +version = "1.0.1" +authors = ["Maxime David"] +edition = "2021" +rust-version = "1.84.0" description = "AWS Lambda Runtime integration tests" license = "Apache-2.0" -repository = "https://github.com/awslabs/aws-lambda-rust-runtime" +repository = "https://github.com/aws/aws-lambda-rust-runtime" categories = ["web-programming::http-server"] keywords = ["AWS", "Lambda", "API"] readme = "../README.md" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] -lambda_http = { path = "../lambda-http", version = "0.7" } -lambda_runtime = { path = "../lambda-runtime", version = "0.7" } -lambda-extension = { path = "../lambda-extension", version = "0.7" } -serde = { version = "1", features = ["derive"] } +lambda_runtime = { path = "../lambda-runtime", features = ["tracing", "graceful-shutdown"] } +aws_lambda_events = { path = "../lambda-events" } +serde_json = "1.0.121" tokio = { version = "1", features = ["full"] } -tracing = { version = "0.1", features = ["log"] } -tracing-subscriber = "0.3" \ No newline at end of file +serde = { version = "1.0.204", features = ["derive"] } +tracing = "0.1" + +[dev-dependencies] +reqwest = { version = "0.13.1", features = ["blocking"] } + +[features] +catch-all-fields = ["aws_lambda_events/catch-all-fields"] + +[[bin]] +name = "helloworld" +path = "src/helloworld.rs" + +[[bin]] +name = "authorizer" +path = "src/authorizer.rs" + +[[bin]] +name = "tenant-id-test" +path = "src/tenant_id_test.rs" diff --git a/lambda-integration-tests/python/main.py b/lambda-integration-tests/python/main.py deleted file mode 100644 index e7e2114b2..000000000 --- a/lambda-integration-tests/python/main.py +++ /dev/null @@ -1,4 +0,0 @@ -def handler(event, context): - return { - "message": event["command"].upper() - } \ No newline at end of file diff --git a/lambda-integration-tests/samconfig.toml b/lambda-integration-tests/samconfig.toml new file mode 100644 index 000000000..f3f49b5ae --- /dev/null +++ b/lambda-integration-tests/samconfig.toml @@ -0,0 +1,24 @@ +version = 0.1 + +[default] +[default.build.parameters] +cached = true +parallel = false +build_in_source = true + +[default.validate.parameters] +lint = true + +[default.deploy.parameters] +capabilities = "CAPABILITY_IAM" +confirm_changeset = true +resolve_s3 = true + +[default.sync.parameters] +watch = true + +[default.local_start_api.parameters] +warm_containers = "EAGER" + +[default.local_start_lambda.parameters] +warm_containers = "EAGER" \ No newline at end of file diff --git a/lambda-integration-tests/src/authorizer.rs b/lambda-integration-tests/src/authorizer.rs new file mode 100644 index 000000000..23f17b296 --- /dev/null +++ b/lambda-integration-tests/src/authorizer.rs @@ -0,0 +1,69 @@ +use std::env; + +use aws_lambda_events::{ + apigw::{ApiGatewayCustomAuthorizerPolicy, ApiGatewayCustomAuthorizerResponse}, + event::iam::IamPolicyStatement, +}; +use lambda_runtime::{service_fn, tracing, Error, LambdaEvent}; +use serde::Deserialize; +use serde_json::json; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct APIGatewayCustomAuthorizerRequest { + authorization_token: String, + method_arn: String, +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing::init_default_subscriber(); + let func = service_fn(func); + lambda_runtime::run(func).await?; + Ok(()) +} + +async fn func( + event: LambdaEvent, +) -> Result { + let expected_token = env::var("SECRET_TOKEN").expect("could not read the secret token"); + if event.payload.authorization_token == expected_token { + return Ok(allow(&event.payload.method_arn)); + } + panic!("token is not valid"); +} + +fn allow(method_arn: &str) -> ApiGatewayCustomAuthorizerResponse { + let stmt = { + let mut statement = IamPolicyStatement::default(); + statement.action = vec!["execute-api:Invoke".to_string()]; + statement.resource = vec![method_arn.to_owned()]; + statement.effect = aws_lambda_events::iam::IamPolicyEffect::Allow; + statement.condition = None; + #[cfg(feature = "catch-all-fields")] + { + statement.other = Default::default(); + } + statement + }; + let policy = { + let mut policy = ApiGatewayCustomAuthorizerPolicy::default(); + policy.version = Some("2012-10-17".to_string()); + policy.statement = vec![stmt]; + #[cfg(feature = "catch-all-fields")] + { + policy.other = Default::default(); + } + policy + }; + let mut response = ApiGatewayCustomAuthorizerResponse::default(); + response.principal_id = Some("user".to_owned()); + response.policy_document = policy; + response.context = json!({ "hello": "world" }); + response.usage_identifier_key = None; + #[cfg(feature = "catch-all-fields")] + { + response.other = Default::default(); + } + response +} diff --git a/lambda-integration-tests/src/bin/extension-fn.rs b/lambda-integration-tests/src/bin/extension-fn.rs deleted file mode 100644 index ea5fc26c3..000000000 --- a/lambda-integration-tests/src/bin/extension-fn.rs +++ /dev/null @@ -1,31 +0,0 @@ -use lambda_extension::{service_fn, Error, LambdaEvent, NextEvent}; -use tracing::info; - -async fn my_extension(event: LambdaEvent) -> Result<(), Error> { - match event.next { - NextEvent::Shutdown(e) => { - info!("[extension-fn] Shutdown event received: {:?}", e); - } - NextEvent::Invoke(e) => { - info!("[extension-fn] Request event received: {:?}", e); - } - } - - Ok(()) -} - -#[tokio::main] -async fn main() -> Result<(), Error> { - // The runtime logging can be enabled here by initializing `tracing` with `tracing-subscriber` - // While `tracing` is used internally, `log` can be used as well if preferred. - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - // this needs to be set to false, otherwise ANSI color codes will - // show up in a confusing manner in CloudWatch logs. - .with_ansi(false) - // disabling time is handy because CloudWatch will add the ingestion time. - .without_time() - .init(); - - lambda_extension::run(service_fn(my_extension)).await -} diff --git a/lambda-integration-tests/src/bin/extension-trait.rs b/lambda-integration-tests/src/bin/extension-trait.rs deleted file mode 100644 index ecf46c81e..000000000 --- a/lambda-integration-tests/src/bin/extension-trait.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::{ - future::{ready, Future}, - pin::Pin, - sync::atomic::{AtomicBool, Ordering}, -}; - -use lambda_extension::{Error, LambdaEvent, NextEvent, Service}; -use tracing::info; - -struct MyExtension { - invoke_count: usize, - ready: AtomicBool, -} - -impl Default for MyExtension { - fn default() -> Self { - Self { - invoke_count: usize::default(), - // New instances are not ready to be called until polled. - ready: false.into(), - } - } -} - -impl Clone for MyExtension { - fn clone(&self) -> Self { - Self { - invoke_count: self.invoke_count, - // Cloned instances may not be immediately ready to be called. - // https://docs.rs/tower/0.4.13/tower/trait.Service.html#be-careful-when-cloning-inner-services - ready: false.into(), - } - } -} - -impl Service for MyExtension { - type Error = Error; - type Future = Pin>>>; - type Response = (); - - fn poll_ready(&mut self, _cx: &mut core::task::Context<'_>) -> core::task::Poll> { - if self.ready.swap(true, Ordering::SeqCst) { - info!("[extension] Service was already ready"); - } else { - info!("[extension] Service is now ready"); - }; - - core::task::Poll::Ready(Ok(())) - } - - fn call(&mut self, event: LambdaEvent) -> Self::Future { - match event.next { - NextEvent::Shutdown(e) => { - info!("[extension] Shutdown event received: {:?}", e); - } - NextEvent::Invoke(e) => { - self.invoke_count += 1; - info!("[extension] Request event {} received: {:?}", self.invoke_count, e); - } - } - - // After being called once, the service is no longer ready until polled again. - if self.ready.swap(false, Ordering::SeqCst) { - info!("[extension] The service is ready"); - } else { - // https://docs.rs/tower/latest/tower/trait.Service.html#backpressure - // https://docs.rs/tower/latest/tower/trait.Service.html#be-careful-when-cloning-inner-services - // > Services are permitted to panic if `call` is invoked without obtaining - // > `Poll::Ready(Ok(()))` from `poll_ready`. - panic!("[extension] The service is not ready; `.poll_ready()` must be called first"); - } - - Box::pin(ready(Ok(()))) - } -} - -#[tokio::main] -async fn main() -> Result<(), Error> { - // The runtime logging can be enabled here by initializing `tracing` with `tracing-subscriber` - // While `tracing` is used internally, `log` can be used as well if preferred. - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - // this needs to be set to false, otherwise ANSI color codes will - // show up in a confusing manner in CloudWatch logs. - .with_ansi(false) - // disabling time is handy because CloudWatch will add the ingestion time. - .without_time() - .init(); - - lambda_extension::run(MyExtension::default()).await -} diff --git a/lambda-integration-tests/src/bin/http-fn.rs b/lambda-integration-tests/src/bin/http-fn.rs deleted file mode 100644 index 4170d29fc..000000000 --- a/lambda-integration-tests/src/bin/http-fn.rs +++ /dev/null @@ -1,29 +0,0 @@ -use lambda_http::{service_fn, Body, Error, IntoResponse, Request, RequestExt, Response}; -use tracing::info; - -async fn handler(event: Request) -> Result { - let _context = event.lambda_context(); - info!("[http-fn] Received event {} {}", event.method(), event.uri().path()); - - Ok(Response::builder() - .status(200) - .body(Body::from("Hello, world!")) - .unwrap()) -} - -#[tokio::main] -async fn main() -> Result<(), Error> { - // The runtime logging can be enabled here by initializing `tracing` with `tracing-subscriber` - // While `tracing` is used internally, `log` can be used as well if preferred. - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - // this needs to be set to false, otherwise ANSI color codes will - // show up in a confusing manner in CloudWatch logs. - .with_ansi(false) - // disabling time is handy because CloudWatch will add the ingestion time. - .without_time() - .init(); - - let handler = service_fn(handler); - lambda_http::run(handler).await -} diff --git a/lambda-integration-tests/src/bin/http-trait.rs b/lambda-integration-tests/src/bin/http-trait.rs deleted file mode 100644 index fffe0db09..000000000 --- a/lambda-integration-tests/src/bin/http-trait.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::{ - future::{ready, Future}, - pin::Pin, - sync::atomic::{AtomicBool, Ordering}, -}; - -use lambda_http::{Body, Error, Request, RequestExt, Response, Service}; -use tracing::info; - -struct MyHandler { - invoke_count: usize, - ready: AtomicBool, -} - -impl Default for MyHandler { - fn default() -> Self { - Self { - invoke_count: usize::default(), - // New instances are not ready to be called until polled. - ready: false.into(), - } - } -} - -impl Clone for MyHandler { - fn clone(&self) -> Self { - Self { - invoke_count: self.invoke_count, - // Cloned instances may not be immediately ready to be called. - // https://docs.rs/tower/0.4.13/tower/trait.Service.html#be-careful-when-cloning-inner-services - ready: false.into(), - } - } -} - -impl Service for MyHandler { - type Error = Error; - type Future = Pin> + Send>>; - type Response = Response; - - fn poll_ready(&mut self, _cx: &mut core::task::Context<'_>) -> core::task::Poll> { - if self.ready.swap(true, Ordering::SeqCst) { - info!("[http-trait] Service was already ready"); - } else { - info!("[http-trait] Service is now ready"); - }; - - core::task::Poll::Ready(Ok(())) - } - - fn call(&mut self, request: Request) -> Self::Future { - self.invoke_count += 1; - info!("[http-trait] Received event {}: {:?}", self.invoke_count, request); - info!("[http-trait] Lambda context: {:?}", request.lambda_context()); - - // After being called once, the service is no longer ready until polled again. - if self.ready.swap(false, Ordering::SeqCst) { - info!("[http-trait] The service is ready"); - } else { - // https://docs.rs/tower/latest/tower/trait.Service.html#backpressure - // https://docs.rs/tower/latest/tower/trait.Service.html#be-careful-when-cloning-inner-services - // > Services are permitted to panic if `call` is invoked without obtaining - // > `Poll::Ready(Ok(()))` from `poll_ready`. - panic!("[http-trait] The service is not ready; `.poll_ready()` must be called first"); - } - - Box::pin(ready(Ok(Response::builder() - .status(200) - .body(Body::from("Hello, World!")) - .unwrap()))) - } -} - -#[tokio::main] -async fn main() -> Result<(), Error> { - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - // this needs to be set to false, otherwise ANSI color codes will - // show up in a confusing manner in CloudWatch logs. - .with_ansi(false) - // disabling time is handy because CloudWatch will add the ingestion time. - .without_time() - .init(); - - lambda_http::run(MyHandler::default()).await -} diff --git a/lambda-integration-tests/src/bin/logs-trait.rs b/lambda-integration-tests/src/bin/logs-trait.rs deleted file mode 100644 index 3f5a49091..000000000 --- a/lambda-integration-tests/src/bin/logs-trait.rs +++ /dev/null @@ -1,78 +0,0 @@ -use lambda_extension::{Error, Extension, LambdaLog, LambdaLogRecord, Service, SharedService}; -use std::{ - future::{ready, Future}, - pin::Pin, - sync::{ - atomic::{AtomicUsize, Ordering::SeqCst}, - Arc, - }, - task::Poll, -}; -use tracing::info; - -/// Custom log processor that increments a counter for each log record. -/// -/// This is a simple example of a custom log processor that can be used to -/// count the number of log records that are processed. -/// -/// This needs to derive Clone (and store the counter in an Arc) as the runtime -/// could need multiple `Service`s to process the logs. -#[derive(Clone, Default)] -struct MyLogsProcessor { - counter: Arc, -} - -impl MyLogsProcessor { - pub fn new() -> Self { - Self::default() - } -} - -type MyLogsFuture = Pin> + Send>>; - -/// Implementation of the actual log processor -/// -/// This receives a `Vec` whenever there are new log entries available. -impl Service> for MyLogsProcessor { - type Response = (); - type Error = Error; - type Future = MyLogsFuture; - - fn poll_ready(&mut self, _cx: &mut core::task::Context<'_>) -> core::task::Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, logs: Vec) -> Self::Future { - let counter = self.counter.fetch_add(1, SeqCst); - for log in logs { - match log.record { - LambdaLogRecord::Function(record) => { - info!("[logs] {} [function] {}: {}", log.time, counter, record.trim()) - } - LambdaLogRecord::Extension(record) => { - info!("[logs] {} [extension] {}: {}", log.time, counter, record.trim()) - } - _ => (), - } - } - - Box::pin(ready(Ok(()))) - } -} - -#[tokio::main] -async fn main() -> Result<(), Error> { - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - // this needs to be set to false, otherwise ANSI color codes will - // show up in a confusing manner in CloudWatch logs. - .with_ansi(false) - // disabling time is handy because CloudWatch will add the ingestion time. - .without_time() - .init(); - - let logs_processor = SharedService::new(MyLogsProcessor::new()); - Extension::new().with_logs_processor(logs_processor).run().await?; - - Ok(()) -} diff --git a/lambda-integration-tests/src/bin/runtime-fn.rs b/lambda-integration-tests/src/bin/runtime-fn.rs deleted file mode 100644 index 1b3f3e0d1..000000000 --- a/lambda-integration-tests/src/bin/runtime-fn.rs +++ /dev/null @@ -1,39 +0,0 @@ -use lambda_runtime::{service_fn, Error, LambdaEvent}; -use serde::{Deserialize, Serialize}; -use tracing::info; - -#[derive(Deserialize, Debug)] -struct Request { - command: String, -} - -#[derive(Serialize, Debug)] -struct Response { - message: String, -} - -async fn handler(event: LambdaEvent) -> Result { - info!("[handler-fn] Received event: {:?}", event); - - let (event, _) = event.into_parts(); - - Ok(Response { - message: event.command.to_uppercase(), - }) -} - -#[tokio::main] -async fn main() -> Result<(), Error> { - // The runtime logging can be enabled here by initializing `tracing` with `tracing-subscriber` - // While `tracing` is used internally, `log` can be used as well if preferred. - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - // this needs to be set to false, otherwise ANSI color codes will - // show up in a confusing manner in CloudWatch logs. - .with_ansi(false) - // disabling time is handy because CloudWatch will add the ingestion time. - .without_time() - .init(); - - lambda_runtime::run(service_fn(handler)).await -} diff --git a/lambda-integration-tests/src/bin/runtime-trait.rs b/lambda-integration-tests/src/bin/runtime-trait.rs deleted file mode 100644 index b925e1389..000000000 --- a/lambda-integration-tests/src/bin/runtime-trait.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::{ - future::{ready, Future}, - pin::Pin, - sync::atomic::{AtomicBool, Ordering}, -}; - -use lambda_runtime::{Error, LambdaEvent, Service}; -use serde::{Deserialize, Serialize}; -use tracing::info; - -#[derive(Deserialize, Debug)] -struct Request { - command: String, -} - -#[derive(Serialize, Debug)] -struct Response { - message: String, -} - -struct MyHandler { - invoke_count: usize, - ready: AtomicBool, -} - -impl Default for MyHandler { - fn default() -> Self { - Self { - invoke_count: usize::default(), - // New instances are not ready to be called until polled. - ready: false.into(), - } - } -} - -impl Clone for MyHandler { - fn clone(&self) -> Self { - Self { - invoke_count: self.invoke_count, - // Cloned instances may not be immediately ready to be called. - // https://docs.rs/tower/0.4.13/tower/trait.Service.html#be-careful-when-cloning-inner-services - ready: false.into(), - } - } -} - -impl Service> for MyHandler { - type Error = Error; - type Future = Pin>>>; - type Response = Response; - - fn poll_ready(&mut self, _cx: &mut core::task::Context<'_>) -> core::task::Poll> { - if self.ready.swap(true, Ordering::SeqCst) { - info!("[runtime-trait] Service was already ready"); - } else { - info!("[runtime-trait] Service is now ready"); - }; - - core::task::Poll::Ready(Ok(())) - } - - fn call(&mut self, request: LambdaEvent) -> Self::Future { - self.invoke_count += 1; - info!("[runtime-trait] Received event {}: {:?}", self.invoke_count, request); - - // After being called once, the service is no longer ready until polled again. - if self.ready.swap(false, Ordering::SeqCst) { - info!("[runtime-trait] The service is ready"); - } else { - // https://docs.rs/tower/latest/tower/trait.Service.html#backpressure - // https://docs.rs/tower/latest/tower/trait.Service.html#be-careful-when-cloning-inner-services - // > Services are permitted to panic if `call` is invoked without obtaining - // > `Poll::Ready(Ok(()))` from `poll_ready`. - panic!("[runtime-trait] The service is not ready; `.poll_ready()` must be called first"); - } - - Box::pin(ready(Ok(Response { - message: request.payload.command.to_uppercase(), - }))) - } -} - -#[tokio::main] -async fn main() -> Result<(), Error> { - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - // this needs to be set to false, otherwise ANSI color codes will - // show up in a confusing manner in CloudWatch logs. - .with_ansi(false) - // disabling time is handy because CloudWatch will add the ingestion time. - .without_time() - .init(); - - lambda_runtime::run(MyHandler::default()).await -} diff --git a/lambda-integration-tests/src/helloworld.rs b/lambda-integration-tests/src/helloworld.rs new file mode 100644 index 000000000..2eafc409a --- /dev/null +++ b/lambda-integration-tests/src/helloworld.rs @@ -0,0 +1,33 @@ +use aws_lambda_events::{ + apigw::{ApiGatewayProxyRequest, ApiGatewayProxyResponse}, + http::HeaderMap, +}; +use lambda_runtime::{service_fn, tracing, Error, LambdaEvent}; + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing::init_default_subscriber(); + let func = service_fn(func); + lambda_runtime::spawn_graceful_shutdown_handler(|| async move {}).await; + lambda_runtime::run(func).await?; + Ok(()) +} + +async fn func(_event: LambdaEvent) -> Result { + let mut headers = HeaderMap::new(); + headers.insert("content-type", "text/html".parse().unwrap()); + let resp = { + let mut response = ApiGatewayProxyResponse::default(); + response.status_code = 200; + response.multi_value_headers = headers.clone(); + response.is_base64_encoded = false; + response.body = Some("Hello world!".into()); + response.headers = headers; + #[cfg(feature = "catch-all-fields")] + { + response.other = Default::default(); + } + response + }; + Ok(resp) +} diff --git a/lambda-integration-tests/src/tenant_id_test.rs b/lambda-integration-tests/src/tenant_id_test.rs new file mode 100644 index 000000000..aeebc1827 --- /dev/null +++ b/lambda-integration-tests/src/tenant_id_test.rs @@ -0,0 +1,26 @@ +use lambda_runtime::{service_fn, Error, LambdaEvent}; +use serde_json::{json, Value}; + +async fn function_handler(event: LambdaEvent) -> Result { + let (event, context) = event.into_parts(); + + tracing::info!("Processing request with tenant ID: {:?}", context.tenant_id); + + let response = json!({ + "statusCode": 200, + "body": json!({ + "message": "Tenant ID test successful", + "request_id": context.request_id, + "tenant_id": context.tenant_id, + "input": event + }).to_string() + }); + + Ok(response) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + lambda_runtime::tracing::init_default_subscriber(); + lambda_runtime::run(service_fn(function_handler)).await +} diff --git a/lambda-integration-tests/template.yaml b/lambda-integration-tests/template.yaml index d3716d7cc..9ba694e32 100644 --- a/lambda-integration-tests/template.yaml +++ b/lambda-integration-tests/template.yaml @@ -1,239 +1,77 @@ AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 +Description: maxday-test + +Parameters: + LambdaRole: + Type: String + SecretToken: + Type: String Globals: Function: - MemorySize: 128 - Handler: bootstrap - Timeout: 5 + Timeout: 3 Resources: - # Rust function using runtime_fn running on AL2 - RuntimeFnAl2: - Type: AWS::Serverless::Function - Properties: - CodeUri: ../build/runtime-fn/ - Runtime: provided.al2 - Layers: - - !Ref LogsTrait - - !Ref ExtensionFn - - !Ref ExtensionTrait - - # Rust function using runtime_fn running on AL1 - RuntimeFn: - Type: AWS::Serverless::Function - Properties: - CodeUri: ../build/runtime-fn/ - Runtime: provided - Layers: - - !Ref LogsTrait - - !Ref ExtensionFn - - !Ref ExtensionTrait - - # Rust function using a Service implementation running on AL2 - RuntimeTraitAl2: - Type: AWS::Serverless::Function - Properties: - CodeUri: ../build/runtime-trait/ - Runtime: provided.al2 - Layers: - - !Ref LogsTrait - - !Ref ExtensionFn - - !Ref ExtensionTrait - - # Rust function using a Service implementation running on AL1 - RuntimeTrait: - Type: AWS::Serverless::Function - Properties: - CodeUri: ../build/runtime-trait/ - Runtime: provided - Layers: - - !Ref LogsTrait - - !Ref ExtensionFn - - !Ref ExtensionTrait - - # Rust function using lambda_http::service_fn running on AL2 - HttpFnAl2: - Type: AWS::Serverless::Function - Properties: - CodeUri: ../build/http-fn/ - Runtime: provided.al2 - Events: - ApiGet: - Type: Api - Properties: - Method: GET - Path: /al2/get - ApiPost: - Type: Api - Properties: - Method: POST - Path: /al2/post - ApiV2Get: - Type: HttpApi - Properties: - Method: GET - Path: /al2/get - ApiV2Post: - Type: HttpApi - Properties: - Method: POST - Path: /al2/post - Layers: - - !Ref LogsTrait - - !Ref ExtensionFn - - !Ref ExtensionTrait - - # Rust function using lambda_http with Service running on AL2 - HttpTraitAl2: - Type: AWS::Serverless::Function - Properties: - CodeUri: ../build/http-trait/ - Runtime: provided.al2 - Events: - ApiGet: - Type: Api - Properties: - Method: GET - Path: /al2-trait/get - ApiPost: - Type: Api - Properties: - Method: POST - Path: /al2-trait/post - ApiV2Get: - Type: HttpApi - Properties: - Method: GET - Path: /al2-trait/get - ApiV2Post: - Type: HttpApi - Properties: - Method: POST - Path: /al2-trait/post - Layers: - - !Ref LogsTrait - - !Ref ExtensionFn - - !Ref ExtensionTrait - - # Rust function using lambda_http::service_fn running on AL1 - HttpFn: - Type: AWS::Serverless::Function + API: + Type: AWS::Serverless::Api Properties: - CodeUri: ../build/http-fn/ - Runtime: provided - Events: - ApiGet: - Type: Api - Properties: - Method: GET - Path: /get - ApiPost: - Type: Api - Properties: - Method: POST - Path: /post - ApiV2Get: - Type: HttpApi - Properties: - Method: GET - Path: /get - ApiV2Post: - Type: HttpApi - Properties: - Method: POST - Path: /post - Layers: - - !Ref LogsTrait - - !Ref ExtensionFn - - !Ref ExtensionTrait - - # Rust function using lambda_http with Service running on AL1 - HttpTrait: + StageName: integ-test + Auth: + DefaultAuthorizer: MyLambdaAuthorizer + Authorizers: + MyLambdaAuthorizer: + FunctionArn: !GetAtt AuthorizerFunction.Arn + HelloWorldFunction: Type: AWS::Serverless::Function + Metadata: + BuildMethod: rust-cargolambda + BuildProperties: + Binary: helloworld Properties: - CodeUri: ../build/http-trait/ - Runtime: provided + CodeUri: ./ + Handler: bootstrap + Runtime: provided.al2023 + Role: !Ref LambdaRole Events: - ApiGet: - Type: Api - Properties: - Method: GET - Path: /trait/get - ApiPost: + HelloWorld: Type: Api Properties: - Method: POST - Path: /trait/post - ApiV2Get: - Type: HttpApi - Properties: - Method: GET - Path: /trait/get - ApiV2Post: - Type: HttpApi - Properties: - Method: POST - Path: /trait/post - Layers: - - !Ref LogsTrait - - !Ref ExtensionFn - - !Ref ExtensionTrait + RestApiId: !Ref API + Path: /hello + Method: get - # Python function running on AL2 - PythonAl2: + TenantIdTestFunction: Type: AWS::Serverless::Function + Metadata: + BuildMethod: rust-cargolambda + BuildProperties: + Binary: tenant-id-test Properties: - CodeUri: ./python/ - Handler: main.handler - Runtime: python3.9 - Layers: - - !Ref LogsTrait - - !Ref ExtensionFn - - !Ref ExtensionTrait + CodeUri: ./ + Handler: bootstrap + Runtime: provided.al2023 + Role: !Ref LambdaRole - # Python function running on AL1 - Python: + AuthorizerFunction: Type: AWS::Serverless::Function + Metadata: + BuildMethod: rust-cargolambda + BuildProperties: + Binary: authorizer Properties: - CodeUri: ./python/ - Handler: main.handler - Runtime: python3.7 - Layers: - - !Ref LogsTrait - - !Ref ExtensionFn - - !Ref ExtensionTrait - - LogsTrait: - Type: AWS::Serverless::LayerVersion - Properties: - ContentUri: ../build/logs-trait/ - - ExtensionFn: - Type: AWS::Serverless::LayerVersion - Properties: - ContentUri: ../build/extension-fn/ - - ExtensionTrait: - Type: AWS::Serverless::LayerVersion - Properties: - ContentUri: ../build/extension-trait/ + CodeUri: ./ + Handler: bootstrap + Runtime: provided.al2023 + Role: !Ref LambdaRole + Environment: + Variables: + SECRET_TOKEN: !Ref SecretToken Outputs: - RuntimeFnAl2: - Value: !GetAtt RuntimeFnAl2.Arn - RuntimeFn: - Value: !GetAtt RuntimeFn.Arn - RuntimeTraitAl2: - Value: !GetAtt RuntimeTraitAl2.Arn - RuntimeTrait: - Value: !GetAtt RuntimeTrait.Arn - PythonAl2: - Value: !GetAtt PythonAl2.Arn - Python: - Value: !GetAtt Python.Arn - - RestApiUrl: - Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod" - HttpApiUrl: - Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" \ No newline at end of file + HelloApiEndpoint: + Description: "API Gateway endpoint URL for HelloWorld" + Value: !Sub "https://${API}.execute-api.${AWS::Region}.amazonaws.com/integ-test/hello/" + TenantIdTestFunction: + Description: "Tenant ID test function name" + Value: !Ref TenantIdTestFunction \ No newline at end of file diff --git a/lambda-integration-tests/tests/integration_test.rs b/lambda-integration-tests/tests/integration_test.rs new file mode 100644 index 000000000..557b8e0d3 --- /dev/null +++ b/lambda-integration-tests/tests/integration_test.rs @@ -0,0 +1,12 @@ +#[test] +fn test_calling_lambda_should_return_200() { + let test_endpoint = std::env::var("TEST_ENDPOINT").expect("could not read TEST_ENDPOINT"); + let secret_token = std::env::var("SECRET_TOKEN").expect("could not read SECRET_TOKEN"); + let client = reqwest::blocking::Client::new(); + let res = client + .get(test_endpoint) + .header("Authorization", secret_token) + .send() + .expect("could not the request"); + assert_eq!(res.status(), 200); +} diff --git a/lambda-integration-tests/tests/tenant_id_prod_test.rs b/lambda-integration-tests/tests/tenant_id_prod_test.rs new file mode 100644 index 000000000..19c4e7c88 --- /dev/null +++ b/lambda-integration-tests/tests/tenant_id_prod_test.rs @@ -0,0 +1,57 @@ +use serde_json::json; + +#[test] +fn test_tenant_id_functionality_in_production() { + let function_name = + std::env::var("TENANT_ID_TEST_FUNCTION").expect("TENANT_ID_TEST_FUNCTION environment variable not set"); + + // Test with tenant ID + let payload_with_tenant = json!({ + "test": "tenant_id_test", + "message": "Testing with tenant ID" + }); + + let output = std::process::Command::new("aws") + .args([ + "lambda", + "invoke", + "--function-name", + &function_name, + "--payload", + &payload_with_tenant.to_string(), + "--cli-binary-format", + "raw-in-base64-out", + "/tmp/tenant_response.json", + ]) + .output() + .expect("Failed to invoke Lambda function"); + + assert!( + output.status.success(), + "Lambda invocation failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + // Read and verify response + let response = std::fs::read_to_string("/tmp/tenant_response.json").expect("Failed to read response file"); + + let response_json: serde_json::Value = serde_json::from_str(&response).expect("Failed to parse response JSON"); + + // Verify the function executed successfully + assert_eq!(response_json["statusCode"], 200); + + // Parse the body to check tenant_id field exists (even if null) + let body: serde_json::Value = + serde_json::from_str(response_json["body"].as_str().expect("Body should be a string")) + .expect("Failed to parse body JSON"); + + assert!( + body.get("tenant_id").is_some(), + "tenant_id field should be present in response" + ); + assert!(body.get("request_id").is_some(), "request_id should be present"); + assert_eq!(body["message"], "Tenant ID test successful"); + + println!("✅ Tenant ID production test passed"); + println!("Response: {}", serde_json::to_string_pretty(&response_json).unwrap()); +} diff --git a/lambda-runtime-api-client/CHANGELOG.md b/lambda-runtime-api-client/CHANGELOG.md new file mode 100644 index 000000000..f912e1c8d --- /dev/null +++ b/lambda-runtime-api-client/CHANGELOG.md @@ -0,0 +1,55 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.1.1](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_runtime_api_client-v1.1.0...lambda_runtime_api_client-v1.1.1) - 2026-09-02 + +### Other + +- release ([#1151](https://github.com/aws/aws-lambda-rust-runtime/pull/1151)) + +## [1.1.0](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_runtime_api_client-v1.0.3...lambda_runtime_api_client-v1.1.0) - 2026-07-09 + +### Added + +- add SnapStart support via SnapStartResource trait ([#1150](https://github.com/aws/aws-lambda-rust-runtime/pull/1150)) + +### Added + +- `PooledClient`: a Runtime API client that can rebuild its connection pool after a + SnapStart VM restore via `reset_pool()`, so stale connections to the Runtime API + (RAPID) are discarded. Implemented lock-free with `OnceLock`; `call()` has no + hot-path lock. Construct via `PooledClient::builder().build()`. +- `PooledClientBuilder`, returned by `PooledClient::builder()`. +- `RuntimeApiClient` trait abstracting `call`, implemented by both `Client` and + `PooledClient`. + +### Deprecated + +- `Client` and `ClientBuilder::build` are superseded by `PooledClient` / + `PooledClient::builder().build()`. `Client` is unchanged and fully functional; it + simply cannot reset its connection pool after a SnapStart restore. They are + documented as superseded but do **not** carry a `#[deprecated]` attribute, so no + compiler warnings are emitted; they are expected to be removed in the next major + version (2.0.0). This keeps the crate non-breaking (no major version bump). + +## [1.0.3](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_runtime_api_client-v1.0.2...lambda_runtime_api_client-v1.0.3) - 2026-03-19 + +### Other + +- release ([#1118](https://github.com/aws/aws-lambda-rust-runtime/pull/1118)) + +## [1.0.2](https://github.com/aws/aws-lambda-rust-runtime/compare/v1.0.2...lambda_runtime_api_client-v1.0.2) - 2026-01-06 + +### Added + +- *(lambda-managed-instances)* API client connection pooling for concurrent requests ([#1067](https://github.com/aws/aws-lambda-rust-runtime/pull/1067)) + +### Changed + +- MSRV updated from 1.82.0 to 1.84.0, enabled MSRV-aware resolver ([#1078](https://github.com/aws/aws-lambda-rust-runtime/pull/1078)) diff --git a/lambda-runtime-api-client/Cargo.toml b/lambda-runtime-api-client/Cargo.toml index dab52a48b..d1fb622c9 100644 --- a/lambda-runtime-api-client/Cargo.toml +++ b/lambda-runtime-api-client/Cargo.toml @@ -1,20 +1,40 @@ [package] name = "lambda_runtime_api_client" -version = "0.7.0" +version = "1.1.1" edition = "2021" +rust-version = "1.84.0" authors = [ "David Calavera ", - "Harold Sun " + "Harold Sun ", ] description = "AWS Lambda Runtime interaction API" license = "Apache-2.0" -repository = "https://github.com/awslabs/aws-lambda-rust-runtime" +repository = "https://github.com/aws/aws-lambda-rust-runtime" categories = ["web-programming::http-server"] keywords = ["AWS", "Lambda", "API"] readme = "README.md" +[features] +default = ["tracing"] +tracing = ["dep:tracing", "dep:tracing-subscriber"] + [dependencies] -http = "0.2" -hyper = { version = "0.14.20", features = ["http1", "client", "stream", "tcp"] } -tower-service = "0.3" -tokio = { version = "1.0", features = ["io-util"] } +bytes = { workspace = true } +futures-channel = { workspace = true } +futures-util = { workspace = true } +http = { workspace = true } +http-body = { workspace = true } +http-body-util = { workspace = true } +hyper = { workspace = true, features = ["http1", "client"] } +hyper-util = { workspace = true, features = [ + "client", + "client-legacy", + "http1", + "tokio", +] } +tower = { workspace = true, features = ["util"] } +tracing = { version = "0.1", features = ["log"], optional = true } +tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "json", "env-filter"], optional = true } + +[package.metadata.docs.rs] +all-features = true diff --git a/lambda-runtime-api-client/README.md b/lambda-runtime-api-client/README.md index 2251cbc7c..530fefdd5 100644 --- a/lambda-runtime-api-client/README.md +++ b/lambda-runtime-api-client/README.md @@ -32,4 +32,4 @@ async fn main() -> Result<(), Error> { client.call(request).await } -``` \ No newline at end of file +``` diff --git a/lambda-runtime-api-client/src/body/channel.rs b/lambda-runtime-api-client/src/body/channel.rs new file mode 100644 index 000000000..f1e094c3c --- /dev/null +++ b/lambda-runtime-api-client/src/body/channel.rs @@ -0,0 +1,108 @@ +//! Body::channel utilities. Extracted from Hyper under MIT license. +//! + +use std::{ + pin::Pin, + task::{Context, Poll}, +}; + +use crate::body::{sender, watch}; +use bytes::Bytes; +use futures_channel::{mpsc, oneshot}; +use futures_util::{stream::FusedStream, Future, Stream}; +use http::HeaderMap; +use http_body::{Body, Frame, SizeHint}; +pub use sender::Sender; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) struct DecodedLength(u64); + +impl DecodedLength { + pub(crate) const CLOSE_DELIMITED: DecodedLength = DecodedLength(u64::MAX); + pub(crate) const CHUNKED: DecodedLength = DecodedLength(u64::MAX - 1); + pub(crate) const ZERO: DecodedLength = DecodedLength(0); + + pub(crate) fn sub_if(&mut self, amt: u64) { + match *self { + DecodedLength::CHUNKED | DecodedLength::CLOSE_DELIMITED => (), + DecodedLength(ref mut known) => { + *known -= amt; + } + } + } + + /// Converts to an `Option` representing a Known or Unknown length. + pub(crate) fn into_opt(self) -> Option { + match self { + DecodedLength::CHUNKED | DecodedLength::CLOSE_DELIMITED => None, + DecodedLength(known) => Some(known), + } + } +} + +pub struct ChannelBody { + content_length: DecodedLength, + want_tx: watch::Sender, + data_rx: mpsc::Receiver>, + trailers_rx: oneshot::Receiver, +} + +pub fn channel() -> (Sender, ChannelBody) { + let (data_tx, data_rx) = mpsc::channel(0); + let (trailers_tx, trailers_rx) = oneshot::channel(); + + let (want_tx, want_rx) = watch::channel(sender::WANT_READY); + + let tx = Sender { + want_rx, + data_tx, + trailers_tx: Some(trailers_tx), + }; + let rx = ChannelBody { + content_length: DecodedLength::CHUNKED, + want_tx, + data_rx, + trailers_rx, + }; + + (tx, rx) +} + +impl Body for ChannelBody { + type Data = Bytes; + type Error = crate::Error; + + fn poll_frame( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + self.want_tx.send(sender::WANT_READY); + + if !self.data_rx.is_terminated() { + if let Some(chunk) = ready!(Pin::new(&mut self.data_rx).poll_next(cx)?) { + self.content_length.sub_if(chunk.len() as u64); + return Poll::Ready(Some(Ok(Frame::data(chunk)))); + } + } + + // check trailers after data is terminated + match ready!(Pin::new(&mut self.trailers_rx).poll(cx)) { + Ok(t) => Poll::Ready(Some(Ok(Frame::trailers(t)))), + Err(_) => Poll::Ready(None), + } + } + + fn is_end_stream(&self) -> bool { + self.content_length == DecodedLength::ZERO + } + + fn size_hint(&self) -> SizeHint { + let mut hint = SizeHint::default(); + + if let Some(content_length) = self.content_length.into_opt() { + hint.set_exact(content_length); + } + + hint + } +} diff --git a/lambda-runtime-api-client/src/body/mod.rs b/lambda-runtime-api-client/src/body/mod.rs new file mode 100644 index 000000000..13bfcaa03 --- /dev/null +++ b/lambda-runtime-api-client/src/body/mod.rs @@ -0,0 +1,145 @@ +//! HTTP body utilities. Extracted from Axum under MIT license. +//! + +use crate::{BoxError, Error}; +use bytes::Bytes; +use futures_util::stream::Stream; +use http_body::{Body as _, Frame}; +use http_body_util::{BodyExt, Collected}; +use std::{ + pin::Pin, + task::{Context, Poll}, +}; + +use self::channel::Sender; + +macro_rules! ready { + ($e:expr) => { + match $e { + std::task::Poll::Ready(v) => v, + std::task::Poll::Pending => return std::task::Poll::Pending, + } + }; +} + +mod channel; +pub mod sender; +mod watch; + +type BoxBody = http_body_util::combinators::UnsyncBoxBody; + +fn boxed(body: B) -> BoxBody +where + B: http_body::Body + Send + 'static, + B::Error: Into, +{ + try_downcast(body).unwrap_or_else(|body| body.map_err(Error::new).boxed_unsync()) +} + +pub(crate) fn try_downcast(k: K) -> Result +where + T: 'static, + K: Send + 'static, +{ + let mut k = Some(k); + if let Some(k) = ::downcast_mut::>(&mut k) { + Ok(k.take().unwrap()) + } else { + Err(k.unwrap()) + } +} + +/// The body type used in axum requests and responses. +#[derive(Debug)] +pub struct Body(BoxBody); + +impl Body { + /// Create a new `Body` that wraps another [`http_body::Body`]. + pub fn new(body: B) -> Self + where + B: http_body::Body + Send + 'static, + B::Error: Into, + { + try_downcast(body).unwrap_or_else(|body| Self(boxed(body))) + } + + /// Create an empty body. + pub fn empty() -> Self { + Self::new(http_body_util::Empty::new()) + } + + /// Create a new `Body` stream with associated Sender half. + pub fn channel() -> (Sender, Body) { + let (sender, body) = channel::channel(); + (sender, Body::new(body)) + } + + /// Collect the body into `Bytes` + pub async fn collect(self) -> Result, Error> { + self.0.collect().await + } +} + +impl Default for Body { + fn default() -> Self { + Self::empty() + } +} + +macro_rules! body_from_impl { + ($ty:ty) => { + impl From<$ty> for Body { + fn from(buf: $ty) -> Self { + Self::new(http_body_util::Full::from(buf)) + } + } + }; +} + +body_from_impl!(&'static [u8]); +body_from_impl!(std::borrow::Cow<'static, [u8]>); +body_from_impl!(Vec); + +body_from_impl!(&'static str); +body_from_impl!(std::borrow::Cow<'static, str>); +body_from_impl!(String); + +body_from_impl!(Bytes); + +impl http_body::Body for Body { + type Data = Bytes; + type Error = Error; + + #[inline] + fn poll_frame( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + Pin::new(&mut self.0).poll_frame(cx) + } + + #[inline] + fn size_hint(&self) -> http_body::SizeHint { + self.0.size_hint() + } + + #[inline] + fn is_end_stream(&self) -> bool { + self.0.is_end_stream() + } +} + +impl Stream for Body { + type Item = Result; + + #[inline] + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match futures_util::ready!(Pin::new(&mut self).poll_frame(cx)?) { + Some(frame) => match frame.into_data() { + Ok(data) => Poll::Ready(Some(Ok(data))), + Err(_frame) => Poll::Ready(None), + }, + None => Poll::Ready(None), + } + } +} diff --git a/lambda-runtime-api-client/src/body/sender.rs b/lambda-runtime-api-client/src/body/sender.rs new file mode 100644 index 000000000..14c1d918e --- /dev/null +++ b/lambda-runtime-api-client/src/body/sender.rs @@ -0,0 +1,135 @@ +//! Body::channel utilities. Extracted from Hyper under MIT license. +//! + +use crate::Error; +use std::task::{Context, Poll}; + +use bytes::Bytes; +use futures_channel::{mpsc, oneshot}; +use http::HeaderMap; + +use super::watch; + +type BodySender = mpsc::Sender>; +type TrailersSender = oneshot::Sender; + +pub(crate) const WANT_PENDING: usize = 1; +pub(crate) const WANT_READY: usize = 2; + +/// A sender half created through [`Body::channel()`]. +/// +/// Useful when wanting to stream chunks from another thread. +/// +/// ## Body Closing +/// +/// Note that the request body will always be closed normally when the sender is dropped (meaning +/// that the empty terminating chunk will be sent to the remote). If you desire to close the +/// connection with an incomplete response (e.g. in the case of an error during asynchronous +/// processing), call the [`Sender::abort()`] method to abort the body in an abnormal fashion. +/// +/// [`Body::channel()`]: struct.Body.html#method.channel +/// [`Sender::abort()`]: struct.Sender.html#method.abort +#[must_use = "Sender does nothing unless sent on"] +pub struct Sender { + pub(crate) want_rx: watch::Receiver, + pub(crate) data_tx: BodySender, + pub(crate) trailers_tx: Option, +} + +impl Sender { + /// Check to see if this `Sender` can send more data. + pub fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + // Check if the receiver end has tried polling for the body yet + ready!(self.poll_want(cx)?); + self.data_tx + .poll_ready(cx) + .map_err(|_| Error::new(SenderError::ChannelClosed)) + } + + fn poll_want(&mut self, cx: &mut Context<'_>) -> Poll> { + match self.want_rx.load(cx) { + WANT_READY => Poll::Ready(Ok(())), + WANT_PENDING => Poll::Pending, + watch::CLOSED => Poll::Ready(Err(Error::new(SenderError::ChannelClosed))), + unexpected => unreachable!("want_rx value: {}", unexpected), + } + } + + async fn ready(&mut self) -> Result<(), Error> { + futures_util::future::poll_fn(|cx| self.poll_ready(cx)).await + } + + /// Send data on data channel when it is ready. + #[allow(unused)] + pub async fn send_data(&mut self, chunk: Bytes) -> Result<(), Error> { + self.ready().await?; + self.data_tx + .try_send(Ok(chunk)) + .map_err(|_| Error::new(SenderError::ChannelClosed)) + } + + /// Send trailers on trailers channel. + #[allow(unused)] + pub async fn send_trailers(&mut self, trailers: HeaderMap) -> Result<(), Error> { + let tx = match self.trailers_tx.take() { + Some(tx) => tx, + None => return Err(Error::new(SenderError::ChannelClosed)), + }; + tx.send(trailers).map_err(|_| Error::new(SenderError::ChannelClosed)) + } + + /// Try to send data on this channel. + /// + /// # Errors + /// + /// Returns `Err(Bytes)` if the channel could not (currently) accept + /// another `Bytes`. + /// + /// # Note + /// + /// This is mostly useful for when trying to send from some other thread + /// that doesn't have an async context. If in an async context, prefer + /// `send_data()` instead. + pub fn try_send_data(&mut self, chunk: Bytes) -> Result<(), Bytes> { + self.data_tx + .try_send(Ok(chunk)) + .map_err(|err| err.into_inner().expect("just sent Ok")) + } + + /// Send a `SenderError::BodyWriteAborted` error and terminate the stream. + #[allow(unused)] + pub fn abort(mut self) { + self.send_error(Error::new(SenderError::BodyWriteAborted)); + } + + /// Terminate the stream with an error. + pub fn send_error(&mut self, err: Error) { + let _ = self + .data_tx + // clone so the send works even if buffer is full + .clone() + .try_send(Err(err)); + } +} + +#[derive(Debug)] +enum SenderError { + ChannelClosed, + BodyWriteAborted, +} + +impl SenderError { + fn description(&self) -> &str { + match self { + SenderError::BodyWriteAborted => "user body write aborted", + SenderError::ChannelClosed => "channel closed", + } + } +} + +impl std::fmt::Display for SenderError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.description()) + } +} +impl std::error::Error for SenderError {} diff --git a/lambda-runtime-api-client/src/body/watch.rs b/lambda-runtime-api-client/src/body/watch.rs new file mode 100644 index 000000000..f31f4f273 --- /dev/null +++ b/lambda-runtime-api-client/src/body/watch.rs @@ -0,0 +1,71 @@ +//! Body::channel utilities. Extracted from Hyper under MIT license. +//! + +//! An SPSC broadcast channel. +//! +//! - The value can only be a `usize`. +//! - The consumer is only notified if the value is different. +//! - The value `0` is reserved for closed. + +use futures_util::task::AtomicWaker; +use std::{ + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, + task, +}; + +type Value = usize; + +pub(crate) const CLOSED: usize = 0; + +pub(crate) fn channel(initial: Value) -> (Sender, Receiver) { + debug_assert!(initial != CLOSED, "watch::channel initial state of 0 is reserved"); + + let shared = Arc::new(Shared { + value: AtomicUsize::new(initial), + waker: AtomicWaker::new(), + }); + + (Sender { shared: shared.clone() }, Receiver { shared }) +} + +pub(crate) struct Sender { + shared: Arc, +} + +pub(crate) struct Receiver { + shared: Arc, +} + +struct Shared { + value: AtomicUsize, + waker: AtomicWaker, +} + +impl Sender { + pub(crate) fn send(&mut self, value: Value) { + if self.shared.value.swap(value, Ordering::SeqCst) != value { + self.shared.waker.wake(); + } + } +} + +impl Drop for Sender { + fn drop(&mut self) { + self.send(CLOSED); + } +} + +impl Receiver { + pub(crate) fn load(&mut self, cx: &mut task::Context<'_>) -> Value { + self.shared.waker.register(cx.waker()); + self.shared.value.load(Ordering::SeqCst) + } + + #[allow(unused)] + pub(crate) fn peek(&self) -> Value { + self.shared.value.load(Ordering::Relaxed) + } +} diff --git a/lambda-runtime-api-client/src/error.rs b/lambda-runtime-api-client/src/error.rs new file mode 100644 index 000000000..d8ff30b22 --- /dev/null +++ b/lambda-runtime-api-client/src/error.rs @@ -0,0 +1,33 @@ +//! Extracted from Axum under MIT license. +//! +use std::{error::Error as StdError, fmt}; +pub use tower::BoxError; +/// Errors that can happen when using axum. +#[derive(Debug)] +pub struct Error { + inner: BoxError, +} + +impl Error { + /// Create a new `Error` from a boxable error. + pub fn new(error: impl Into) -> Self { + Self { inner: error.into() } + } + + /// Convert an `Error` back into the underlying boxed trait object. + pub fn into_inner(self) -> BoxError { + self.inner + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.inner.fmt(f) + } +} + +impl StdError for Error { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + Some(&*self.inner) + } +} diff --git a/lambda-runtime-api-client/src/lib.rs b/lambda-runtime-api-client/src/lib.rs index 253d561c2..ead8abe75 100644 --- a/lambda-runtime-api-client/src/lib.rs +++ b/lambda-runtime-api-client/src/lib.rs @@ -1,109 +1,217 @@ #![deny(clippy::all, clippy::cargo)] #![warn(missing_docs, nonstandard_style, rust_2018_idioms)] -#![warn(clippy::multiple_crate_versions)] +#![allow(clippy::multiple_crate_versions)] +#![cfg_attr(docsrs, feature(doc_cfg))] //! This crate includes a base HTTP client to interact with //! the AWS Lambda Runtime API. -use http::{uri::PathAndQuery, uri::Scheme, Request, Response, Uri}; -use hyper::{ - client::{connect::Connection, HttpConnector}, - Body, +use futures_util::{future::BoxFuture, FutureExt, TryFutureExt}; +use http::{ + uri::{PathAndQuery, Scheme}, + Request, Response, Uri, }; -use std::{convert::TryInto, fmt::Debug}; -use tokio::io::{AsyncRead, AsyncWrite}; -use tower_service::Service; +use hyper::body::Incoming; +use hyper_util::client::legacy::connect::HttpConnector; +use std::{convert::TryInto, fmt::Debug, future, sync::OnceLock}; const USER_AGENT_HEADER: &str = "User-Agent"; const DEFAULT_USER_AGENT: &str = concat!("aws-lambda-rust/", env!("CARGO_PKG_VERSION")); const CUSTOM_USER_AGENT: Option<&str> = option_env!("LAMBDA_RUNTIME_USER_AGENT"); -/// Error type that lambdas may result in -pub type Error = Box; +mod error; +pub use error::*; +pub mod body; + +#[cfg(feature = "tracing")] +#[cfg_attr(docsrs, doc(cfg(feature = "tracing")))] +pub mod tracing; + +/// The single method the runtime needs from any client: send a request to the +/// Lambda Runtime API (RAPID). Implemented by both [`Client`] and [`PooledClient`]. +pub trait RuntimeApiClient { + /// Send a given request to the Runtime API. + fn call(&self, req: Request) -> BoxFuture<'static, Result, BoxError>>; +} /// API client to interact with the AWS Lambda Runtime API. +/// +/// **Superseded by [`PooledClient`]**, which additionally supports rebuilding its +/// connection pool after a SnapStart VM restore via [`PooledClient::reset_pool`]. +/// `Client` remains fully functional; it simply cannot reset its pool after a +/// restore. Prefer [`PooledClient`] for new code; `Client` is expected to be +/// removed in the next major version (2.0.0). #[derive(Debug)] -pub struct Client { +pub struct Client { /// The runtime API URI pub base: Uri, /// The client that manages the API connections - pub client: hyper::Client, + pub client: hyper_util::client::legacy::Client, } impl Client { /// Create a builder struct to configure the client. - pub fn builder() -> ClientBuilder { - ClientBuilder { - connector: HttpConnector::new(), - uri: None, + pub fn builder() -> ClientBuilder { + ClientBuilder::new() + } + + /// Send a given request to the Runtime API. + /// Use the client's base URI to ensure the API endpoint is correct. + pub fn call(&self, req: Request) -> BoxFuture<'static, Result, BoxError>> { + ::call(self, req) + } + + /// Create a new client with a given base URI, HTTP connector, and optional pool size hint. + fn with(base: Uri, connector: HttpConnector, pool_size: Option) -> Self { + Self { + base, + client: build_hyper_client(&connector, pool_size), } } } -impl Client -where - C: hyper::client::connect::Connect + Sync + Send + Clone + 'static, -{ +impl RuntimeApiClient for Client { + fn call(&self, req: Request) -> BoxFuture<'static, Result, BoxError>> { + // NOTE: This method returns a boxed future such that the future has a static lifetime. + // Due to limitations around the Rust async implementation as of Mar 2024, this is + // required to minimize constraints on the handler passed to [lambda_runtime::run]. + let req = match set_origin(&self.base, req) { + Ok(req) => req, + Err(err) => return future::ready(Err(err)).boxed(), + }; + self.client.request(req).map_err(Into::into).boxed() + } +} + +/// API client to interact with the AWS Lambda Runtime API, with support for +/// rebuilding its connection pool after a SnapStart VM restore. +#[derive(Debug)] +pub struct PooledClient { + /// The runtime API URI + pub base: Uri, + /// The client that manages the API connections + client: hyper_util::client::legacy::Client, + /// The HTTP connector used to rebuild the client after a SnapStart restore + connector: HttpConnector, + /// Optional pool size hint for the hyper client + pool_size: Option, + /// Holds a freshly built client after a SnapStart restore. When set, [`PooledClient::call`] + /// uses it instead of [`PooledClient::client`]; otherwise the original client is used. + /// + /// This is populated exactly once by [`PooledClient::reset_pool`] during the restore + /// lifecycle, before any concurrent polling starts, so the `OnceLock` write never + /// races with reads. Using `OnceLock` keeps `call` lock-free on the hot path. + restored: OnceLock>, +} + +impl PooledClient { + /// Create a builder struct to configure the client. + pub fn builder() -> PooledClientBuilder { + PooledClientBuilder::new() + } + /// Send a given request to the Runtime API. /// Use the client's base URI to ensure the API endpoint is correct. - pub async fn call(&self, req: Request) -> Result, Error> { - let req = self.set_origin(req)?; - let response = self.client.request(req).await?; - Ok(response) + pub fn call(&self, req: Request) -> BoxFuture<'static, Result, BoxError>> { + ::call(self, req) } - /// Create a new client with a given base URI and HTTP connector. - pub fn with(base: Uri, connector: C) -> Self { - let client = hyper::Client::builder().build(connector); - Self { base, client } + /// Create a new client with a given base URI, HTTP connector, and optional pool size hint. + fn with(base: Uri, connector: HttpConnector, pool_size: Option) -> Self { + let client = build_hyper_client(&connector, pool_size); + Self { + base, + client, + connector, + pool_size, + restored: OnceLock::new(), + } } - fn set_origin(&self, req: Request) -> Result, Error> { - let (mut parts, body) = req.into_parts(); - let (scheme, authority, base_path) = { - let scheme = self.base.scheme().unwrap_or(&Scheme::HTTP); - let authority = self.base.authority().expect("Authority not found"); - let base_path = self.base.path().trim_end_matches('/'); - (scheme, authority, base_path) + /// Reset the internal connection pool by building a fresh hyper client that + /// subsequent calls will use. + /// + /// This is useful after a SnapStart VM restore where existing connections + /// to the Lambda Runtime API (RAPID) are stale and must be discarded. It is + /// called exactly once during the restore lifecycle, before concurrent + /// polling starts, so the underlying `OnceLock` write never races. + pub fn reset_pool(&self) { + let _ = self.restored.set(build_hyper_client(&self.connector, self.pool_size)); + } +} + +impl RuntimeApiClient for PooledClient { + fn call(&self, req: Request) -> BoxFuture<'static, Result, BoxError>> { + let req = match set_origin(&self.base, req) { + Ok(req) => req, + Err(err) => return future::ready(Err(err)).boxed(), }; - let path = parts.uri.path_and_query().expect("PathAndQuery not found"); - let pq: PathAndQuery = format!("{}{}", base_path, path).parse().expect("PathAndQuery invalid"); + // After a SnapStart restore, `restored` holds a fresh client with no stale + // connections; otherwise fall back to the original client. Lock-free read. + let hyper_client = self.restored.get().unwrap_or(&self.client); + hyper_client.request(req).map_err(Into::into).boxed() + } +} - let uri = Uri::builder() - .scheme(scheme.as_ref()) - .authority(authority.as_ref()) - .path_and_query(pq) - .build() - .map_err(Box::new)?; +/// Build a fresh hyper client from the given connector and pool size settings. +fn build_hyper_client( + connector: &HttpConnector, + pool_size: Option, +) -> hyper_util::client::legacy::Client { + let mut builder = hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()); + builder.http1_max_buf_size(1024 * 1024); - parts.uri = uri; - Ok(Request::from_parts(parts, body)) + if let Some(size) = pool_size { + builder.pool_max_idle_per_host(size); } + + builder.build(connector.clone()) +} + +/// Rewrite a request's URI against the client's base (scheme/authority/base path). +fn set_origin(base: &Uri, req: Request) -> Result, BoxError> { + let (mut parts, body) = req.into_parts(); + let (scheme, authority, base_path) = { + let scheme = base.scheme().unwrap_or(&Scheme::HTTP); + let authority = base.authority().expect("Authority not found"); + let base_path = base.path().trim_end_matches('/'); + (scheme, authority, base_path) + }; + let path = parts.uri.path_and_query().expect("PathAndQuery not found"); + let pq: PathAndQuery = format!("{base_path}{path}").parse().expect("PathAndQuery invalid"); + + let uri = Uri::builder() + .scheme(scheme.as_ref()) + .authority(authority.as_ref()) + .path_and_query(pq) + .build() + .map_err(Box::new)?; + + parts.uri = uri; + Ok(Request::from_parts(parts, body)) } /// Builder implementation to construct any Runtime API clients. -pub struct ClientBuilder = hyper::client::HttpConnector> { - connector: C, +pub struct ClientBuilder { + connector: HttpConnector, uri: Option, + pool_size: Option, } -impl ClientBuilder -where - C: Service + Clone + Send + Sync + Unpin + 'static, - >::Future: Unpin + Send, - >::Error: Into>, - >::Response: AsyncRead + AsyncWrite + Connection + Unpin + Send + 'static, -{ +impl ClientBuilder { + fn new() -> ClientBuilder { + ClientBuilder { + connector: HttpConnector::new(), + uri: None, + pool_size: None, + } + } + /// Create a new builder with a given HTTP connector. - pub fn with_connector(self, connector: C2) -> ClientBuilder - where - C2: Service + Clone + Send + Sync + Unpin + 'static, - >::Future: Unpin + Send, - >::Error: Into>, - >::Response: AsyncRead + AsyncWrite + Connection + Unpin + Send + 'static, - { + pub fn with_connector(self, connector: HttpConnector) -> ClientBuilder { ClientBuilder { connector, uri: self.uri, + pool_size: self.pool_size, } } @@ -113,16 +221,83 @@ where Self { uri: Some(uri), ..self } } + /// Provide a pool size hint for the underlying Hyper client. + /// + /// When using concurrent polling, this should be at least the maximum + /// concurrency (e.g., `AWS_LAMBDA_MAX_CONCURRENCY`) to avoid connection + /// starvation. + pub fn with_pool_size(self, pool_size: usize) -> Self { + Self { + pool_size: Some(pool_size), + ..self + } + } + /// Create the new client to interact with the Runtime API. - pub fn build(self) -> Result, Error> { - let uri = match self.uri { - Some(uri) => uri, - None => { - let uri = std::env::var("AWS_LAMBDA_RUNTIME_API").expect("Missing AWS_LAMBDA_RUNTIME_API env var"); - uri.try_into().expect("Unable to convert to URL") - } - }; - Ok(Client::with(uri, self.connector)) + /// + /// **Superseded by [`PooledClient::builder`]**, which produces a + /// [`PooledClient`] that supports SnapStart connection-pool reset. Prefer + /// `PooledClient::builder().build()` for new code; this method is expected to + /// be removed in the next major version (2.0.0). + pub fn build(self) -> Result { + let uri = resolve_uri(self.uri); + Ok(Client::with(uri, self.connector, self.pool_size)) + } +} + +/// Builder to construct a [`PooledClient`]. +pub struct PooledClientBuilder { + connector: HttpConnector, + uri: Option, + pool_size: Option, +} + +impl PooledClientBuilder { + fn new() -> PooledClientBuilder { + PooledClientBuilder { + connector: HttpConnector::new(), + uri: None, + pool_size: None, + } + } + + /// Use a given HTTP connector. + pub fn with_connector(self, connector: HttpConnector) -> PooledClientBuilder { + PooledClientBuilder { connector, ..self } + } + + /// Use a given base URI. Inherits all other attributes from the existing builder. + pub fn with_endpoint(self, uri: http::Uri) -> Self { + Self { uri: Some(uri), ..self } + } + + /// Provide a pool size hint for the underlying Hyper client. + /// + /// When using concurrent polling, this should be at least the maximum + /// concurrency (e.g., `AWS_LAMBDA_MAX_CONCURRENCY`) to avoid connection + /// starvation. + pub fn with_pool_size(self, pool_size: usize) -> Self { + Self { + pool_size: Some(pool_size), + ..self + } + } + + /// Create the new [`PooledClient`] to interact with the Runtime API. + pub fn build(self) -> PooledClient { + let uri = resolve_uri(self.uri); + PooledClient::with(uri, self.connector, self.pool_size) + } +} + +/// Resolve the base URI from an explicit value or the `AWS_LAMBDA_RUNTIME_API` env var. +fn resolve_uri(uri: Option) -> Uri { + match uri { + Some(uri) => uri, + None => { + let uri = std::env::var("AWS_LAMBDA_RUNTIME_API").expect("Missing AWS_LAMBDA_RUNTIME_API env var"); + uri.try_into().expect("Unable to convert to URL") + } } } @@ -145,13 +320,12 @@ mod tests { #[test] fn test_set_origin() { - let base = "http://localhost:9001"; - let client = Client::builder().with_endpoint(base.parse().unwrap()).build().unwrap(); + let base: Uri = "http://localhost:9001".parse().unwrap(); let req = build_request() .uri("/2018-06-01/runtime/invocation/next") .body(()) .unwrap(); - let req = client.set_origin(req).unwrap(); + let req = set_origin(&base, req).unwrap(); assert_eq!( "http://localhost:9001/2018-06-01/runtime/invocation/next", &req.uri().to_string() @@ -160,28 +334,49 @@ mod tests { #[test] fn test_set_origin_with_base_path() { - let base = "http://localhost:9001/foo"; - let client = Client::builder().with_endpoint(base.parse().unwrap()).build().unwrap(); + let base: Uri = "http://localhost:9001/foo".parse().unwrap(); let req = build_request() .uri("/2018-06-01/runtime/invocation/next") .body(()) .unwrap(); - let req = client.set_origin(req).unwrap(); + let req = set_origin(&base, req).unwrap(); assert_eq!( "http://localhost:9001/foo/2018-06-01/runtime/invocation/next", &req.uri().to_string() ); - let base = "http://localhost:9001/foo/"; - let client = Client::builder().with_endpoint(base.parse().unwrap()).build().unwrap(); + let base: Uri = "http://localhost:9001/foo/".parse().unwrap(); let req = build_request() .uri("/2018-06-01/runtime/invocation/next") .body(()) .unwrap(); - let req = client.set_origin(req).unwrap(); + let req = set_origin(&base, req).unwrap(); assert_eq!( "http://localhost:9001/foo/2018-06-01/runtime/invocation/next", &req.uri().to_string() ); } + + #[test] + fn builder_accepts_pool_size() { + let base = "http://localhost:9001"; + let expected: Uri = base.parse().unwrap(); + let client = PooledClient::builder() + .with_pool_size(4) + .with_endpoint(base.parse().unwrap()) + .build(); + + assert_eq!(client.base, expected); + } + + #[test] + fn test_reset_pool() { + let base = "http://localhost:9001"; + let client = PooledClient::builder() + .with_pool_size(4) + .with_endpoint(base.parse().unwrap()) + .build(); + client.reset_pool(); + assert_eq!(client.base, base.parse::().unwrap()); + } } diff --git a/lambda-runtime-api-client/src/tracing.rs b/lambda-runtime-api-client/src/tracing.rs new file mode 100644 index 000000000..5aa9bfa17 --- /dev/null +++ b/lambda-runtime-api-client/src/tracing.rs @@ -0,0 +1,84 @@ +//! This module provides primitives to work with `tracing` +//! and `tracing-subscriber` in Lambda functions. +//! +//! The `tracing` and `tracing-subscriber` crates are re-exported +//! so you don't have to include them as direct dependencies in +//! your projects. + +use std::{env, str::FromStr}; + +use subscriber::filter::{EnvFilter, LevelFilter}; +/// Re-export the `tracing` crate to have access to tracing macros +/// like `info!`, `debug!`, `trace!` and so on. +pub use tracing::*; + +/// Re-export the `tracing-subscriber` crate to build your own subscribers. +pub use tracing_subscriber as subscriber; +use tracing_subscriber::fmt::MakeWriter; + +const DEFAULT_LOG_LEVEL: &str = "INFO"; + +/// Initialize `tracing-subscriber` with default logging options. +/// +/// The default subscriber writes logs to STDOUT in the current context. +/// If you want to customize the writer, see [`init_default_subscriber_with_writer()`]. +/// +/// This function uses environment variables set with [Lambda's advanced logging controls](https://aws.amazon.com/blogs/compute/introducing-advanced-logging-controls-for-aws-lambda-functions/) +/// if they're configured for your function. +/// +/// This subscriber sets the logging level based on environment variables: +/// - if `AWS_LAMBDA_LOG_LEVEL` is set, it takes precedence over any other environment variables. +/// - if `AWS_LAMBDA_LOG_LEVEL` is not set, check if `RUST_LOG` is set. +/// - if none of those two variables are set, use `INFO` as the logging level. +/// +/// The logging format can also be changed based on Lambda's advanced logging controls. +/// If the `AWS_LAMBDA_LOG_FORMAT` environment variable is set to `JSON`, the log lines will be formatted as json objects, +/// otherwise they will be formatted with the default tracing format. +pub fn init_default_subscriber() { + init_default_subscriber_with_writer(std::io::stdout); +} + +/// Initialize `tracing-subscriber` with default logging options, and a custom writer. +/// +/// You might want to avoid writing to STDOUT in the local context via [`init_default_subscriber()`], if you have a high-throughput Lambdas that involve +/// a lot of async concurrency. Since, writing to STDOUT can briefly block your tokio runtime - ref [tracing #2653](https://github.com/tokio-rs/tracing/issues/2653). +/// In that case, you might prefer to use [tracing_appender::NonBlocking](https://docs.rs/tracing-appender/latest/tracing_appender/non_blocking/struct.NonBlocking.html) instead - particularly if your Lambda is fairly long-running and stays warm. +/// Though, note that you are then responsible +/// for ensuring gracefuls shutdown. See [aws-samples/graceful-shutdown-with-aws-lambda](https://github.com/aws-samples/graceful-shutdown-with-aws-lambda) for a complete example. +/// +/// This function uses environment variables set with [Lambda's advanced logging controls](https://aws.amazon.com/blogs/compute/introducing-advanced-logging-controls-for-aws-lambda-functions/) +/// if they're configured for your function. +/// +/// This subscriber sets the logging level based on environment variables: +/// - if `AWS_LAMBDA_LOG_LEVEL` is set, it takes precedence over any other environment variables. +/// - if `AWS_LAMBDA_LOG_LEVEL` is not set, check if `RUST_LOG` is set. +/// - if none of those two variables are set, use `INFO` as the logging level. +/// +/// The logging format can also be changed based on Lambda's advanced logging controls. +/// If the `AWS_LAMBDA_LOG_FORMAT` environment variable is set to `JSON`, the log lines will be formatted as json objects, +/// otherwise they will be formatted with the default tracing format. +pub fn init_default_subscriber_with_writer(writer: Writer) +where + Writer: for<'writer> MakeWriter<'writer> + Send + Sync + 'static, +{ + let log_format = env::var("AWS_LAMBDA_LOG_FORMAT").unwrap_or_default(); + let log_level_str = env::var("AWS_LAMBDA_LOG_LEVEL").or_else(|_| env::var("RUST_LOG")); + let log_level = + LevelFilter::from_str(log_level_str.as_deref().unwrap_or(DEFAULT_LOG_LEVEL)).unwrap_or(LevelFilter::INFO); + + let collector = tracing_subscriber::fmt() + .with_target(false) + .without_time() + .with_env_filter( + EnvFilter::builder() + .with_default_directive(log_level.into()) + .from_env_lossy(), + ) + .with_writer(writer); + + if log_format.eq_ignore_ascii_case("json") { + collector.json().init() + } else { + collector.init() + } +} diff --git a/lambda-runtime/CHANGELOG.md b/lambda-runtime/CHANGELOG.md new file mode 100644 index 000000000..b06473406 --- /dev/null +++ b/lambda-runtime/CHANGELOG.md @@ -0,0 +1,128 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.5.0](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_runtime-v1.4.0...lambda_runtime-v1.5.0) - 2026-09-29 + +### Added + +- log worker count and max concurrency ([#1167](https://github.com/aws/aws-lambda-rust-runtime/pull/1167)) + +### Other + +- explain AWS-LC cold-start tradeoff ([#1172](https://github.com/aws/aws-lambda-rust-runtime/pull/1172)) +- Add #[serde(default)] to `to` field of SimpleEmailCommonHeaders ([#1171](https://github.com/aws/aws-lambda-rust-runtime/pull/1171)) + +## [1.4.0](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_runtime-v1.3.0...lambda_runtime-v1.4.0) - 2026-09-02 + +### Added + +- add lambda-runtime-invocation-id header ([#1159](https://github.com/aws/aws-lambda-rust-runtime/pull/1159)) + +### Other + +- Record faas.name on Lambda OTEL spans. ([#1163](https://github.com/aws/aws-lambda-rust-runtime/pull/1163)) +- show custom event handler usage ([#1154](https://github.com/aws/aws-lambda-rust-runtime/pull/1154)) +- release ([#1151](https://github.com/aws/aws-lambda-rust-runtime/pull/1151)) + +## [1.3.0](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_runtime-v1.2.1...lambda_runtime-v1.3.0) - 2026-07-09 + +### Added + +- add SnapStart support via SnapStartResource trait ([#1150](https://github.com/aws/aws-lambda-rust-runtime/pull/1150)) + +## [1.2.1](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_runtime-v1.2.0...lambda_runtime-v1.2.1) - 2026-05-25 + +### Other + +- *(deps)* update opentelemetry-semantic-conventions requirement ([#1147](https://github.com/aws/aws-lambda-rust-runtime/pull/1147)) + +## [1.2.0](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_runtime-v1.1.3...lambda_runtime-v1.2.0) - 2026-05-08 + +### Added + +- Add VPC Lattice as an upstream source for lambda-http ([#1136](https://github.com/aws/aws-lambda-rust-runtime/pull/1136)) + +### Fixed + +- *(lambda-http)* into_api_gateway_v2_request joins cookies with "; " ([#1143](https://github.com/aws/aws-lambda-rust-runtime/pull/1143)) + +### Other + +- Added `builders` pass-through feature to enable `aws_lambda_events/builders` ([#1146](https://github.com/aws/aws-lambda-rust-runtime/pull/1146)) + +## [1.1.3](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_runtime-v1.1.2...lambda_runtime-v1.1.3) - 2026-04-16 + +### Other + +- remove non exhaustive from time structs ([#1138](https://github.com/aws/aws-lambda-rust-runtime/pull/1138)) + +## [1.1.2](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_runtime-v1.1.1...lambda_runtime-v1.1.2) - 2026-03-19 + +### Fixed + +- fix handling of null groupConfiguration ([#1130](https://github.com/aws/aws-lambda-rust-runtime/pull/1130)) + +### Other + +- release ([#1118](https://github.com/aws/aws-lambda-rust-runtime/pull/1118)) + +## [1.1.1](https://github.com/aws/aws-lambda-rust-runtime/compare/v1.0.2...lambda_runtime-v1.1.1) - 2026-03-11 + +Thank you to all the contributors who helped make this release possible. We appreciate your time, effort, and passion for the Rust Lambda community. ❤️ + +### Lambda Managed Instances + +The runtime now supports Lambda Managed Instances via the `concurrency-tokio` feature flag (previously `experimental-concurrency`). Lambda Managed Instances allow multiple concurrent requests to be processed within a single execution environment. When `AWS_LAMBDA_MAX_CONCURRENCY` is set, the runtime spawns multiple independent long-poll workers to handle concurrent invocations. If the env var is unset or <= 1, it falls back to sequential behavior automatically — so the same handler works on both classic Lambda and Lambda Managed Instances. ([#1067](https://github.com/aws/aws-lambda-rust-runtime/pull/1067)) + +```toml +[dependencies] +lambda_runtime = { version = "1.1", features = ["concurrency-tokio"] } +``` + +```rust +lambda_runtime::run_concurrent(service_fn(my_handler)).await?; +``` + +For a complete working example, see [examples/basic-lambda-concurrent](https://github.com/aws/aws-lambda-rust-runtime/tree/main/examples/basic-lambda-concurrent). For detailed guidance on building functions for multi-concurrency, including shared state patterns and database connection pools, see the [Rust runtime for Lambda Managed Instances](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances-rust.html) documentation. + +We would like also to involve the community in a broader discussion about improving our approach on multiconcurrency. You can find the discussion in ([#1120](https://github.com/aws/aws-lambda-rust-runtime/issues/1120)) + +### Added + +- *(lambda-managed-instances)* log non-2xx Lambda Runtime API responses with status and body ([#1109](https://github.com/aws/aws-lambda-rust-runtime/pull/1109)) +- tenant ID propagation for multi-tenant Lambda use cases. The `tenant_id` is available in the `context.tenant_id` field (`Option`) and is automatically extracted from the `lambda-runtime-aws-tenant-id` header when present ([#1082](https://github.com/aws/aws-lambda-rust-runtime/pull/1082)) +- Add builder pattern support for event response types ([#1090](https://github.com/aws/aws-lambda-rust-runtime/pull/1090)) + +### Fixed + +- *(test)* fix test_concurrent_structured_logging_isolation ([#1121](https://github.com/aws/aws-lambda-rust-runtime/pull/1121)) + +### Changed + +- MSRV updated from 1.82.0 to 1.84.0, enabled MSRV-aware resolver ([#1078](https://github.com/aws/aws-lambda-rust-runtime/pull/1078)) +- X-Ray trace ID now sourced from `Context` instead of environment variables ([#1067](https://github.com/aws/aws-lambda-rust-runtime/pull/1067)) + +### Other + +- *(lambda-managed-instances)* add `tokio_unstable` to known cfgs to avoid linter warns ([#1095](https://github.com/aws/aws-lambda-rust-runtime/pull/1095)) +- *(lambda-managed-instances)* verify request-ID isolation in concurrent exec ([#1086](https://github.com/aws/aws-lambda-rust-runtime/pull/1086)) +- *(lambda-managed-instances)* warn on `run()` with `AWS_LAMBDA_MAX_CONCURRENCY`, rename feature `experimental-concurrency` to `concurrency-tokio` ([#1095](https://github.com/aws/aws-lambda-rust-runtime/pull/1095)) +- Introducing Harness Testing ([#1103](https://github.com/aws/aws-lambda-rust-runtime/pull/1103)) + +## [1.1.0-rc1](https://github.com/aws/aws-lambda-rust-runtime/compare/v1.0.2...lambda_runtime-v1.1.0-rc1) - 2026-02-04 + +### Added + +- *(lambda-managed-instances)* Lambda Managed Instances support via `concurrency-tokio` feature flag ([#1067](https://github.com/aws/aws-lambda-rust-runtime/pull/1067)) +- *(lambda-managed-instances)* tenant ID propagation for multi-tenant Lambda use cases ([#1082](https://github.com/aws/aws-lambda-rust-runtime/pull/1082)) + +### Changed + +- MSRV updated from 1.82.0 to 1.84.0, enabled MSRV-aware resolver ([#1078](https://github.com/aws/aws-lambda-rust-runtime/pull/1078)) +- X-Ray trace ID now sourced from `Context` instead of environment variables ([#1067](https://github.com/aws/aws-lambda-rust-runtime/pull/1067)) diff --git a/lambda-runtime/Cargo.toml b/lambda-runtime/Cargo.toml index 14c675b66..b4876663b 100644 --- a/lambda-runtime/Cargo.toml +++ b/lambda-runtime/Cargo.toml @@ -1,33 +1,84 @@ [package] name = "lambda_runtime" -version = "0.7.0" +version = "1.5.0" authors = [ "David Calavera ", - "Harold Sun " + "Harold Sun ", ] description = "AWS Lambda Runtime" edition = "2021" +rust-version = "1.84.0" license = "Apache-2.0" -repository = "https://github.com/awslabs/aws-lambda-rust-runtime" +repository = "https://github.com/aws/aws-lambda-rust-runtime" categories = ["web-programming::http-server"] keywords = ["AWS", "Lambda", "API"] readme = "../README.md" +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)'] } + [features] -default = ["simulated"] -simulated = [] +default = ["tracing"] +tracing = ["lambda_runtime_api_client/tracing"] # enables access to the Tracing utilities +opentelemetry = ["opentelemetry-semantic-conventions"] # enables access to the OpenTelemetry layers and utilities +anyhow = ["dep:anyhow"] # enables From for Diagnostic for anyhow error types, see README.md for more info +eyre = ["dep:eyre"] # enables From for Diagnostic for eyre error types, see README.md for more info +miette = ["dep:miette"] # enables From for Diagnostic for miette error types, see README.md for more info +# TODO: remove tokio/rt and rt-multi-thread from non-feature-flagged dependencies in new breaking version, since they are unused: +# as well as default features +# https://github.com/aws/aws-lambda-rust-runtime/issues/984 +graceful-shutdown = ["tokio/rt", "tokio/signal", "dep:lambda-extension"] +concurrency-tokio = [] [dependencies] -tokio = { version = "1.0", features = ["macros", "io-util", "sync", "rt-multi-thread"] } -# Hyper requires the `server` feature to work on nightly -hyper = { version = "0.14.20", features = ["http1", "client", "stream", "server"] } -futures = "0.3" -serde = { version = "1", features = ["derive"] } -serde_json = "^1" -bytes = "1.0" -http = "0.2" +anyhow = { version = "1.0.86", optional = true } async-stream = "0.3" -tracing = { version = "0.1", features = ["log"] } -tower = { version = "0.4", features = ["util"] } +base64 = { workspace = true } +bytes = { workspace = true } +eyre = { version = "0.6.12", optional = true } +futures = { workspace = true } +http = { workspace = true } +http-body-util = { workspace = true } +http-serde = { workspace = true } +hyper = { workspace = true, features = ["http1", "client"] } +lambda-extension = { version = "1.1", path = "../lambda-extension", default-features = false, optional = true } +lambda_runtime_api_client = { version = "1.1.1", path = "../lambda-runtime-api-client", default-features = false } +miette = { version = "7.2.0", optional = true } +opentelemetry-semantic-conventions = { version = "0.32", optional = true, features = ["semconv_experimental"] } +pin-project = "1" +serde = { version = "1", features = ["derive", "rc"] } +serde_json = "^1" +serde_path_to_error = "0.1.11" +tokio = { version = "1.46", features = [ + "macros", + "io-util", + "sync", + "rt-multi-thread", +] } tokio-stream = "0.1.2" -lambda_runtime_api_client = { version = "0.7", path = "../lambda-runtime-api-client" } +tower = { workspace = true, features = ["util"] } +tracing = { version = "0.1", features = ["log"] } + +[dev-dependencies] +httpmock = "0.8.1" +hyper-util = { workspace = true, features = [ + "client", + "client-legacy", + "http1", + "server", + "server-auto", + "tokio", +] } +# pin back to pre-1.2.1 to avoid breaking rust MSRV of 1.81: +# https://github.com/hsivonen/idna_adapter/commit/f948802e3a2ae936eec51886eefbd7d536a28791 +idna_adapter = "=1.2.0" +# Self dependency to enable the graceful-shutdown feature for tests +lambda_runtime = { path = ".", features = ["tracing", "graceful-shutdown"] } +pin-project-lite = { workspace = true } +tracing-appender = "0.2" +tracing-capture = "0.1.0" +tracing-subscriber = { version = "0.3", features = ["registry"] } +tracing-test = "0.2" + +[package.metadata.docs.rs] +all-features = true diff --git a/lambda-runtime/src/constants.rs b/lambda-runtime/src/constants.rs new file mode 100644 index 000000000..98c789d0f --- /dev/null +++ b/lambda-runtime/src/constants.rs @@ -0,0 +1,9 @@ +/// Header names used in the Lambda Runtime API. +pub(crate) const LAMBDA_RUNTIME_REQUEST_ID: &str = "lambda-runtime-aws-request-id"; +pub(crate) const LAMBDA_RUNTIME_DEADLINE_MS: &str = "lambda-runtime-deadline-ms"; +pub(crate) const LAMBDA_RUNTIME_INVOKED_FUNCTION_ARN: &str = "lambda-runtime-invoked-function-arn"; +pub(crate) const LAMBDA_RUNTIME_TRACE_ID: &str = "lambda-runtime-trace-id"; +pub(crate) const LAMBDA_RUNTIME_CLIENT_CONTEXT: &str = "lambda-runtime-client-context"; +pub(crate) const LAMBDA_RUNTIME_COGNITO_IDENTITY: &str = "lambda-runtime-cognito-identity"; +pub(crate) const LAMBDA_RUNTIME_TENANT_ID: &str = "lambda-runtime-aws-tenant-id"; +pub(crate) const LAMBDA_RUNTIME_INVOCATION_ID: &str = "lambda-runtime-invocation-id"; diff --git a/lambda-runtime/src/deserializer.rs b/lambda-runtime/src/deserializer.rs new file mode 100644 index 000000000..1841c0500 --- /dev/null +++ b/lambda-runtime/src/deserializer.rs @@ -0,0 +1,43 @@ +use std::{error::Error, fmt}; + +use serde::Deserialize; + +use crate::{Context, LambdaEvent}; + +const ERROR_CONTEXT: &str = "failed to deserialize the incoming data into the function's payload type"; + +/// Event payload deserialization error. +/// Returned when the data sent to the function cannot be deserialized +/// into the type that the function receives. +#[derive(Debug)] +pub(crate) struct DeserializeError { + inner: serde_path_to_error::Error, +} + +impl fmt::Display for DeserializeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let path = self.inner.path().to_string(); + if path == "." { + writeln!(f, "{ERROR_CONTEXT}: {}", self.inner) + } else { + writeln!(f, "{ERROR_CONTEXT}: [{path}] {}", self.inner) + } + } +} + +impl Error for DeserializeError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + Some(&self.inner) + } +} + +/// Deserialize the data sent to the function into the type that the function receives. +pub(crate) fn deserialize(body: &[u8], context: Context) -> Result, DeserializeError> +where + T: for<'de> Deserialize<'de>, +{ + let jd = &mut serde_json::Deserializer::from_slice(body); + serde_path_to_error::deserialize(jd) + .map(|payload| LambdaEvent::new(payload, context)) + .map_err(|inner| DeserializeError { inner }) +} diff --git a/lambda-runtime/src/diagnostic.rs b/lambda-runtime/src/diagnostic.rs new file mode 100644 index 000000000..60917e317 --- /dev/null +++ b/lambda-runtime/src/diagnostic.rs @@ -0,0 +1,207 @@ +use serde::{Deserialize, Serialize}; +use std::any::type_name; + +use crate::{deserializer::DeserializeError, Error}; + +/// Diagnostic information about an error. +/// +/// `Diagnostic` is automatically derived for some common types, +/// like boxed types that implement [`Error`][std::error::Error]. +/// If you use an error type which comes from a external crate like anyhow, +/// you need convert it to common types like `Box`. +/// See the examples for more details. +/// +/// [`error_type`][`Diagnostic::error_type`] is derived from the type name of +/// the original error with [`std::any::type_name`] as a fallback, which may +/// not be reliable for conditional error handling. +/// +/// To get more descriptive [`error_type`][`Diagnostic::error_type`] fields, you can implement `From` for your error type. +/// That gives you full control on what the `error_type` is. +/// +/// Example: +/// ``` +/// use lambda_runtime::{Diagnostic, Error, LambdaEvent}; +/// +/// #[derive(Debug)] +/// struct ErrorResponse(&'static str); +/// +/// impl From for Diagnostic { +/// fn from(error: ErrorResponse) -> Diagnostic { +/// Diagnostic { +/// error_type: "MyError".into(), +/// error_message: error.0.to_string(), +/// } +/// } +/// } +/// +/// async fn function_handler(_event: LambdaEvent<()>) -> Result<(), ErrorResponse> { +/// Err(ErrorResponse("this is an error response")) +/// } +/// ``` +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Diagnostic { + /// `error_type` is the type of exception or error returned by the function. + /// Use this field to categorize the different kinds of errors that your function + /// might experience. + /// + /// In standard implementations, `error_type` is derived from the type name of the original error with + /// [`std::any::type_name`], however this is not descriptive enough for an error type. + /// Implement your own `Into` to return a more descriptive error type. + pub error_type: String, + /// `error_message` is a string expression of the error. + /// In standard implementations, it's the output from the [`Display`][std::fmt::Display] + /// implementation of the original error. + pub error_message: String, +} + +impl From for Diagnostic { + fn from(value: DeserializeError) -> Self { + Diagnostic { + error_type: type_name_of_val(&value), + error_message: value.to_string(), + } + } +} + +impl From for Diagnostic { + fn from(value: Error) -> Self { + Diagnostic { + error_type: type_name_of_val(&value), + error_message: value.to_string(), + } + } +} + +impl From> for Diagnostic { + fn from(value: Box) -> Self { + Diagnostic { + error_type: type_name_of_val(&value), + error_message: value.to_string(), + } + } +} + +impl From for Diagnostic { + fn from(value: std::convert::Infallible) -> Self { + Diagnostic { + error_type: type_name_of_val(&value), + error_message: value.to_string(), + } + } +} + +impl From for Diagnostic { + fn from(value: String) -> Self { + Diagnostic { + error_type: type_name_of_val(&value), + error_message: value.to_string(), + } + } +} + +impl From<&'static str> for Diagnostic { + fn from(value: &'static str) -> Self { + Diagnostic { + error_type: type_name_of_val(&value), + error_message: value.to_string(), + } + } +} + +impl From for Diagnostic { + fn from(value: std::io::Error) -> Self { + Diagnostic { + error_type: type_name_of_val(&value), + error_message: value.to_string(), + } + } +} + +#[cfg(feature = "anyhow")] +#[cfg_attr(docsrs, doc(cfg(feature = "anyhow")))] +impl From for Diagnostic { + fn from(value: anyhow::Error) -> Diagnostic { + Diagnostic { + error_type: type_name_of_val(&value), + error_message: value.to_string(), + } + } +} + +#[cfg(feature = "eyre")] +#[cfg_attr(docsrs, doc(cfg(feature = "eyre")))] +impl From for Diagnostic { + fn from(value: eyre::Report) -> Diagnostic { + Diagnostic { + error_type: type_name_of_val(&value), + error_message: value.to_string(), + } + } +} + +#[cfg(feature = "miette")] +#[cfg_attr(docsrs, doc(cfg(feature = "miette")))] +impl From for Diagnostic { + fn from(value: miette::Report) -> Diagnostic { + Diagnostic { + error_type: type_name_of_val(&value), + error_message: value.to_string(), + } + } +} + +pub(crate) fn type_name_of_val(_: T) -> String { + type_name::().into() +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn round_trip_lambda_error() { + use serde_json::{json, Value}; + let expected = json!({ + "errorType": "InvalidEventDataError", + "errorMessage": "Error parsing event data.", + }); + + let actual = Diagnostic { + error_type: "InvalidEventDataError".into(), + error_message: "Error parsing event data.".into(), + }; + let actual: Value = serde_json::to_value(actual).expect("failed to serialize diagnostic"); + assert_eq!(expected, actual); + } + + #[cfg(feature = "anyhow")] + #[test] + fn test_anyhow_integration() { + use anyhow::Error as AnyhowError; + let error: AnyhowError = anyhow::anyhow!("anyhow error"); + let diagnostic: Diagnostic = error.into(); + assert_eq!(diagnostic.error_type, "&anyhow::Error"); + assert_eq!(diagnostic.error_message, "anyhow error"); + } + + #[cfg(feature = "eyre")] + #[test] + fn test_eyre_integration() { + use eyre::Report; + let error: Report = eyre::eyre!("eyre error"); + let diagnostic: Diagnostic = error.into(); + assert_eq!(diagnostic.error_type, "&eyre::Report"); + assert_eq!(diagnostic.error_message, "eyre error"); + } + + #[cfg(feature = "miette")] + #[test] + fn test_miette_integration() { + use miette::Report; + let error: Report = miette::miette!("miette error"); + let diagnostic: Diagnostic = error.into(); + assert_eq!(diagnostic.error_type, "&miette::eyreish::Report"); + assert_eq!(diagnostic.error_message, "miette error"); + } +} diff --git a/lambda-runtime/src/layers/api_client.rs b/lambda-runtime/src/layers/api_client.rs new file mode 100644 index 000000000..d91ea78f7 --- /dev/null +++ b/lambda-runtime/src/layers/api_client.rs @@ -0,0 +1,325 @@ +// `Client` is referenced only as the default for the `C` generic parameter, kept +// for backward compatibility of the public `RuntimeApiClientService`/future types. + +use crate::LambdaInvocation; +use futures::{future::BoxFuture, ready, FutureExt, TryFutureExt}; +use hyper::body::Incoming; +use lambda_runtime_api_client::{body::Body, BoxError, Client, RuntimeApiClient}; +use pin_project::pin_project; +use std::{future::Future, pin::Pin, sync::Arc, task}; +use tower::Service; +use tracing::error; + +/// Tower service that sends a Lambda Runtime API response to the Lambda Runtime HTTP API using +/// a previously initialized client. +/// +/// This type is only meant for internal use in the Lambda runtime crate. It neither augments the +/// inner service's request type nor its error type. However, this service returns an empty +/// response `()` as the Lambda request has been completed. +pub struct RuntimeApiClientService { + inner: S, + client: Arc, +} + +impl RuntimeApiClientService { + pub fn new(inner: S, client: Arc) -> Self { + Self { inner, client } + } +} + +impl Service for RuntimeApiClientService +where + S: Service, + S::Future: Future, BoxError>>, + C: RuntimeApiClient, +{ + type Response = (); + type Error = S::Error; + type Future = RuntimeApiClientFuture; + + fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> task::Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: LambdaInvocation) -> Self::Future { + let request_fut = self.inner.call(req); + let client = self.client.clone(); + RuntimeApiClientFuture::First(request_fut, client) + } +} + +impl Clone for RuntimeApiClientService +where + S: Clone, +{ + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + client: self.client.clone(), + } + } +} + +#[pin_project(project = RuntimeApiClientFutureProj)] +pub enum RuntimeApiClientFuture { + First(#[pin] F, Arc), + Second(#[pin] BoxFuture<'static, Result, BoxError>>), +} + +impl Future for RuntimeApiClientFuture +where + F: Future, BoxError>>, + C: RuntimeApiClient, +{ + type Output = Result<(), BoxError>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll { + // NOTE: We loop here to directly poll the second future once the first has finished. + task::Poll::Ready(loop { + match self.as_mut().project() { + RuntimeApiClientFutureProj::First(fut, client) => match ready!(fut.poll(cx)) { + Ok(ok) => { + // NOTE: We use 'client.call_boxed' here to obtain a future with static + // lifetime. Otherwise, this future would need to be self-referential... + let next_fut = client + .call(ok) + .map_err(|err| { + error!(error = ?err, "failed to send request to Lambda Runtime API"); + err + }) + .boxed(); + self.set(RuntimeApiClientFuture::Second(next_fut)); + } + Err(err) => { + log_or_print!( + tracing: tracing::error!(error = ?err, "failed to build Lambda Runtime API request"), + fallback: eprintln!("failed to build Lambda Runtime API request: {err:?}") + ); + break Err(err); + } + }, + RuntimeApiClientFutureProj::Second(fut) => match ready!(fut.poll(cx)) { + Ok(resp) if !resp.status().is_success() => { + let status = resp.status(); + + // TODO + // we should consume the response body of the call in order to give a more specific message. + // https://github.com/aws/aws-lambda-rust-runtime/issues/1110 + + log_or_print!( + tracing: tracing::error!(status = %status, "Lambda Runtime API returned non-200 response"), + fallback: eprintln!("Lambda Runtime API returned non-200 response: status={status}") + ); + + // Adding more information on top of 410 Gone, to make it more clear since we cannot access the body of the message + if status == 410 { + log_or_print!( + tracing: tracing::error!( + "Lambda Runtime API rejected the response: invocation timed out or response was stale" + ), + fallback: eprintln!( + "Lambda Runtime API rejected the response: invocation timed out or response was stale" + ) + ); + } + + // Return Ok to maintain existing contract - runtime continues despite API errors + break Ok(()); + } + Ok(_) => break Ok(()), + Err(err) => { + log_or_print!( + tracing: tracing::error!(error = ?err, "Lambda Runtime API request failed"), + fallback: eprintln!("Lambda Runtime API request failed: {err:?}") + ); + break Err(err); + } + }, + } + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use http::StatusCode; + use http_body_util::Full; + use hyper::body::Bytes; + use lambda_runtime_api_client::body::Body; + use std::convert::Infallible; + use tokio::net::TcpListener; + use tracing_test::traced_test; + + async fn start_mock_server(status: StatusCode) -> String { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let url = format!("http://{}", addr); + + tokio::spawn(async move { + let (stream, _) = listener.accept().await.unwrap(); + let io = hyper_util::rt::TokioIo::new(stream); + + let service = hyper::service::service_fn(move |_req| async move { + Ok::<_, Infallible>( + http::Response::builder() + .status(status) + .body(Full::new(Bytes::from("test response"))) + .unwrap(), + ) + }); + + let _ = hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new()) + .serve_connection(io, service) + .await; + }); + + // Give the server a moment to start + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; + url + } + + #[tokio::test] + #[traced_test] + async fn test_successful_response() { + let url = start_mock_server(StatusCode::OK).await; + let client = Arc::new( + lambda_runtime_api_client::Client::builder() + .with_endpoint(url.parse().unwrap()) + .build() + .unwrap(), + ); + + let request_fut = + async { Ok::<_, BoxError>(http::Request::builder().uri("/test").body(Body::empty()).unwrap()) }; + + let future = RuntimeApiClientFuture::First(request_fut, client); + let result = future.await; + + assert!(result.is_ok()); + // No error logs should be present + assert!(!logs_contain("Lambda Runtime API returned non-200 response")); + } + + #[tokio::test] + #[traced_test] + async fn test_410_timeout_error() { + let url = start_mock_server(StatusCode::GONE).await; + let client = Arc::new( + lambda_runtime_api_client::Client::builder() + .with_endpoint(url.parse().unwrap()) + .build() + .unwrap(), + ); + + let request_fut = + async { Ok::<_, BoxError>(http::Request::builder().uri("/test").body(Body::empty()).unwrap()) }; + + let future = RuntimeApiClientFuture::First(request_fut, client); + let result = future.await; + + // Returns Ok to maintain contract, but logs the error + assert!(result.is_ok()); + + // Verify the error was logged + assert!(logs_contain("Lambda Runtime API returned non-200 response")); + assert!(logs_contain( + "Lambda Runtime API rejected the response: invocation timed out or response was stale" + )); + } + + #[tokio::test] + #[traced_test] + async fn test_500_error() { + let url = start_mock_server(StatusCode::INTERNAL_SERVER_ERROR).await; + let client = Arc::new( + lambda_runtime_api_client::Client::builder() + .with_endpoint(url.parse().unwrap()) + .build() + .unwrap(), + ); + + let request_fut = + async { Ok::<_, BoxError>(http::Request::builder().uri("/test").body(Body::empty()).unwrap()) }; + + let future = RuntimeApiClientFuture::First(request_fut, client); + let result = future.await; + + // Returns Ok to maintain contract, but logs the error + assert!(result.is_ok()); + + // Verify the error was logged with status code + assert!(logs_contain("Lambda Runtime API returned non-200 response")); + } + + #[tokio::test] + #[traced_test] + async fn test_404_error() { + let url = start_mock_server(StatusCode::NOT_FOUND).await; + let client = Arc::new( + lambda_runtime_api_client::Client::builder() + .with_endpoint(url.parse().unwrap()) + .build() + .unwrap(), + ); + + let request_fut = + async { Ok::<_, BoxError>(http::Request::builder().uri("/test").body(Body::empty()).unwrap()) }; + + let future = RuntimeApiClientFuture::First(request_fut, client); + let result = future.await; + + // Returns Ok to maintain contract, but logs the error + assert!(result.is_ok()); + + // Verify the error was logged + assert!(logs_contain("Lambda Runtime API returned non-200 response")); + } + + #[tokio::test] + #[traced_test] + async fn test_request_build_error() { + let client = Arc::new( + lambda_runtime_api_client::Client::builder() + .with_endpoint("http://localhost:9001".parse().unwrap()) + .build() + .unwrap(), + ); + + let request_fut = async { Err::, BoxError>("Request build error".into()) }; + + let future = RuntimeApiClientFuture::First(request_fut, client); + let result = future.await; + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.to_string().contains("Request build error")); + + // Verify the error was logged + assert!(logs_contain("failed to build Lambda Runtime API request")); + } + + #[tokio::test] + #[traced_test] + async fn test_network_error() { + // Use an invalid endpoint that will fail to connect + let client = Arc::new( + lambda_runtime_api_client::Client::builder() + .with_endpoint("http://127.0.0.1:1".parse().unwrap()) // Port 1 should be unreachable + .build() + .unwrap(), + ); + + let request_fut = + async { Ok::<_, BoxError>(http::Request::builder().uri("/test").body(Body::empty()).unwrap()) }; + + let future = RuntimeApiClientFuture::First(request_fut, client); + let result = future.await; + + // Network errors should propagate as Err + assert!(result.is_err()); + + // Verify the error was logged + assert!(logs_contain("Lambda Runtime API request failed")); + } +} diff --git a/lambda-runtime/src/layers/api_response.rs b/lambda-runtime/src/layers/api_response.rs new file mode 100644 index 000000000..54a2bfb1c --- /dev/null +++ b/lambda-runtime/src/layers/api_response.rs @@ -0,0 +1,289 @@ +use crate::{ + constants::LAMBDA_RUNTIME_INVOCATION_ID, + deserializer, + rate_limiter::rate_limited, + requests::{EventCompletionRequest, IntoRequest}, + runtime::LambdaInvocation, + Diagnostic, EventErrorRequest, IntoFunctionResponse, LambdaEvent, +}; +use futures::{ready, Stream}; +use lambda_runtime_api_client::{body::Body, BoxError}; +use pin_project::pin_project; +use serde::{Deserialize, Serialize}; +use std::{fmt::Debug, future::Future, marker::PhantomData, pin::Pin, task, time::Duration}; +use tower::Service; +use tracing::{error, trace, warn}; + +const MALFORMED_INVOCATION_ID_LOG_INTERVAL: Duration = Duration::from_secs(60); + +/// Tower service that turns the result or an error of a handler function into a Lambda Runtime API +/// response. +/// +/// This type is only meant for internal use in the Lambda runtime crate. The service augments both +/// inputs and outputs: the input is converted from a [LambdaInvocation] into a [LambdaEvent] +/// while any errors encountered during the conversion are turned into error responses. The service +/// outputs either a HTTP request to send to the Lambda Runtime API or a boxed error which ought to +/// be propagated to the caller to terminate the runtime. +pub struct RuntimeApiResponseService< + S, + EventPayload, + Response, + BufferedResponse, + StreamingResponse, + StreamItem, + StreamError, +> { + inner: S, + _phantom: PhantomData<( + EventPayload, + Response, + BufferedResponse, + StreamingResponse, + StreamItem, + StreamError, + )>, +} + +impl + RuntimeApiResponseService +{ + pub fn new(inner: S) -> Self { + Self { + inner, + _phantom: PhantomData, + } + } +} + +impl Clone + for RuntimeApiResponseService< + S, + EventPayload, + Response, + BufferedResponse, + StreamingResponse, + StreamItem, + StreamError, + > +where + S: Clone, +{ + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + _phantom: PhantomData, + } + } +} + +impl Service + for RuntimeApiResponseService< + S, + EventPayload, + Response, + BufferedResponse, + StreamingResponse, + StreamItem, + StreamError, + > +where + S: Service, Response = Response, Error = Diagnostic>, + EventPayload: for<'de> Deserialize<'de>, + Response: IntoFunctionResponse, + BufferedResponse: Serialize, + StreamingResponse: Stream> + Unpin + Send + 'static, + StreamItem: Into + Send, + StreamError: Into + Send + Debug, +{ + type Response = http::Request; + type Error = BoxError; + type Future = + RuntimeApiResponseFuture; + + fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> task::Poll> { + self.inner + .poll_ready(cx) + .map_err(|err| BoxError::from(format!("{}: {}", err.error_type, err.error_message))) + } + + fn call(&mut self, req: LambdaInvocation) -> Self::Future { + #[cfg(debug_assertions)] + if req.parts.status.is_server_error() { + error!("Lambda Runtime server returned an unexpected error"); + return RuntimeApiResponseFuture::Ready(Box::new(Some(Err(req.parts.status.to_string().into())))); + } + + // Utility closure to propagate potential error from conditionally executed trace + let trace_fn = || { + trace!( + body = std::str::from_utf8(&req.body)?, + "raw JSON event received from Lambda" + ); + Ok(()) + }; + if let Err(err) = trace_fn() { + error!(error = ?err, "Failed to parse raw JSON event received from Lambda. The handler will not be called. Log at TRACE level to see the payload."); + return RuntimeApiResponseFuture::Ready(Box::new(Some(Err(err)))); + }; + + let request_id = req.context.request_id.clone(); + + // The invocation ID assigned by the Lambda runtime for cross-wiring protection. + // Echoed back on `/response` and `/error` to allow RAPID to reject stale responses + // from timed-out invocations. `None` when running against older RAPID versions + // that don't send this header. + let invocation_id = match req.parts.headers.get(LAMBDA_RUNTIME_INVOCATION_ID) { + Some(value) => match value.to_str() { + Ok(value) => Some(value.to_owned()), + Err(error) => { + rate_limited!(MALFORMED_INVOCATION_ID_LOG_INTERVAL, { + warn!( + error = ?error, + rate_limit_interval_ms = MALFORMED_INVOCATION_ID_LOG_INTERVAL.as_millis(), + "Ignoring malformed Lambda runtime invocation ID header" + ); + }); + None + } + }, + None => None, + }; + + let lambda_event = match deserializer::deserialize::(&req.body, req.context) { + Ok(lambda_event) => lambda_event, + Err(err) => match build_event_error_request(request_id, invocation_id, err) { + Ok(request) => return RuntimeApiResponseFuture::Ready(Box::new(Some(Ok(request)))), + Err(err) => { + error!(error = ?err, "failed to build error response for Lambda Runtime API"); + return RuntimeApiResponseFuture::Ready(Box::new(Some(Err(err)))); + } + }, + }; + + // Once the handler input has been generated successfully, pass it through to inner services + // allowing processing both before reaching the handler function and after the handler completes. + let fut = self.inner.call(lambda_event); + RuntimeApiResponseFuture::Future(fut, request_id, invocation_id, PhantomData) + } +} + +fn build_event_error_request( + request_id: String, + invocation_id: Option, + err: T, +) -> Result, BoxError> +where + T: Into + Debug, +{ + error!(error = ?err, "Request payload deserialization into LambdaEvent failed. The handler will not be called. Log at TRACE level to see the payload."); + EventErrorRequest::new(&request_id, invocation_id.as_deref(), err).into_req() +} + +#[pin_project(project = RuntimeApiResponseFutureProj)] +pub enum RuntimeApiResponseFuture { + Future( + #[pin] F, + String, + Option, + PhantomData<( + (), + Response, + BufferedResponse, + StreamingResponse, + StreamItem, + StreamError, + )>, + ), + /// This variant is used in case the invocation fails to be processed into an event. + /// We box it to avoid bloating the size of the more likely variant, which is + /// the future that drives event processing. + Ready(Box, BoxError>>>), +} + +impl Future + for RuntimeApiResponseFuture +where + F: Future>, + Response: IntoFunctionResponse, + BufferedResponse: Serialize, + StreamingResponse: Stream> + Unpin + Send + 'static, + StreamItem: Into + Send, + StreamError: Into + Send + Debug, +{ + type Output = Result, BoxError>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll { + task::Poll::Ready(match self.as_mut().project() { + RuntimeApiResponseFutureProj::Future(fut, request_id, invocation_id, _) => match ready!(fut.poll(cx)) { + Ok(ok) => EventCompletionRequest::new(request_id, invocation_id.as_deref(), ok).into_req(), + Err(err) => EventErrorRequest::new(request_id, invocation_id.as_deref(), err).into_req(), + }, + RuntimeApiResponseFutureProj::Ready(ready) => ready.take().expect("future polled after completion"), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{constants::LAMBDA_RUNTIME_INVOCATION_ID, runtime::LambdaInvocation, Context}; + use http::{HeaderValue, Response}; + use serde_json::json; + use tower::{service_fn, Service}; + + #[tokio::test] + async fn forwards_invocation_id_from_next_response_headers() { + let mut response = Response::new(()); + response + .headers_mut() + .insert(LAMBDA_RUNTIME_INVOCATION_ID, HeaderValue::from_static("invocation-123")); + let (parts, _) = response.into_parts(); + + let mut service = RuntimeApiResponseService::new(service_fn(|_event: LambdaEvent| async { + Ok::<_, Diagnostic>(json!({"ok": true})) + })); + + let request = service + .call(LambdaInvocation { + parts, + body: bytes::Bytes::from_static(b"{}"), + context: Context::default(), + }) + .await + .expect("response request should be created"); + + assert_eq!( + request.headers().get(LAMBDA_RUNTIME_INVOCATION_ID), + Some(&HeaderValue::from_static("invocation-123")), + ); + } + + #[tokio::test] + async fn malformed_invocation_id_does_not_block_deserialization_error_response() { + let mut response = Response::new(()); + response.headers_mut().insert( + LAMBDA_RUNTIME_INVOCATION_ID, + HeaderValue::from_bytes(&[0xff]).expect("header value should accept opaque bytes"), + ); + let (parts, _) = response.into_parts(); + + let mut service = RuntimeApiResponseService::new(service_fn(|_event: LambdaEvent| async { + Ok::<_, Diagnostic>(json!({"ok": true})) + })); + + let request = service + .call(LambdaInvocation { + parts, + body: bytes::Bytes::from_static(b"{"), + context: Context { + request_id: "request-123".to_owned(), + ..Context::default() + }, + }) + .await + .expect("deserialization errors should produce an error request"); + + assert_eq!(request.uri().path(), "/2018-06-01/runtime/invocation/request-123/error",); + assert!(request.headers().get(LAMBDA_RUNTIME_INVOCATION_ID).is_none()); + } +} diff --git a/lambda-runtime/src/layers/mod.rs b/lambda-runtime/src/layers/mod.rs new file mode 100644 index 000000000..37f52f472 --- /dev/null +++ b/lambda-runtime/src/layers/mod.rs @@ -0,0 +1,19 @@ +// Internally used services. +mod api_client; +mod api_response; +mod panic; + +// Publicly available services. +/// Tracing utilities for Lambda runtime. +pub mod trace; + +pub(crate) use api_client::RuntimeApiClientService; +pub(crate) use api_response::RuntimeApiResponseService; +pub(crate) use panic::CatchPanicService; +pub use trace::TracingLayer; + +#[cfg(feature = "opentelemetry")] +mod otel; +#[cfg(feature = "opentelemetry")] +#[cfg_attr(docsrs, doc(cfg(feature = "opentelemetry")))] +pub use otel::{OpenTelemetryFaasTrigger, OpenTelemetryLayer}; diff --git a/lambda-runtime/src/layers/otel.rs b/lambda-runtime/src/layers/otel.rs new file mode 100644 index 000000000..0d874dcd6 --- /dev/null +++ b/lambda-runtime/src/layers/otel.rs @@ -0,0 +1,229 @@ +use std::{fmt::Display, future::Future, pin::Pin, task}; + +use crate::LambdaInvocation; +use opentelemetry_semantic_conventions::attribute; +use pin_project::pin_project; +use tower::{Layer, Service}; +use tracing::{field, instrument::Instrumented, Instrument}; + +/// Tower layer to add OpenTelemetry tracing to a Lambda function invocation. The layer accepts +/// a function to flush OpenTelemetry after the end of the invocation. +pub struct OpenTelemetryLayer { + flush_fn: F, + otel_attribute_trigger: OpenTelemetryFaasTrigger, +} + +impl OpenTelemetryLayer +where + F: Fn() + Clone, +{ + /// Create a new [OpenTelemetryLayer] with the provided flush function. + pub fn new(flush_fn: F) -> Self { + Self { + flush_fn, + otel_attribute_trigger: Default::default(), + } + } + + /// Configure the `faas.trigger` attribute of the OpenTelemetry span. + pub fn with_trigger(self, trigger: OpenTelemetryFaasTrigger) -> Self { + Self { + otel_attribute_trigger: trigger, + ..self + } + } +} + +impl Layer for OpenTelemetryLayer +where + F: Fn() + Clone, +{ + type Service = OpenTelemetryService; + + fn layer(&self, inner: S) -> Self::Service { + OpenTelemetryService { + inner, + flush_fn: self.flush_fn.clone(), + coldstart: true, + otel_attribute_trigger: self.otel_attribute_trigger.to_string(), + } + } +} + +/// Tower service created by [OpenTelemetryLayer]. +pub struct OpenTelemetryService { + inner: S, + flush_fn: F, + coldstart: bool, + otel_attribute_trigger: String, +} + +impl Service for OpenTelemetryService +where + S: Service, + F: Fn() + Clone, +{ + type Error = S::Error; + type Response = (); + type Future = OpenTelemetryFuture, F>; + + fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> task::Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: LambdaInvocation) -> Self::Future { + let span = if let Some(tenant_id) = &req.context.tenant_id { + tracing::info_span!( + "Lambda function invocation", + "otel.name" = req.context.env_config.function_name, + "otel.kind" = field::Empty, + { attribute::FAAS_NAME } = req.context.env_config.function_name, + { attribute::FAAS_TRIGGER } = &self.otel_attribute_trigger, + { attribute::FAAS_INVOCATION_ID } = req.context.request_id, + { attribute::FAAS_COLDSTART } = self.coldstart, + "tenant_id" = tenant_id + ) + } else { + tracing::info_span!( + "Lambda function invocation", + "otel.name" = req.context.env_config.function_name, + "otel.kind" = field::Empty, + { attribute::FAAS_NAME } = req.context.env_config.function_name, + { attribute::FAAS_TRIGGER } = &self.otel_attribute_trigger, + { attribute::FAAS_INVOCATION_ID } = req.context.request_id, + { attribute::FAAS_COLDSTART } = self.coldstart + ) + }; + + // After the first execution, we can set 'coldstart' to false + self.coldstart = false; + + let future = { + // Enter the span before calling the inner service + // to ensure that it's assigned as parent of the inner spans. + let _guard = span.enter(); + self.inner.call(req) + }; + OpenTelemetryFuture { + future: Some(future.instrument(span)), + flush_fn: self.flush_fn.clone(), + } + } +} + +/// Future created by [OpenTelemetryService]. +#[pin_project] +pub struct OpenTelemetryFuture { + #[pin] + future: Option, + flush_fn: F, +} + +impl Future for OpenTelemetryFuture +where + Fut: Future, + F: Fn(), +{ + type Output = Fut::Output; + + fn poll(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll { + // First, try to get the ready value of the future + let ready = task::ready!(self + .as_mut() + .project() + .future + .as_pin_mut() + .expect("future polled after completion") + .poll(cx)); + + // If we got the ready value, we first drop the future: this ensures that the + // OpenTelemetry span attached to it is closed and included in the subsequent flush. + Pin::set(&mut self.as_mut().project().future, None); + (self.project().flush_fn)(); + task::Poll::Ready(ready) + } +} + +/// Represent the possible values for the OpenTelemetry `faas.trigger` attribute. +/// See for more details. +#[derive(Default, Clone, Copy)] +#[non_exhaustive] +pub enum OpenTelemetryFaasTrigger { + /// A response to some data source operation such as a database or filesystem read/write + #[default] + Datasource, + /// To provide an answer to an inbound HTTP request + Http, + /// A function is set to be executed when messages are sent to a messaging system + PubSub, + /// A function is scheduled to be executed regularly + Timer, + /// If none of the others apply + Other, +} + +impl Display for OpenTelemetryFaasTrigger { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + OpenTelemetryFaasTrigger::Datasource => write!(f, "datasource"), + OpenTelemetryFaasTrigger::Http => write!(f, "http"), + OpenTelemetryFaasTrigger::PubSub => write!(f, "pubsub"), + OpenTelemetryFaasTrigger::Timer => write!(f, "timer"), + OpenTelemetryFaasTrigger::Other => write!(f, "other"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Config, Context}; + use lambda_runtime_api_client::BoxError; + use std::sync::Arc; + use tower::service_fn; + use tracing_capture::{CaptureLayer, SharedStorage}; + use tracing_subscriber::layer::SubscriberExt; + + fn invocation_with_function_name(function_name: &str) -> LambdaInvocation { + let (parts, _) = http::Response::new(()).into_parts(); + LambdaInvocation { + parts, + body: bytes::Bytes::new(), + context: Context { + env_config: Arc::new(Config { + function_name: function_name.to_owned(), + ..Default::default() + }), + ..Default::default() + }, + } + } + + #[tokio::test] + async fn should_record_faas_name_from_the_function_configuration() { + // given + let storage = SharedStorage::default(); + let subscriber = tracing_subscriber::registry().with(CaptureLayer::new(&storage)); + let _guard = tracing::subscriber::set_default(subscriber); + let inner = service_fn(|_req: LambdaInvocation| async { Ok::<(), BoxError>(()) }); + let mut service = OpenTelemetryLayer::new(|| {}).layer(inner); + + // when + service + .call(invocation_with_function_name("my-function")) + .await + .expect("invocation should succeed"); + + // then + let storage = storage.lock(); + let span = storage + .all_spans() + .find(|span| span.metadata().name() == "Lambda function invocation") + .expect("otel span should be recorded"); + let faas_name = span.value("faas.name").expect("faas.name attribute should be recorded"); + assert_eq!( + *faas_name, "my-function", + "faas.name should match the function's configured name" + ); + } +} diff --git a/lambda-runtime/src/layers/panic.rs b/lambda-runtime/src/layers/panic.rs new file mode 100644 index 000000000..257a8f396 --- /dev/null +++ b/lambda-runtime/src/layers/panic.rs @@ -0,0 +1,112 @@ +use crate::{diagnostic::type_name_of_val, Diagnostic, LambdaEvent}; +use futures::{future::CatchUnwind, FutureExt}; +use pin_project::pin_project; +use std::{any::Any, fmt::Debug, future::Future, marker::PhantomData, panic::AssertUnwindSafe, pin::Pin, task}; +use tower::Service; +use tracing::error; + +/// Tower service that transforms panics into an error. Panics are converted to errors both when +/// constructed in [tower::Service::call] and when constructed in the returned +/// [tower::Service::Future]. +/// +/// This type is only meant for internal use in the Lambda runtime crate. It neither augments the +/// inner service's request type, nor its response type. It merely transforms the error type +/// from `Into + Debug` into `Diagnostic<'a>` to turn panics into diagnostics. +#[derive(Clone)] +pub struct CatchPanicService<'a, S> { + inner: S, + _phantom: PhantomData<&'a ()>, +} + +impl CatchPanicService<'_, S> { + pub fn new(inner: S) -> Self { + Self { + inner, + _phantom: PhantomData, + } + } +} + +impl<'a, S, Payload> Service> for CatchPanicService<'a, S> +where + S: Service>, + S::Future: 'a, + S::Error: Into + Debug, +{ + type Error = Diagnostic; + type Response = S::Response; + type Future = CatchPanicFuture<'a, S::Future>; + + fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> task::Poll> { + self.inner.poll_ready(cx).map_err(|err| err.into()) + } + + fn call(&mut self, req: LambdaEvent) -> Self::Future { + // Catch panics that result from calling `call` on the service + let task = std::panic::catch_unwind(AssertUnwindSafe(|| self.inner.call(req))); + + // Catch panics that result from polling the future returned from `call` + match task { + Ok(task) => { + let fut = AssertUnwindSafe(task).catch_unwind(); + CatchPanicFuture::Future(fut, PhantomData) + } + Err(error) => { + error!(?error, "user handler panicked"); + CatchPanicFuture::Error(error) + } + } + } +} + +/// Future returned by [CatchPanicService]. +#[pin_project(project = CatchPanicFutureProj)] +pub enum CatchPanicFuture<'a, F> { + Future(#[pin] CatchUnwind>, PhantomData<&'a ()>), + Error(Box), +} + +impl Future for CatchPanicFuture<'_, F> +where + F: Future>, + E: Into + Debug, +{ + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll { + use task::Poll; + match self.project() { + CatchPanicFutureProj::Future(fut, _) => match fut.poll(cx) { + Poll::Ready(ready) => match ready { + Ok(Ok(success)) => Poll::Ready(Ok(success)), + Ok(Err(error)) => { + error!("{error:?}"); + Poll::Ready(Err(error.into())) + } + Err(error) => { + error!(?error, "user handler panicked"); + Poll::Ready(Err(Self::build_panic_diagnostic(&error))) + } + }, + Poll::Pending => Poll::Pending, + }, + CatchPanicFutureProj::Error(error) => Poll::Ready(Err(Self::build_panic_diagnostic(error))), + } + } +} + +impl CatchPanicFuture<'_, F> { + fn build_panic_diagnostic(err: &Box) -> Diagnostic { + let error_message = if let Some(msg) = err.downcast_ref::<&str>() { + format!("Lambda panicked: {msg}") + } else if let Some(msg) = err.downcast_ref::() { + format!("Lambda panicked: {msg}") + } else { + "Lambda panicked".to_string() + }; + Diagnostic { + error_type: type_name_of_val(err), + error_message, + } + } +} diff --git a/lambda-runtime/src/layers/trace.rs b/lambda-runtime/src/layers/trace.rs new file mode 100644 index 000000000..9980e8aa5 --- /dev/null +++ b/lambda-runtime/src/layers/trace.rs @@ -0,0 +1,91 @@ +use tower::{Layer, Service}; +use tracing::{instrument::Instrumented, Instrument}; + +use crate::{Context, LambdaInvocation}; +use lambda_runtime_api_client::BoxError; +use std::task; + +/// Tower middleware to create a tracing span for invocations of the Lambda function. +#[derive(Default)] +pub struct TracingLayer {} + +impl TracingLayer { + /// Create a new tracing layer. + pub fn new() -> Self { + Self::default() + } +} + +impl Layer for TracingLayer { + type Service = TracingService; + + fn layer(&self, inner: S) -> Self::Service { + TracingService { inner } + } +} + +/// Tower service returned by [TracingLayer]. +#[derive(Clone)] +pub struct TracingService { + inner: S, +} + +impl Service for TracingService +where + S: Service, +{ + type Response = (); + type Error = BoxError; + type Future = Instrumented; + + fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> task::Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: LambdaInvocation) -> Self::Future { + let span = request_span(&req.context); + let future = { + // Enter the span before calling the inner service + // to ensure that it's assigned as parent of the inner spans. + let _guard = span.enter(); + self.inner.call(req) + }; + future.instrument(span) + } +} + +/* ------------------------------------------- UTILS ------------------------------------------- */ + +/// Creates a tracing span for a Lambda request with context information. +/// +/// This function creates a span that includes the request ID and optionally +/// the X-Ray trace ID and tenant ID if they are available in the context. +pub fn request_span(ctx: &Context) -> tracing::Span { + match (&ctx.xray_trace_id, &ctx.tenant_id) { + (Some(trace_id), Some(tenant_id)) => { + tracing::info_span!( + "Lambda runtime invoke", + requestId = &ctx.request_id, + xrayTraceId = trace_id, + tenantId = tenant_id + ) + } + (Some(trace_id), None) => { + tracing::info_span!( + "Lambda runtime invoke", + requestId = &ctx.request_id, + xrayTraceId = trace_id + ) + } + (None, Some(tenant_id)) => { + tracing::info_span!( + "Lambda runtime invoke", + requestId = &ctx.request_id, + tenantId = tenant_id + ) + } + (None, None) => { + tracing::info_span!("Lambda runtime invoke", requestId = &ctx.request_id) + } + } +} diff --git a/lambda-runtime/src/lib.rs b/lambda-runtime/src/lib.rs index 7664a4991..ce6688baa 100644 --- a/lambda-runtime/src/lib.rs +++ b/lambda-runtime/src/lib.rs @@ -1,44 +1,66 @@ #![deny(clippy::all, clippy::cargo)] #![allow(clippy::multiple_crate_versions)] #![warn(missing_docs, nonstandard_style, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg))] //! The mechanism available for defining a Lambda function is as follows: //! //! Create a type that conforms to the [`tower::Service`] trait. This type can //! then be passed to the the `lambda_runtime::run` function, which launches //! and runs the Lambda runtime. -use futures::FutureExt; -use hyper::{ - client::{connect::Connection, HttpConnector}, - http::Request, - Body, -}; -use lambda_runtime_api_client::Client; use serde::{Deserialize, Serialize}; use std::{ - convert::TryFrom, env, - fmt::{self, Debug, Display}, + fmt::{self, Debug}, future::Future, - panic, + pin::Pin, + sync::Arc, }; -use tokio::io::{AsyncRead, AsyncWrite}; -use tokio_stream::{Stream, StreamExt}; +use tokio_stream::Stream; +use tower::util::ServiceFn; pub use tower::{self, service_fn, Service}; -use tower::{util::ServiceFn, ServiceExt}; -use tracing::{error, trace}; +#[macro_use] +mod macros; + +mod constants; + +/// Diagnostic utilities to convert Rust types into Lambda Error types. +pub mod diagnostic; +pub use diagnostic::Diagnostic; + +mod rate_limiter; + +mod deserializer; +/// Tower middleware to be applied to runtime invocations. +pub mod layers; mod requests; -#[cfg(test)] -mod simulated; +mod runtime; +/// SnapStart snapshot/restore lifecycle support. +pub mod snapstart; +/// Utilities for Lambda Streaming functions. +pub mod streaming; + +/// Utilities to initialize and use `tracing` and `tracing-subscriber` in Lambda Functions. +#[cfg(feature = "tracing")] +#[cfg_attr(docsrs, doc(cfg(feature = "tracing")))] +pub use lambda_runtime_api_client::tracing; + /// Types available to a Lambda function. mod types; -use requests::{EventCompletionRequest, EventErrorRequest, IntoRequest, NextEventRequest}; -pub use types::{Context, LambdaEvent}; +use requests::EventErrorRequest; +pub use runtime::{LambdaInvocation, Runtime}; +pub use snapstart::SnapStartResource; +pub use types::{Context, FunctionResponse, IntoFunctionResponse, LambdaEvent, MetadataPrelude, StreamResponse}; + +/// A boxed, `Send` future with a bound lifetime — the return type of +/// [`SnapStartResource`] hooks. Inlined (rather than re-exported from `futures`) +/// to avoid leaking the `futures` crate into this crate's public type surface. +pub type BoxFuture<'a, T> = Pin + Send + 'a>>; /// Error type that lambdas may result in -pub type Error = lambda_runtime_api_client::Error; +pub type Error = lambda_runtime_api_client::BoxError; /// Configuration derived from environment variables. #[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)] @@ -55,10 +77,12 @@ pub struct Config { pub log_group: String, } +type RefConfig = Arc; + impl Config { /// Attempts to read configuration from environment variables. - pub fn from_env() -> Result { - let conf = Config { + pub fn from_env() -> Self { + Config { function_name: env::var("AWS_LAMBDA_FUNCTION_NAME").expect("Missing AWS_LAMBDA_FUNCTION_NAME env var"), memory: env::var("AWS_LAMBDA_FUNCTION_MEMORY_SIZE") .expect("Missing AWS_LAMBDA_FUNCTION_MEMORY_SIZE env var") @@ -67,8 +91,7 @@ impl Config { version: env::var("AWS_LAMBDA_FUNCTION_VERSION").expect("Missing AWS_LAMBDA_FUNCTION_VERSION env var"), log_stream: env::var("AWS_LAMBDA_LOG_STREAM_NAME").unwrap_or_default(), log_group: env::var("AWS_LAMBDA_LOG_GROUP_NAME").unwrap_or_default(), - }; - Ok(conf) + } } } @@ -81,137 +104,19 @@ where service_fn(move |req: LambdaEvent| f(req.payload, req.context)) } -struct Runtime = HttpConnector> { - client: Client, -} - -impl Runtime -where - C: Service + Clone + Send + Sync + Unpin + 'static, - C::Future: Unpin + Send, - C::Error: Into>, - C::Response: AsyncRead + AsyncWrite + Connection + Unpin + Send + 'static, -{ - pub async fn run( - &self, - incoming: impl Stream, Error>> + Send, - mut handler: F, - config: &Config, - ) -> Result<(), Error> - where - F: Service>, - F::Future: Future>, - F::Error: fmt::Debug + fmt::Display, - A: for<'de> Deserialize<'de>, - B: Serialize, - { - let client = &self.client; - tokio::pin!(incoming); - while let Some(next_event_response) = incoming.next().await { - trace!("New event arrived (run loop)"); - let event = next_event_response?; - let (parts, body) = event.into_parts(); - - #[cfg(debug_assertions)] - if parts.status == http::StatusCode::NO_CONTENT { - // Ignore the event if the status code is 204. - // This is a way to keep the runtime alive when - // there are no events pending to be processed. - continue; - } - - let body = hyper::body::to_bytes(body).await?; - trace!("response body - {}", std::str::from_utf8(&body)?); - - #[cfg(debug_assertions)] - if parts.status.is_server_error() { - error!("Lambda Runtime server returned an unexpected error"); - return Err(parts.status.to_string().into()); - } - - let ctx: Context = Context::try_from(parts.headers)?; - let ctx: Context = ctx.with_config(config); - let request_id = &ctx.request_id.clone(); - - let xray_trace_id = &ctx.xray_trace_id.clone(); - match xray_trace_id { - Some(trace_id) => env::set_var("_X_AMZN_TRACE_ID", trace_id), - None => env::remove_var("_X_AMZN_TRACE_ID"), - } - let body = match serde_json::from_slice(&body) { - Ok(body) => body, - Err(err) => { - let req = build_event_error_request(request_id, err)?; - client.call(req).await.expect("Unable to send response to Runtime APIs"); - return Ok(()); - } - }; - - let req = match handler.ready().await { - Ok(handler) => { - // Catches panics outside of a `Future` - let task = - panic::catch_unwind(panic::AssertUnwindSafe(|| handler.call(LambdaEvent::new(body, ctx)))); - - let task = match task { - // Catches panics inside of the `Future` - Ok(task) => panic::AssertUnwindSafe(task).catch_unwind().await, - Err(err) => Err(err), - }; - - match task { - Ok(response) => match response { - Ok(response) => { - trace!("Ok response from handler (run loop)"); - EventCompletionRequest { - request_id, - body: response, - } - .into_req() - } - Err(err) => build_event_error_request(request_id, err), - }, - Err(err) => { - error!("{:?}", err); - let error_type = type_name_of_val(&err); - let msg = if let Some(msg) = err.downcast_ref::<&str>() { - format!("Lambda panicked: {}", msg) - } else { - "Lambda panicked".to_string() - }; - EventErrorRequest::new(request_id, error_type, &msg).into_req() - } - } - } - Err(err) => build_event_error_request(request_id, err), - }?; - - client.call(req).await.expect("Unable to send response to Runtime APIs"); - } - Ok(()) - } -} - -fn incoming(client: &Client) -> impl Stream, Error>> + Send + '_ -where - C: Service + Clone + Send + Sync + Unpin + 'static, - >::Future: Unpin + Send, - >::Error: Into>, - >::Response: AsyncRead + AsyncWrite + Connection + Unpin + Send + 'static, -{ - async_stream::stream! { - loop { - trace!("Waiting for next event (incoming loop)"); - let req = NextEventRequest.into_req().expect("Unable to construct request"); - let res = client.call(req).await; - yield res; - } - } -} - /// Starts the Lambda Rust runtime and begins polling for events on the [Lambda /// Runtime APIs](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html). /// +/// If you need more control over the runtime and add custom middleware, use the +/// [Runtime] type directly. +/// +/// # Managed concurrency +/// If `AWS_LAMBDA_MAX_CONCURRENCY` is set, a warning is logged. +/// If your handler can satisfy `Clone + Send + 'static`, +/// prefer [`run_concurrent`] (requires the `concurrency-tokio` feature), +/// which honors managed concurrency and falls back to sequential behavior when +/// unset. +/// /// # Example /// ```no_run /// use lambda_runtime::{Error, service_fn, LambdaEvent}; @@ -228,350 +133,189 @@ where /// Ok(event.payload) /// } /// ``` -pub async fn run(handler: F) -> Result<(), Error> +/// +/// # Panics +/// +/// This function panics if required Lambda environment variables are missing +/// (`AWS_LAMBDA_FUNCTION_NAME`, `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`, +/// `AWS_LAMBDA_FUNCTION_VERSION`, `AWS_LAMBDA_RUNTIME_API`). +pub async fn run(handler: F) -> Result<(), Error> where - F: Service>, - F::Future: Future>, - F::Error: fmt::Debug + fmt::Display, + F: Service, Response = R>, + F::Future: Future>, + F::Error: Into + fmt::Debug, A: for<'de> Deserialize<'de>, + R: IntoFunctionResponse, B: Serialize, + S: Stream> + Unpin + Send + 'static, + D: Into + Send, + E: Into + Send + Debug, { - trace!("Loading config from env"); - let config = Config::from_env()?; - let client = Client::builder().build().expect("Unable to create a runtime client"); - let runtime = Runtime { client }; - - let client = &runtime.client; - let incoming = incoming(client); - runtime.run(incoming, handler, &config).await -} - -fn type_name_of_val(_: T) -> &'static str { - std::any::type_name::() + let runtime = Runtime::new(handler).layer(layers::TracingLayer::new()); + runtime.run().await } -fn build_event_error_request(request_id: &str, err: T) -> Result, Error> +/// Starts the Lambda Rust runtime in a mode that is compatible with +/// Lambda Managed Instances (concurrent invocations). +/// +/// Requires the `concurrency-tokio` feature. +/// +/// When `AWS_LAMBDA_MAX_CONCURRENCY` is set to a value greater than 1, this +/// will spawn `AWS_LAMBDA_MAX_CONCURRENCY` tokio worker tasks, each running its own +/// `/next` polling loop. When the environment variable is unset or `<= 1`, it +/// falls back to the same sequential behavior as [`run`], so the same handler +/// can run on both classic Lambda and Lambda Managed Instances. +/// +/// If you need more control over the runtime and add custom middleware, use the +/// [Runtime] type directly. +/// +/// # Example +/// ```no_run +/// use lambda_runtime::{Error, service_fn, LambdaEvent}; +/// use serde_json::Value; +/// +/// #[tokio::main] +/// async fn main() -> Result<(), Error> { +/// let func = service_fn(func); +/// lambda_runtime::run_concurrent(func).await?; +/// Ok(()) +/// } +/// +/// async fn func(event: LambdaEvent) -> Result { +/// Ok(event.payload) +/// } +/// ``` +/// +/// # Panics +/// +/// This function panics if: +/// - Called outside of a Tokio runtime +/// - Required Lambda environment variables are missing (`AWS_LAMBDA_FUNCTION_NAME`, +/// `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`, `AWS_LAMBDA_FUNCTION_VERSION`, +/// `AWS_LAMBDA_RUNTIME_API`) +#[cfg(feature = "concurrency-tokio")] +#[cfg_attr(docsrs, doc(cfg(feature = "concurrency-tokio")))] +pub async fn run_concurrent(handler: F) -> Result<(), Error> where - T: Display + Debug, + F: Service, Response = R> + Clone + Send + 'static, + F::Future: Future> + Send + 'static, + F::Error: Into + fmt::Debug, + A: for<'de> Deserialize<'de> + Send + 'static, + R: IntoFunctionResponse + Send + 'static, + B: Serialize + Send + 'static, + S: Stream> + Unpin + Send + 'static, + D: Into + Send + 'static, + E: Into + Send + Debug + 'static, { - error!("{:?}", err); // logs the error in CloudWatch - let error_type = type_name_of_val(&err); - let msg = format!("{}", err); - - EventErrorRequest::new(request_id, error_type, &msg).into_req() + let runtime = Runtime::new(handler).layer(layers::TracingLayer::new()); + runtime.run_concurrent().await } -#[cfg(test)] -mod endpoint_tests { - use crate::{ - incoming, - requests::{ - EventCompletionRequest, EventErrorRequest, IntoRequest, IntoResponse, NextEventRequest, NextEventResponse, - }, - simulated, - types::Diagnostic, - Error, Runtime, - }; - use futures::future::BoxFuture; - use http::{uri::PathAndQuery, HeaderValue, Method, Request, Response, StatusCode, Uri}; - use hyper::{server::conn::Http, service::service_fn, Body}; - use lambda_runtime_api_client::Client; - use serde_json::json; - use simulated::DuplexStreamWrapper; - use std::{convert::TryFrom, env}; - use tokio::{ - io::{self, AsyncRead, AsyncWrite}, - select, - sync::{self, oneshot}, - }; - use tokio_stream::StreamExt; - - #[cfg(test)] - async fn next_event(req: &Request) -> Result, Error> { - let path = "/2018-06-01/runtime/invocation/next"; - assert_eq!(req.method(), Method::GET); - assert_eq!(req.uri().path_and_query().unwrap(), &PathAndQuery::from_static(path)); - let body = json!({"message": "hello"}); - - let rsp = NextEventResponse { - request_id: "8476a536-e9f4-11e8-9739-2dfe598c3fcd", - deadline: 1_542_409_706_888, - arn: "arn:aws:lambda:us-east-2:123456789012:function:custom-runtime", - trace_id: "Root=1-5bef4de7-ad49b0e87f6ef6c87fc2e700;Parent=9a9197af755a6419", - body: serde_json::to_vec(&body)?, - }; - rsp.into_rsp() - } - - #[cfg(test)] - async fn complete_event(req: &Request, id: &str) -> Result, Error> { - assert_eq!(Method::POST, req.method()); - let rsp = Response::builder() - .status(StatusCode::ACCEPTED) - .body(Body::empty()) - .expect("Unable to construct response"); - - let expected = format!("/2018-06-01/runtime/invocation/{}/response", id); - assert_eq!(expected, req.uri().path()); - - Ok(rsp) - } - - #[cfg(test)] - async fn event_err(req: &Request, id: &str) -> Result, Error> { - let expected = format!("/2018-06-01/runtime/invocation/{}/error", id); - assert_eq!(expected, req.uri().path()); - - assert_eq!(req.method(), Method::POST); - let header = "lambda-runtime-function-error-type"; - let expected = "unhandled"; - assert_eq!(req.headers()[header], HeaderValue::try_from(expected)?); - - let rsp = Response::builder().status(StatusCode::ACCEPTED).body(Body::empty())?; - Ok(rsp) - } - - #[cfg(test)] - async fn handle_incoming(req: Request) -> Result, Error> { - let path: Vec<&str> = req - .uri() - .path_and_query() - .expect("PathAndQuery not found") - .as_str() - .split('/') - .collect::>(); - match path[1..] { - ["2018-06-01", "runtime", "invocation", "next"] => next_event(&req).await, - ["2018-06-01", "runtime", "invocation", id, "response"] => complete_event(&req, id).await, - ["2018-06-01", "runtime", "invocation", id, "error"] => event_err(&req, id).await, - ["2018-06-01", "runtime", "init", "error"] => unimplemented!(), - _ => unimplemented!(), - } - } - - #[cfg(test)] - async fn handle(io: I, rx: oneshot::Receiver<()>) -> Result<(), hyper::Error> - where - I: AsyncRead + AsyncWrite + Unpin + 'static, - { - let conn = Http::new().serve_connection(io, service_fn(handle_incoming)); - select! { - _ = rx => { - Ok(()) - } - res = conn => { - match res { - Ok(()) => Ok(()), - Err(e) => { - Err(e) - } - } +/// Spawns a task that will be execute a provided async closure when the process +/// receives unix graceful shutdown signals. If the closure takes longer than 500ms +/// to execute, an unhandled `SIGKILL` signal might be received. +/// +/// You can use this future to execute cleanup or flush related logic prior to runtime shutdown. +/// +/// This function's returned future must be resolved prior to `lambda_runtime::run()`. +/// +/// Note that this implicitly also registers and drives a no-op internal extension that subscribes to no events. +/// This extension will be named `_lambda-rust-runtime-no-op-graceful-shutdown-helper`. This extension name +/// can not be reused by other registered extensions. This is necessary in order to receive graceful shutdown signals. +/// +/// This extension is cheap to run because it receives no events, but is not zero cost. If you have another extension +/// registered already, you might prefer to manually construct your own graceful shutdown handling without the dummy extension. +/// +/// For more information on general AWS Lambda graceful shutdown handling, see: +/// +/// +/// # Panics +/// +/// This function panics if: +/// - this function is called after `lambda_runtime::run()` +/// - this function is called outside of a context that has access to the tokio i/o +/// - the no-op extension cannot be registered +/// - either signal listener panics [tokio::signal::unix](https://docs.rs/tokio/latest/tokio/signal/unix/fn.signal.html#errors) +/// +/// # Example +/// ```no_run +/// use lambda_runtime::{Error, service_fn, LambdaEvent}; +/// use serde_json::Value; +/// +/// #[tokio::main] +/// async fn main() -> Result<(), Error> { +/// let func = service_fn(func); +/// +/// let (writer, log_guard) = tracing_appender::non_blocking(std::io::stdout()); +/// lambda_runtime::tracing::init_default_subscriber_with_writer(writer); +/// +/// let shutdown_hook = || async move { +/// std::mem::drop(log_guard); +/// }; +/// lambda_runtime::spawn_graceful_shutdown_handler(shutdown_hook).await; +/// +/// lambda_runtime::run(func).await?; +/// Ok(()) +/// } +/// +/// async fn func(event: LambdaEvent) -> Result { +/// Ok(event.payload) +/// } +/// ``` +#[cfg(all(unix, feature = "graceful-shutdown"))] +#[cfg_attr(docsrs, doc(cfg(all(unix, feature = "graceful-shutdown"))))] +pub async fn spawn_graceful_shutdown_handler(shutdown_hook: impl FnOnce() -> Fut + Send + 'static) +where + Fut: Future + Send + 'static, +{ + // You need an extension registered with the Lambda orchestrator in order for your process + // to receive a SIGTERM for graceful shutdown. + // + // We accomplish this here by registering a no-op internal extension, which does not subscribe to any events. + // + // This extension is cheap to run since after it connects to the lambda orchestration, the connection + // will just wait forever for data to come, which never comes, so it won't cause wakes. + let extension = lambda_extension::Extension::new() + // Don't subscribe to any event types + .with_events(&[]) + // Internal extension names MUST be unique within a given Lambda function. + .with_extension_name("_lambda-rust-runtime-no-op-graceful-shutdown-helper") + // Extensions MUST be registered before calling lambda_runtime::run(), which ends the Init + // phase and begins the Invoke phase. + .register() + .await + .expect("could not register no-op extension for graceful shutdown"); + + tokio::task::spawn(async move { + let graceful_shutdown_future = async move { + let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt()).unwrap(); + let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()).unwrap(); + tokio::select! { + _sigint = sigint.recv() => { + eprintln!("[runtime] SIGINT received"); + eprintln!("[runtime] Graceful shutdown in progress ..."); + shutdown_hook().await; + eprintln!("[runtime] Graceful shutdown completed"); + std::process::exit(0); + }, + _sigterm = sigterm.recv()=> { + eprintln!("[runtime] SIGTERM received"); + eprintln!("[runtime] Graceful shutdown in progress ..."); + shutdown_hook().await; + eprintln!("[runtime] Graceful shutdown completed"); + std::process::exit(0); + }, } - } - } - - #[tokio::test] - async fn test_next_event() -> Result<(), Error> { - let base = Uri::from_static("http://localhost:9001"); - let (client, server) = io::duplex(64); - - let (tx, rx) = sync::oneshot::channel(); - let server = tokio::spawn(async { - handle(server, rx).await.expect("Unable to handle request"); - }); - - let conn = simulated::Connector::with(base.clone(), DuplexStreamWrapper::new(client))?; - let client = Client::with(base, conn); - - let req = NextEventRequest.into_req()?; - let rsp = client.call(req).await.expect("Unable to send request"); - - assert_eq!(rsp.status(), StatusCode::OK); - let header = "lambda-runtime-deadline-ms"; - assert_eq!(rsp.headers()[header], &HeaderValue::try_from("1542409706888")?); - - // shutdown server... - tx.send(()).expect("Receiver has been dropped"); - match server.await { - Ok(_) => Ok(()), - Err(e) if e.is_panic() => Err::<(), Error>(e.into()), - Err(_) => unreachable!("This branch shouldn't be reachable"), - } - } - - #[tokio::test] - async fn test_ok_response() -> Result<(), Error> { - let (client, server) = io::duplex(64); - let (tx, rx) = sync::oneshot::channel(); - let base = Uri::from_static("http://localhost:9001"); - - let server = tokio::spawn(async { - handle(server, rx).await.expect("Unable to handle request"); - }); - - let conn = simulated::Connector::with(base.clone(), DuplexStreamWrapper::new(client))?; - let client = Client::with(base, conn); - - let req = EventCompletionRequest { - request_id: "156cb537-e2d4-11e8-9b34-d36013741fb9", - body: "done", - }; - let req = req.into_req()?; - - let rsp = client.call(req).await?; - assert_eq!(rsp.status(), StatusCode::ACCEPTED); - - // shutdown server - tx.send(()).expect("Receiver has been dropped"); - match server.await { - Ok(_) => Ok(()), - Err(e) if e.is_panic() => Err::<(), Error>(e.into()), - Err(_) => unreachable!("This branch shouldn't be reachable"), - } - } - - #[tokio::test] - async fn test_error_response() -> Result<(), Error> { - let (client, server) = io::duplex(200); - let (tx, rx) = sync::oneshot::channel(); - let base = Uri::from_static("http://localhost:9001"); - - let server = tokio::spawn(async { - handle(server, rx).await.expect("Unable to handle request"); - }); - - let conn = simulated::Connector::with(base.clone(), DuplexStreamWrapper::new(client))?; - let client = Client::with(base, conn); - - let req = EventErrorRequest { - request_id: "156cb537-e2d4-11e8-9b34-d36013741fb9", - diagnostic: Diagnostic { - error_type: "InvalidEventDataError", - error_message: "Error parsing event data", - }, }; - let req = req.into_req()?; - let rsp = client.call(req).await?; - assert_eq!(rsp.status(), StatusCode::ACCEPTED); - - // shutdown server - tx.send(()).expect("Receiver has been dropped"); - match server.await { - Ok(_) => Ok(()), - Err(e) if e.is_panic() => Err::<(), Error>(e.into()), - Err(_) => unreachable!("This branch shouldn't be reachable"), - } - } - - #[tokio::test] - async fn successful_end_to_end_run() -> Result<(), Error> { - let (client, server) = io::duplex(64); - let (tx, rx) = sync::oneshot::channel(); - let base = Uri::from_static("http://localhost:9001"); - let server = tokio::spawn(async { - handle(server, rx).await.expect("Unable to handle request"); + let _: (_, ()) = tokio::join!( + // we always poll the graceful shutdown future first, + // which results in a smaller future due to lack of bookkeeping of which was last polled + biased; + graceful_shutdown_future, async { + // we suppress extension errors because we don't actually mind if it crashes, + // all we need to do is kick off the run so that lambda exits the init phase + let _ = extension.run().await; }); - let conn = simulated::Connector::with(base.clone(), DuplexStreamWrapper::new(client))?; - - let client = Client::builder() - .with_endpoint(base) - .with_connector(conn) - .build() - .expect("Unable to build client"); - - async fn func(event: crate::LambdaEvent) -> Result { - let (event, _) = event.into_parts(); - Ok(event) - } - let f = crate::service_fn(func); - - // set env vars needed to init Config if they are not already set in the environment - if env::var("AWS_LAMBDA_RUNTIME_API").is_err() { - env::set_var("AWS_LAMBDA_RUNTIME_API", "http://localhost:9001"); - } - if env::var("AWS_LAMBDA_FUNCTION_NAME").is_err() { - env::set_var("AWS_LAMBDA_FUNCTION_NAME", "test_fn"); - } - if env::var("AWS_LAMBDA_FUNCTION_MEMORY_SIZE").is_err() { - env::set_var("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "128"); - } - if env::var("AWS_LAMBDA_FUNCTION_VERSION").is_err() { - env::set_var("AWS_LAMBDA_FUNCTION_VERSION", "1"); - } - if env::var("AWS_LAMBDA_LOG_STREAM_NAME").is_err() { - env::set_var("AWS_LAMBDA_LOG_STREAM_NAME", "test_stream"); - } - if env::var("AWS_LAMBDA_LOG_GROUP_NAME").is_err() { - env::set_var("AWS_LAMBDA_LOG_GROUP_NAME", "test_log"); - } - let config = crate::Config::from_env().expect("Failed to read env vars"); - - let runtime = Runtime { client }; - let client = &runtime.client; - let incoming = incoming(client).take(1); - runtime.run(incoming, f, &config).await?; - - // shutdown server - tx.send(()).expect("Receiver has been dropped"); - match server.await { - Ok(_) => Ok(()), - Err(e) if e.is_panic() => Err::<(), Error>(e.into()), - Err(_) => unreachable!("This branch shouldn't be reachable"), - } - } - - async fn run_panicking_handler(func: F) -> Result<(), Error> - where - F: FnMut(crate::LambdaEvent) -> BoxFuture<'static, Result>, - { - let (client, server) = io::duplex(64); - let (_tx, rx) = oneshot::channel(); - let base = Uri::from_static("http://localhost:9001"); - - let server = tokio::spawn(async { - handle(server, rx).await.expect("Unable to handle request"); - }); - let conn = simulated::Connector::with(base.clone(), DuplexStreamWrapper::new(client))?; - - let client = Client::builder() - .with_endpoint(base) - .with_connector(conn) - .build() - .expect("Unable to build client"); - - let f = crate::service_fn(func); - - let config = crate::Config { - function_name: "test_fn".to_string(), - memory: 128, - version: "1".to_string(), - log_stream: "test_stream".to_string(), - log_group: "test_log".to_string(), - }; - - let runtime = Runtime { client }; - let client = &runtime.client; - let incoming = incoming(client).take(1); - runtime.run(incoming, f, &config).await?; - - match server.await { - Ok(_) => Ok(()), - Err(e) if e.is_panic() => Err::<(), Error>(e.into()), - Err(_) => unreachable!("This branch shouldn't be reachable"), - } - } - - #[tokio::test] - async fn panic_in_async_run() -> Result<(), Error> { - run_panicking_handler(|_| Box::pin(async { panic!("This is intentionally here") })).await - } - - #[tokio::test] - async fn panic_outside_async_run() -> Result<(), Error> { - run_panicking_handler(|_| { - panic!("This is intentionally here"); - }) - .await - } + }); } diff --git a/lambda-runtime/src/macros.rs b/lambda-runtime/src/macros.rs new file mode 100644 index 000000000..8c531470d --- /dev/null +++ b/lambda-runtime/src/macros.rs @@ -0,0 +1,10 @@ +// Logs using tracing `error!` if a dispatcher is set, otherwise falls back to `eprintln!`. +macro_rules! log_or_print { + (tracing: $tracing_expr:expr, fallback: $fallback_expr:expr) => { + if tracing::dispatcher::has_been_set() { + $tracing_expr; + } else { + $fallback_expr; + } + }; +} diff --git a/lambda-runtime/src/rate_limiter.rs b/lambda-runtime/src/rate_limiter.rs new file mode 100644 index 000000000..0c4bd6765 --- /dev/null +++ b/lambda-runtime/src/rate_limiter.rs @@ -0,0 +1,88 @@ +//! Internal call-site rate limiting for infrequent runtime events. + +use std::{ + sync::OnceLock, + time::{Duration, Instant}, +}; + +/// Returns monotonic process-relative time for the rate limiter. +pub(crate) fn time_since_epoch() -> Duration { + static EPOCH: OnceLock = OnceLock::new(); + + Instant::now().duration_since(*EPOCH.get_or_init(Instant::now)) +} + +/// Evaluates a call at most once per interval at each macro call site. +/// +/// The limiter state is local to the call site and persists for the lifetime of +/// the process. It is therefore shared across warm invocations in one Lambda +/// execution environment and reset by a cold start. +macro_rules! rate_limited { + ($interval:expr, $call:expr) => {{ + use std::sync::atomic::{AtomicU64, Ordering}; + + static NEXT_CALL: AtomicU64 = AtomicU64::new(u64::MIN); + let interval: std::time::Duration = $interval; + let time = $crate::rate_limiter::time_since_epoch(); + let next = NEXT_CALL.load(Ordering::Relaxed); + + if next <= time.as_secs() { + let new_next = time.checked_add(interval).unwrap_or(std::time::Duration::MAX).as_secs(); + + if NEXT_CALL + .compare_exchange(next, new_next, Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { + $call; + } + } + }}; +} + +pub(crate) use rate_limited; + +#[cfg(test)] +mod tests { + use super::*; + use std::{ + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, + thread, + }; + + #[test] + fn allows_first_call_and_rejects_calls_inside_interval() { + let mut calls = 0; + + for _ in 0..2 { + rate_limited!(Duration::from_secs(60), { + calls += 1; + }); + } + + assert_eq!(calls, 1); + } + + #[test] + fn allows_only_one_concurrent_call() { + let calls = Arc::new(AtomicUsize::new(0)); + let handles = (0..8) + .map(|_| { + let calls = Arc::clone(&calls); + thread::spawn(move || { + rate_limited!(Duration::from_secs(60), { + calls.fetch_add(1, Ordering::Relaxed); + }); + }) + }) + .collect::>(); + + for handle in handles { + handle.join().unwrap(); + } + + assert_eq!(calls.load(Ordering::Relaxed), 1); + } +} diff --git a/lambda-runtime/src/requests.rs b/lambda-runtime/src/requests.rs index 26257d207..933e3100a 100644 --- a/lambda-runtime/src/requests.rs +++ b/lambda-runtime/src/requests.rs @@ -1,18 +1,18 @@ -use crate::{types::Diagnostic, Error}; -use http::{Method, Request, Response, Uri}; -use hyper::Body; -use lambda_runtime_api_client::build_request; +use crate::{ + constants::LAMBDA_RUNTIME_INVOCATION_ID, types::ToStreamErrorTrailer, Diagnostic, Error, FunctionResponse, + IntoFunctionResponse, +}; +use bytes::Bytes; +use http::{header::CONTENT_TYPE, Method, Request, Uri}; +use lambda_runtime_api_client::{body::Body, build_request}; use serde::Serialize; -use std::str::FromStr; +use std::{fmt::Debug, marker::PhantomData, str::FromStr}; +use tokio_stream::{Stream, StreamExt}; pub(crate) trait IntoRequest { fn into_req(self) -> Result, Error>; } -pub(crate) trait IntoResponse { - fn into_rsp(self) -> Result, Error>; -} - // /runtime/invocation/next #[derive(Debug, Eq, PartialEq)] pub(crate) struct NextEventRequest; @@ -22,164 +22,461 @@ impl IntoRequest for NextEventRequest { let req = build_request() .method(Method::GET) .uri(Uri::from_static("/2018-06-01/runtime/invocation/next")) - .body(Body::empty())?; + .body(Default::default())?; Ok(req) } } +// /runtime/restore/next #[derive(Debug, Eq, PartialEq)] -pub struct NextEventResponse<'a> { - // lambda-runtime-aws-request-id - pub request_id: &'a str, - // lambda-runtime-deadline-ms - pub deadline: u64, - // lambda-runtime-invoked-function-arn - pub arn: &'a str, - // lambda-runtime-trace-id - pub trace_id: &'a str, - // the actual body, - pub body: Vec, +pub(crate) struct RestoreNextRequest; + +impl IntoRequest for RestoreNextRequest { + fn into_req(self) -> Result, Error> { + let req = build_request() + .method(Method::GET) + .uri(Uri::from_static("/2018-06-01/runtime/restore/next")) + .body(Default::default())?; + Ok(req) + } } -impl<'a> IntoResponse for NextEventResponse<'a> { - fn into_rsp(self) -> Result, Error> { - let rsp = Response::builder() - .header("lambda-runtime-aws-request-id", self.request_id) - .header("lambda-runtime-deadline-ms", self.deadline) - .header("lambda-runtime-invoked-function-arn", self.arn) - .header("lambda-runtime-trace-id", self.trace_id) - .body(Body::from(self.body))?; - Ok(rsp) +// /runtime/init/error +pub(crate) struct InitErrorRequest { + pub(crate) diagnostic: Diagnostic, +} + +impl IntoRequest for InitErrorRequest { + fn into_req(self) -> Result, Error> { + let uri = Uri::from_static("/2018-06-01/runtime/init/error"); + let body = serde_json::to_vec(&self.diagnostic)?; + let body = Body::from(body); + + let req = build_request() + .method(Method::POST) + .uri(uri) + .header("lambda-runtime-function-error-type", "Runtime.InitError") + .body(body)?; + Ok(req) } } -#[test] -fn test_next_event_request() { - let req = NextEventRequest; - let req = req.into_req().unwrap(); - assert_eq!(req.method(), Method::GET); - assert_eq!(req.uri(), &Uri::from_static("/2018-06-01/runtime/invocation/next")); - assert!(match req.headers().get("User-Agent") { - Some(header) => header.to_str().unwrap().starts_with("aws-lambda-rust/"), - None => false, - }); + +// /runtime/restore/error +pub(crate) struct RestoreErrorRequest { + pub(crate) diagnostic: Diagnostic, +} + +impl IntoRequest for RestoreErrorRequest { + fn into_req(self) -> Result, Error> { + let uri = Uri::from_static("/2018-06-01/runtime/restore/error"); + let body = serde_json::to_vec(&self.diagnostic)?; + let body = Body::from(body); + + let req = build_request() + .method(Method::POST) + .uri(uri) + .header("lambda-runtime-function-error-type", "Runtime.AfterRestoreError") + .body(body)?; + Ok(req) + } } // /runtime/invocation/{AwsRequestId}/response -pub(crate) struct EventCompletionRequest<'a, T> { +pub(crate) struct EventCompletionRequest<'a, R, B, S, D, E> +where + R: IntoFunctionResponse, + B: Serialize, + S: Stream> + Unpin + Send + 'static, + D: Into + Send, + E: Into + Send + Debug, +{ pub(crate) request_id: &'a str, - pub(crate) body: T, + pub(crate) invocation_id: Option<&'a str>, + pub(crate) body: R, + pub(crate) _unused_b: PhantomData, + pub(crate) _unused_s: PhantomData, } -impl<'a, T> IntoRequest for EventCompletionRequest<'a, T> +impl<'a, R, B, D, E, S> EventCompletionRequest<'a, R, B, S, D, E> where - T: for<'serialize> Serialize, + R: IntoFunctionResponse, + B: Serialize, + S: Stream> + Unpin + Send + 'static, + D: Into + Send, + E: Into + Send + Debug, { - fn into_req(self) -> Result, Error> { - let uri = format!("/2018-06-01/runtime/invocation/{}/response", self.request_id); - let uri = Uri::from_str(&uri)?; - let body = serde_json::to_vec(&self.body)?; - let body = Body::from(body); - - let req = build_request().method(Method::POST).uri(uri).body(body)?; - Ok(req) + /// Initialize a new EventCompletionRequest + pub(crate) fn new( + request_id: &'a str, + invocation_id: Option<&'a str>, + body: R, + ) -> EventCompletionRequest<'a, R, B, S, D, E> { + EventCompletionRequest { + request_id, + invocation_id, + body, + _unused_b: PhantomData::, + _unused_s: PhantomData::, + } } } -#[test] -fn test_event_completion_request() { - let req = EventCompletionRequest { - request_id: "id", - body: "hello, world!", - }; - let req = req.into_req().unwrap(); - let expected = Uri::from_static("/2018-06-01/runtime/invocation/id/response"); - assert_eq!(req.method(), Method::POST); - assert_eq!(req.uri(), &expected); - assert!(match req.headers().get("User-Agent") { - Some(header) => header.to_str().unwrap().starts_with("aws-lambda-rust/"), - None => false, - }); +impl IntoRequest for EventCompletionRequest<'_, R, B, S, D, E> +where + R: IntoFunctionResponse, + B: Serialize, + S: Stream> + Unpin + Send + 'static, + D: Into + Send, + E: Into + Send + Debug, +{ + fn into_req(self) -> Result, Error> { + match self.body.into_response() { + FunctionResponse::BufferedResponse(body) => { + let uri = format!("/2018-06-01/runtime/invocation/{}/response", self.request_id); + let uri = Uri::from_str(&uri)?; + + let body = serde_json::to_vec(&body)?; + let body = Body::from(body); + + let mut req = build_request().method(Method::POST).uri(uri).body(body)?; + + if let Some(id) = self.invocation_id { + req.headers_mut().insert(LAMBDA_RUNTIME_INVOCATION_ID, id.parse()?); + } + + Ok(req) + } + FunctionResponse::StreamingResponse(mut response) => { + let uri = format!("/2018-06-01/runtime/invocation/{}/response", self.request_id); + let uri = Uri::from_str(&uri)?; + + let mut builder = build_request().method(Method::POST).uri(uri); + let req_headers = builder.headers_mut().unwrap(); + + req_headers.insert("Transfer-Encoding", "chunked".parse()?); + req_headers.insert("Lambda-Runtime-Function-Response-Mode", "streaming".parse()?); + // Report midstream errors using error trailers. + // See the details in Lambda Developer Doc: https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html#runtimes-custom-response-streaming + req_headers.append("Trailer", "Lambda-Runtime-Function-Error-Type".parse()?); + req_headers.append("Trailer", "Lambda-Runtime-Function-Error-Body".parse()?); + + if let Some(id) = self.invocation_id { + req_headers.insert(LAMBDA_RUNTIME_INVOCATION_ID, id.parse()?); + } + + req_headers.insert( + "Content-Type", + "application/vnd.awslambda.http-integration-response".parse()?, + ); + + // default Content-Type + let preloud_headers = &mut response.metadata_prelude.headers; + preloud_headers + .entry(CONTENT_TYPE) + .or_insert("application/octet-stream".parse()?); + + let metadata_prelude = serde_json::to_string(&response.metadata_prelude)?; + + tracing::trace!(?metadata_prelude); + + let (mut tx, rx) = Body::channel(); + + tokio::spawn(async move { + if tx.send_data(metadata_prelude.into()).await.is_err() { + tracing::error!("Error sending metadata prelude, response channel closed"); + return; + } + + if tx.send_data("\u{0}".repeat(8).into()).await.is_err() { + tracing::error!("Error sending metadata prelude delimiter, response channel closed"); + return; + } + + while let Some(chunk) = response.stream.next().await { + let chunk = match chunk { + Ok(chunk) => chunk.into(), + Err(err) => err.into().to_tailer().into(), + }; + + if tx.send_data(chunk).await.is_err() { + tracing::error!("Error sending response body chunk, response channel closed"); + return; + } + } + }); + + let req = builder.body(rx)?; + Ok(req) + } + } + } } // /runtime/invocation/{AwsRequestId}/error pub(crate) struct EventErrorRequest<'a> { pub(crate) request_id: &'a str, - pub(crate) diagnostic: Diagnostic<'a>, + pub(crate) invocation_id: Option<&'a str>, + pub(crate) diagnostic: Diagnostic, } impl<'a> EventErrorRequest<'a> { - pub(crate) fn new(request_id: &'a str, error_type: &'a str, error_message: &'a str) -> EventErrorRequest<'a> { + pub(crate) fn new( + request_id: &'a str, + invocation_id: Option<&'a str>, + diagnostic: impl Into, + ) -> EventErrorRequest<'a> { EventErrorRequest { request_id, - diagnostic: Diagnostic { - error_type, - error_message, - }, + invocation_id, + diagnostic: diagnostic.into(), } } } -impl<'a> IntoRequest for EventErrorRequest<'a> { +impl IntoRequest for EventErrorRequest<'_> { fn into_req(self) -> Result, Error> { let uri = format!("/2018-06-01/runtime/invocation/{}/error", self.request_id); let uri = Uri::from_str(&uri)?; let body = serde_json::to_vec(&self.diagnostic)?; let body = Body::from(body); - let req = build_request() + let mut req = build_request() .method(Method::POST) .uri(uri) .header("lambda-runtime-function-error-type", "unhandled") .body(body)?; + + if let Some(id) = self.invocation_id { + req.headers_mut().insert(LAMBDA_RUNTIME_INVOCATION_ID, id.parse()?); + } + Ok(req) } } -#[test] -fn test_event_error_request() { - let req = EventErrorRequest { - request_id: "id", - diagnostic: Diagnostic { - error_type: "InvalidEventDataError", - error_message: "Error parsing event data", - }, - }; - let req = req.into_req().unwrap(); - let expected = Uri::from_static("/2018-06-01/runtime/invocation/id/error"); - assert_eq!(req.method(), Method::POST); - assert_eq!(req.uri(), &expected); - assert!(match req.headers().get("User-Agent") { - Some(header) => header.to_str().unwrap().starts_with("aws-lambda-rust/"), - None => false, - }); -} +#[cfg(test)] +mod tests { + use super::*; -// /runtime/init/error -struct InitErrorRequest; + #[test] + fn test_next_event_request() { + let req = NextEventRequest; + let req = req.into_req().unwrap(); + assert_eq!(req.method(), Method::GET); + assert_eq!(req.uri(), &Uri::from_static("/2018-06-01/runtime/invocation/next")); + assert!(match req.headers().get("User-Agent") { + Some(header) => header.to_str().unwrap().starts_with("aws-lambda-rust/"), + None => false, + }); + } -impl IntoRequest for InitErrorRequest { - fn into_req(self) -> Result, Error> { - let uri = "/2018-06-01/runtime/init/error".to_string(); - let uri = Uri::from_str(&uri)?; + #[test] + fn test_event_completion_request() { + let req = EventCompletionRequest::new("id", Option::Some("invocation_id"), "hello, world!"); + let req = req.into_req().unwrap(); + let expected = Uri::from_static("/2018-06-01/runtime/invocation/id/response"); + assert_eq!(req.method(), Method::POST); + assert_eq!(req.uri(), &expected); - let req = build_request() - .method(Method::POST) - .uri(uri) - .header("lambda-runtime-function-error-type", "unhandled") - .body(Body::empty())?; - Ok(req) + assert!(req + .headers() + .get("User-Agent") + .unwrap() + .to_str() + .unwrap() + .starts_with("aws-lambda-rust/")); + + assert_eq!( + req.headers().get(LAMBDA_RUNTIME_INVOCATION_ID).unwrap(), + "invocation_id" + ); } -} -#[test] -fn test_init_error_request() { - let req = InitErrorRequest; - let req = req.into_req().unwrap(); - let expected = Uri::from_static("/2018-06-01/runtime/init/error"); - assert_eq!(req.method(), Method::POST); - assert_eq!(req.uri(), &expected); - assert!(match req.headers().get("User-Agent") { - Some(header) => header.to_str().unwrap().starts_with("aws-lambda-rust/"), - None => false, - }); + #[test] + fn test_event_completion_request_invocation_id_not_added_when_none() { + let req = EventCompletionRequest::new("id", Option::None, "hello, world!"); + let req = req.into_req().unwrap(); + + assert!(req.headers().get(LAMBDA_RUNTIME_INVOCATION_ID).is_none()); + } + + #[test] + fn test_streaming_event_completion_request_with_invocation_id() { + use crate::StreamResponse; + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + + runtime.block_on(async { + let stream = tokio_stream::iter(vec![Ok::(Bytes::from_static(b"chunk"))]); + let stream_response: StreamResponse<_> = stream.into(); + let response = FunctionResponse::StreamingResponse(stream_response); + + let req: EventCompletionRequest<'_, _, (), _, _, _> = + EventCompletionRequest::new("id", Some("invocation_id"), response); + + let http_req = req.into_req().expect("into_req should succeed"); + let expected = Uri::from_static("/2018-06-01/runtime/invocation/id/response"); + assert_eq!(http_req.method(), Method::POST); + assert_eq!(http_req.uri(), &expected); + + assert_eq!( + http_req.headers().get(LAMBDA_RUNTIME_INVOCATION_ID).unwrap(), + "invocation_id" + ); + }); + } + + #[test] + fn test_streaming_event_completion_request_invocation_id_not_added_when_none() { + use crate::StreamResponse; + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + + runtime.block_on(async { + let stream = tokio_stream::iter(vec![Ok::(Bytes::from_static(b"chunk"))]); + let stream_response: StreamResponse<_> = stream.into(); + let response = FunctionResponse::StreamingResponse(stream_response); + + let req: EventCompletionRequest<'_, _, (), _, _, _> = EventCompletionRequest::new("id", None, response); + + let http_req = req.into_req().expect("into_req should succeed"); + + assert!(http_req.headers().get(LAMBDA_RUNTIME_INVOCATION_ID).is_none()); + }); + } + + #[test] + fn test_event_error_request() { + let req = EventErrorRequest { + request_id: "id", + invocation_id: Option::Some("invocation_id"), + diagnostic: Diagnostic { + error_type: "InvalidEventDataError".into(), + error_message: "Error parsing event data".into(), + }, + }; + let req = req.into_req().unwrap(); + let expected = Uri::from_static("/2018-06-01/runtime/invocation/id/error"); + assert_eq!(req.method(), Method::POST); + assert_eq!(req.uri(), &expected); + assert!(match req.headers().get("User-Agent") { + Some(header) => header.to_str().unwrap().starts_with("aws-lambda-rust/"), + None => false, + }); + + assert!(match req.headers().get(LAMBDA_RUNTIME_INVOCATION_ID) { + Some(header) => header.to_str().unwrap() == "invocation_id", + None => false, + }); + } + + #[test] + fn test_event_error_request_invocation_id_not_added_when_none() { + let req = EventErrorRequest { + request_id: "id", + invocation_id: None, + diagnostic: Diagnostic { + error_type: "InvalidEventDataError".into(), + error_message: "Error parsing event data".into(), + }, + }; + let req = req.into_req().unwrap(); + + assert!(req.headers().get(LAMBDA_RUNTIME_INVOCATION_ID).is_none()); + } + + #[test] + fn test_restore_next_request() { + let req = RestoreNextRequest; + let req = req.into_req().unwrap(); + assert_eq!(req.method(), Method::GET); + assert_eq!(req.uri(), &Uri::from_static("/2018-06-01/runtime/restore/next")); + assert!(req + .headers() + .get("User-Agent") + .unwrap() + .to_str() + .unwrap() + .starts_with("aws-lambda-rust/")); + } + + #[test] + fn test_restore_error_request() { + let req = RestoreErrorRequest { + diagnostic: Diagnostic { + error_type: "Runtime.AfterRestoreError".into(), + error_message: "Failed to reconnect to database".into(), + }, + }; + let req = req.into_req().unwrap(); + let expected = Uri::from_static("/2018-06-01/runtime/restore/error"); + assert_eq!(req.method(), Method::POST); + assert_eq!(req.uri(), &expected); + assert_eq!( + req.headers().get("lambda-runtime-function-error-type").unwrap(), + "Runtime.AfterRestoreError" + ); + } + + #[test] + fn test_init_error_request() { + let req = InitErrorRequest { + diagnostic: Diagnostic { + error_type: "Runtime.InitError".into(), + error_message: "Failed to release resources before snapshot".into(), + }, + }; + let req = req.into_req().unwrap(); + let expected = Uri::from_static("/2018-06-01/runtime/init/error"); + assert_eq!(req.method(), Method::POST); + assert_eq!(req.uri(), &expected); + assert_eq!( + req.headers().get("lambda-runtime-function-error-type").unwrap(), + "Runtime.InitError" + ); + } + + #[test] + #[cfg(tokio_unstable)] + fn streaming_send_data_error_is_ignored() { + use crate::StreamResponse; + + let runtime = tokio::runtime::Builder::new_current_thread() + .unhandled_panic(tokio::runtime::UnhandledPanic::ShutdownRuntime) + .enable_all() + .build() + .unwrap(); + + // We don't want to use a global panic hook to avoid shared state across tests, but we need to know + // if a background task panics. We accomplish this by using UnhandledPanic::ShutdownRuntime + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + runtime.block_on(async { + let stream = tokio_stream::iter(vec![Ok::(Bytes::from_static(b"chunk"))]); + + let stream_response: StreamResponse<_> = stream.into(); + let response = FunctionResponse::StreamingResponse(stream_response); + + let req: EventCompletionRequest<'_, _, (), _, _, _> = EventCompletionRequest::new("id", None, response); + + let http_req = req.into_req().expect("into_req should succeed"); + + // immediate drop simulates client disconnection + drop(http_req); + + // give the spawned task time to complete + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + }) + })); + + assert!( + result.is_ok(), + "spawned task panicked - send_data errors should be ignored" + ); + } } diff --git a/lambda-runtime/src/runtime.rs b/lambda-runtime/src/runtime.rs new file mode 100644 index 000000000..f31bc4f24 --- /dev/null +++ b/lambda-runtime/src/runtime.rs @@ -0,0 +1,1575 @@ +use crate::{ + layers::{CatchPanicService, RuntimeApiClientService, RuntimeApiResponseService}, + requests::{InitErrorRequest, IntoRequest, NextEventRequest, RestoreErrorRequest, RestoreNextRequest}, + snapstart::SnapStartResource, + types::{invoke_request_id, IntoFunctionResponse, LambdaEvent}, + Config, Context, Diagnostic, +}; +#[cfg(feature = "concurrency-tokio")] +use futures::stream::FuturesUnordered; +use http_body_util::BodyExt; +use lambda_runtime_api_client::{BoxError, PooledClient as ApiClient}; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "concurrency-tokio")] +use std::fmt; +use std::{env, fmt::Debug, future::Future, sync::Arc}; +use tokio_stream::{Stream, StreamExt}; +use tower::{Layer, Service, ServiceExt}; +use tracing::trace; +#[cfg(feature = "concurrency-tokio")] +use tracing::{debug, error, info_span, warn, Instrument}; + +/* ----------------------------------------- INVOCATION ---------------------------------------- */ + +/// A simple container that provides information about a single invocation of a Lambda function. +pub struct LambdaInvocation { + /// The header of the request sent to invoke the Lambda function. + pub parts: http::response::Parts, + /// The body of the request sent to invoke the Lambda function. + pub body: bytes::Bytes, + /// The context of the Lambda invocation. + pub context: Context, +} + +/* ------------------------------------------ RUNTIME ------------------------------------------ */ + +/// Lambda runtime executing a handler function on incoming requests. +/// +/// Middleware can be added to a runtime using the [Runtime::layer] method in order to execute +/// logic prior to processing the incoming request and/or after the response has been sent back +/// to the Lambda Runtime API. +/// +/// # Example +/// ```no_run +/// use lambda_runtime::{Error, LambdaEvent, Runtime}; +/// use serde_json::Value; +/// use tower::service_fn; +/// +/// #[tokio::main] +/// async fn main() -> Result<(), Error> { +/// let func = service_fn(func); +/// Runtime::new(func).run().await?; +/// Ok(()) +/// } +/// +/// async fn func(event: LambdaEvent) -> Result { +/// Ok(event.payload) +/// } +/// ```` +pub struct Runtime { + service: S, + config: Arc, + client: Arc, + concurrency_limit: u32, + snapstart_resources: Vec>, +} + +impl + Runtime< + RuntimeApiClientService< + RuntimeApiResponseService< + CatchPanicService<'_, F>, + EventPayload, + Response, + BufferedResponse, + StreamingResponse, + StreamItem, + StreamError, + >, + ApiClient, + >, + > +where + F: Service, Response = Response>, + F::Future: Future>, + F::Error: Into + Debug, + EventPayload: for<'de> Deserialize<'de>, + Response: IntoFunctionResponse, + BufferedResponse: Serialize, + StreamingResponse: Stream> + Unpin + Send + 'static, + StreamItem: Into + Send, + StreamError: Into + Send + Debug, +{ + /// Create a new runtime that executes the provided handler for incoming requests. + /// + /// In order to start the runtime and poll for events on the [Lambda Runtime + /// APIs](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html), you must call + /// [Runtime::run]. + /// + /// Note that manually creating a [Runtime] does not add tracing to the executed handler + /// as is done by [super::run]. If you want to add the default tracing functionality, call + /// [Runtime::layer] with a [super::layers::TracingLayer]. + /// + /// + /// # Panics + /// + /// This function panics if required Lambda environment variables are missing + /// (`AWS_LAMBDA_FUNCTION_NAME`, `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`, + /// `AWS_LAMBDA_FUNCTION_VERSION`, `AWS_LAMBDA_RUNTIME_API`). + pub fn new(handler: F) -> Self { + trace!("Loading config from env"); + let config = Arc::new(Config::from_env()); + let concurrency_limit = max_concurrency_from_env().unwrap_or(1).max(1); + // Strategy: allocate all worker tasks up-front, so size the client pool to match. + let pool_size = concurrency_limit as usize; + let client = Arc::new(ApiClient::builder().with_pool_size(pool_size).build()); + Self { + service: wrap_handler(handler, client.clone()), + config, + client, + concurrency_limit, + snapstart_resources: Vec::new(), + } + } +} + +impl Runtime { + /// Add a new layer to this runtime. For an incoming request, this layer will be executed + /// before any layer that has been added prior. + /// + /// # Example + /// ```no_run + /// use lambda_runtime::{layers, Error, LambdaEvent, Runtime}; + /// use serde_json::Value; + /// use tower::service_fn; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Error> { + /// let runtime = Runtime::new(service_fn(echo)).layer( + /// layers::TracingLayer::new() + /// ); + /// runtime.run().await?; + /// Ok(()) + /// } + /// + /// async fn echo(event: LambdaEvent) -> Result { + /// Ok(event.payload) + /// } + /// ``` + pub fn layer(self, layer: L) -> Runtime + where + L: Layer, + L::Service: Service, + { + Runtime { + client: self.client, + config: self.config, + service: layer.layer(self.service), + concurrency_limit: self.concurrency_limit, + snapstart_resources: self.snapstart_resources, + } + } +} + +impl Runtime { + /// Returns `true` if the current execution environment has SnapStart enabled. + pub fn is_snapstart(&self) -> bool { + is_snapstart_env() + } + + /// Register a [`SnapStartResource`] whose `before_snapshot`/`after_restore` + /// hooks should run around the SnapStart snapshot/restore boundary. + /// + /// Register resources in dependency order — **foundations first** (e.g. + /// credentials before the pool that depends on them). The runtime runs + /// `before_snapshot` in reverse registration order (LIFO) and `after_restore` + /// in registration order (FIFO), so teardown and rebuild both happen in the + /// correct relative order. See the [`snapstart`](crate::snapstart) module + /// docs for details. + /// + /// When SnapStart is not enabled, registered resources are never invoked and + /// add no runtime overhead beyond the cost of holding the `Arc`. + /// + /// # Example + /// ```no_run + /// use lambda_runtime::{Error, LambdaEvent, Runtime, SnapStartResource}; + /// use std::sync::Arc; + /// use serde_json::Value; + /// use tower::service_fn; + /// + /// // Uses the default no-op hooks; override before_snapshot/after_restore as needed. + /// struct Pool; + /// impl SnapStartResource for Pool {} + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Error> { + /// let pool = Arc::new(Pool); + /// let runtime = Runtime::new(service_fn(handler)) + /// .register_snapstart_resource(pool.clone()); + /// runtime.run().await + /// } + /// + /// async fn handler(event: LambdaEvent) -> Result { + /// Ok(event.payload) + /// } + /// ``` + pub fn register_snapstart_resource(mut self, resource: Arc) -> Self { + self.snapstart_resources.push(resource); + self + } +} + +#[cfg(feature = "concurrency-tokio")] +impl Runtime +where + S: Service + Clone + Send + 'static, + S::Future: Send, +{ + /// Start the runtime and begin polling for events on the Lambda Runtime API, + /// in a mode that is compatible with Lambda Managed Instances. + /// + /// When `AWS_LAMBDA_MAX_CONCURRENCY` is set to a value greater than 1, this + /// spawns multiple tokio worker tasks to handle concurrent invocations. When the + /// environment variable is unset or `<= 1`, it falls back to sequential + /// behavior, so the same handler can run on both classic Lambda and Lambda + /// Managed Instances. + /// + /// # Panics + /// + /// This function panics if called outside of a Tokio runtime. + #[cfg_attr(docsrs, doc(cfg(feature = "concurrency-tokio")))] + pub async fn run_concurrent(self) -> Result<(), BoxError> { + if tokio::runtime::Handle::try_current().is_err() { + panic!("`run_concurrent` must be called from within a Tokio runtime"); + } + + snapstart_lifecycle(&self.client, &self.snapstart_resources).await?; + + if self.concurrency_limit > 1 { + trace!("Concurrent mode: _X_AMZN_TRACE_ID is not set; use context.xray_trace_id"); + Self::run_concurrent_inner(self.service, self.config, self.client, self.concurrency_limit).await + } else { + debug!( + "Concurrent polling disabled (AWS_LAMBDA_MAX_CONCURRENCY unset or <= 1); falling back to sequential polling" + ); + let incoming = incoming(&self.client); + Self::run_with_incoming(self.service, self.config, incoming).await + } + } + + /// Concurrent processing using N independent long-poll loops (for Lambda managed-concurrency). + async fn run_concurrent_inner( + service: S, + config: Arc, + client: Arc, + concurrency_limit: u32, + ) -> Result<(), BoxError> { + let limit = concurrency_limit as usize; + + debug!( + event = "runtime_worker_pool_initializing", + worker_count = limit, + execution_environment_max_concurrency = limit, + ); + + // Use FuturesUnordered so we can observe worker exits as they happen, + // rather than waiting for all workers to finish (join_all). + let mut workers: FuturesUnordered)>> = + FuturesUnordered::new(); + let spawn_worker = |service: S, config: Arc, client: Arc| { + tokio::spawn(async move { + let task_id = tokio::task::id(); + let result = concurrent_worker_loop(service, config, client).await; + (task_id, result) + }) + }; + // Spawn one worker per concurrency slot; the last uses the owned service to avoid an extra clone. + for _ in 1..limit { + workers.push(spawn_worker(service.clone(), config.clone(), client.clone())); + } + workers.push(spawn_worker(service, config, client)); + + // Track worker exits across tasks. A single worker failing should not + // terminate the whole runtime (LMI keeps running with the remaining + // healthy workers). We only return an error once there are no workers + // left (i.e., we cannot keep at least 1 worker alive). + // + // Note: Handler errors (Err returned from user code) do NOT trigger this. + // They are reported to Lambda via /invocation/{id}/error and the worker + // continues. This only captures unrecoverable runtime failures like + // API client failures, runtime panics, etc. + let mut errors: Vec = Vec::new(); + let mut remaining_workers = limit; + while let Some(result) = futures::StreamExt::next(&mut workers).await { + remaining_workers = remaining_workers.saturating_sub(1); + match result { + Ok((task_id, Ok(()))) => { + // `concurrent_worker_loop` runs indefinitely, so an Ok return indicates + // an unexpected worker exit; we still decrement because the task is gone. + error!( + task_id = %task_id, + remaining_workers, + "Concurrent worker exited cleanly (unexpected - loop should run forever)" + ); + errors.push(WorkerError::CleanExit(task_id)); + } + Ok((task_id, Err(err))) => { + error!( + task_id = %task_id, + error = %err, + remaining_workers, + "Concurrent worker exited with error" + ); + errors.push(WorkerError::Failure(task_id, err)); + } + Err(join_err) => { + let task_id = join_err.id(); + let err: BoxError = Box::new(join_err); + error!( + task_id = %task_id, + error = %err, + remaining_workers, + "Concurrent worker panicked" + ); + errors.push(WorkerError::Failure(task_id, err)); + } + } + } + + match errors.len() { + 0 => Ok(()), + _ => Err(Box::new(ConcurrentWorkerErrors { errors })), + } + } +} + +#[cfg(feature = "concurrency-tokio")] +#[derive(Debug)] +enum WorkerError { + CleanExit(tokio::task::Id), + Failure(tokio::task::Id, BoxError), +} + +#[cfg(feature = "concurrency-tokio")] +#[derive(Debug)] +struct ConcurrentWorkerErrors { + errors: Vec, +} + +#[cfg(feature = "concurrency-tokio")] +#[derive(Serialize)] +struct ConcurrentWorkerErrorsPayload<'a> { + message: &'a str, + #[serde(skip_serializing_if = "Vec::is_empty")] + clean: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + failures: Vec, +} + +#[cfg(feature = "concurrency-tokio")] +#[derive(Serialize)] +struct WorkerFailurePayload { + id: String, + err: String, +} + +#[cfg(feature = "concurrency-tokio")] +impl fmt::Display for ConcurrentWorkerErrors { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut clean = Vec::new(); + let mut failures = Vec::new(); + for error in &self.errors { + match error { + WorkerError::CleanExit(task_id) => clean.push(task_id), + WorkerError::Failure(task_id, err) => failures.push((task_id, err)), + } + } + + let clean_ids: Vec = clean.iter().map(|task_id| task_id.to_string()).collect(); + let failure_entries: Vec = failures + .iter() + .map(|(task_id, err)| WorkerFailurePayload { + id: task_id.to_string(), + err: err.to_string(), + }) + .collect(); + + let message = if failures.is_empty() && !clean.is_empty() { + "all concurrent workers exited cleanly (unexpected - loop should run forever)" + } else { + "concurrent workers exited unexpectedly" + }; + + let payload = ConcurrentWorkerErrorsPayload { + message, + clean: clean_ids, + failures: failure_entries, + }; + let json = serde_json::to_string(&payload).map_err(|_| fmt::Error)?; + write!(f, "{json}") + } +} + +#[cfg(feature = "concurrency-tokio")] +impl std::error::Error for ConcurrentWorkerErrors {} + +impl Runtime +where + S: Service, +{ + /// Start the runtime and begin polling for events on the Lambda Runtime API. + /// + /// The runtime will process requests sequentially. + /// + /// # Managed concurrency + /// If `AWS_LAMBDA_MAX_CONCURRENCY` is set, a warning is logged. + /// If your handler can satisfy `Clone + Send + 'static`, + /// prefer [`Runtime::run_concurrent`] (requires the `concurrency-tokio` feature), + /// which honors managed concurrency and falls back to sequential behavior when + /// unset. + pub async fn run(self) -> Result<(), BoxError> { + if let Some(raw) = concurrency_env_value() { + log_or_print!( + tracing: tracing::warn!( + "AWS_LAMBDA_MAX_CONCURRENCY is set to '{raw}', but the concurrency-tokio feature is not enabled; running sequentially", + ), + fallback: eprintln!("AWS_LAMBDA_MAX_CONCURRENCY is set to '{raw}', but the concurrency-tokio feature is not enabled; running sequentially") + ); + } + snapstart_lifecycle(&self.client, &self.snapstart_resources).await?; + let incoming = incoming(&self.client); + Self::run_with_incoming(self.service, self.config, incoming).await + } + + /// Internal utility function to start the runtime with a customized incoming stream. + /// This implements the core of the [Runtime::run] method. + pub(crate) async fn run_with_incoming( + mut service: S, + config: Arc, + incoming: impl Stream, BoxError>> + Send, + ) -> Result<(), BoxError> { + tokio::pin!(incoming); + while let Some(next_event_response) = incoming.next().await { + trace!("New event arrived (run loop)"); + let event = next_event_response?; + process_invocation(&mut service, &config, event, true).await?; + } + Ok(()) + } +} + +/* ------------------------------------------- UTILS ------------------------------------------- */ + +/// Returns `true` if the current execution environment has SnapStart enabled. +fn is_snapstart_env() -> bool { + env::var("AWS_LAMBDA_INITIALIZATION_TYPE").as_deref() == Ok("snap-start") +} + +/// Runs the SnapStart restore lifecycle when SnapStart is enabled, and is a +/// no-op otherwise. +/// +/// The sequence is: run `before_snapshot` hooks (LIFO) → call `/restore/next` +/// (blocks until the VM is restored) → reset the internal RAPID connection pool +/// → run `after_restore` hooks (FIFO). A failure in the before-snapshot phase +/// (a hook or `/restore/next`) is reported to Lambda via `/init/error`; an +/// `after_restore` failure is reported via `/restore/error`. Either way the +/// error is returned so the runtime exits cleanly (no `process::exit`, so +/// graceful-shutdown handlers still run). +/// +/// This is a free function taking only the fields it needs (rather than `&self`) +/// so it does not borrow the handler `S` across an `.await`, which would require +/// `Runtime: Sync` in the by-value `run`/`run_concurrent` methods. +async fn snapstart_lifecycle( + client: &Arc, + resources: &[Arc], +) -> Result<(), BoxError> { + if !is_snapstart_env() { + return Ok(()); + } + run_restore_lifecycle(client, resources).await +} + +/// Performs the restore lifecycle unconditionally (the env gate lives in the +/// caller, [`snapstart_lifecycle`]). Split out so it can be tested directly +/// without mutating the process-global `AWS_LAMBDA_INITIALIZATION_TYPE`. +async fn run_restore_lifecycle( + client: &Arc, + resources: &[Arc], +) -> Result<(), BoxError> { + // before_snapshot runs in REVERSE registration order (LIFO): tear down + // dependents before their foundations. A failure here happens during init, + // so report it to /init/error. + for resource in resources.iter().rev() { + if let Err(e) = resource.before_snapshot().await { + return Err(report_init_error(client, e).await); + } + } + + // Signal init complete and block until the VM is restored from snapshot. + let req = RestoreNextRequest.into_req()?; + let resp = match client.call(req).await { + Ok(resp) => resp, + // A transport failure calling /restore/next is still an init-phase error. + Err(e) => return Err(report_init_error(client, e).await), + }; + // `call()` only surfaces transport errors; a non-2xx status from RAPID still + // resolves to `Ok`. Treat a failed `/restore/next` as fatal rather than + // silently proceeding into the invocation loop with an un-restored VM. This + // still happens during init, so report it to /init/error. + let status = resp.status(); + if !status.is_success() { + let err: BoxError = format!("/restore/next returned a non-success status: {status}").into(); + return Err(report_init_error(client, err).await); + } + // Stale connections to RAPID won't survive the snapshot; rebuild the pool. + client.reset_pool(); + + // after_restore runs in registration order (FIFO): rebuild foundations + // before the dependents that need them. A failure here is reported to + // /restore/error and propagated so the runtime exits. + for resource in resources { + if let Err(e) = resource.after_restore().await { + return Err(report_restore_error(client, e).await); + } + } + + Ok(()) +} + +/// Reports an init-phase error (a `before_snapshot` hook or `/restore/next`) to +/// Lambda via `/init/error`, then returns the original error so the caller can +/// propagate it and exit the runtime. +async fn report_init_error(client: &Arc, err: BoxError) -> BoxError { + report_diagnostic(client, err, ReportKind::Init).await +} + +/// Reports an after-restore error to Lambda via `/restore/error`, then returns +/// the original error so the caller can propagate it and exit the runtime. +async fn report_restore_error(client: &Arc, err: BoxError) -> BoxError { + report_diagnostic(client, err, ReportKind::Restore).await +} + +enum ReportKind { + Init, + Restore, +} + +/// Posts a `Diagnostic` (built from the error) to the appropriate RAPID error +/// endpoint, then returns the **original** `BoxError` so the caller propagates it +/// with its concrete type and `source()` chain intact. Failures to build or send +/// the report are logged (never swallowed silently) but do not mask the error. +async fn report_diagnostic(client: &Arc, err: BoxError, kind: ReportKind) -> BoxError { + // Build the diagnostic from a reference so we can return `err` untouched. + let diagnostic = Diagnostic { + error_type: crate::diagnostic::type_name_of_val(&err), + error_message: err.to_string(), + }; + + let (endpoint, req) = match kind { + ReportKind::Init => ("/init/error", InitErrorRequest { diagnostic }.into_req()), + ReportKind::Restore => ("/restore/error", RestoreErrorRequest { diagnostic }.into_req()), + }; + + match req { + Ok(req) => { + if let Err(e) = client.call(req).await { + log_or_print!( + tracing: tracing::error!(error = ?e, "failed to report SnapStart error to {endpoint}"), + fallback: eprintln!("failed to report SnapStart error to {endpoint}: {e:?}") + ); + } + } + Err(e) => { + log_or_print!( + tracing: tracing::error!(error = ?e, "failed to build SnapStart {endpoint} request"), + fallback: eprintln!("failed to build SnapStart {endpoint} request: {e:?}") + ); + } + } + + err +} + +#[allow(clippy::type_complexity)] +fn wrap_handler<'a, F, EventPayload, Response, BufferedResponse, StreamingResponse, StreamItem, StreamError>( + handler: F, + client: Arc, +) -> RuntimeApiClientService< + RuntimeApiResponseService< + CatchPanicService<'a, F>, + EventPayload, + Response, + BufferedResponse, + StreamingResponse, + StreamItem, + StreamError, + >, + ApiClient, +> +where + F: Service, Response = Response>, + F::Future: Future>, + F::Error: Into + Debug, + EventPayload: for<'de> Deserialize<'de>, + Response: IntoFunctionResponse, + BufferedResponse: Serialize, + StreamingResponse: Stream> + Unpin + Send + 'static, + StreamItem: Into + Send, + StreamError: Into + Send + Debug, +{ + let safe_service = CatchPanicService::new(handler); + let response_service = RuntimeApiResponseService::new(safe_service); + RuntimeApiClientService::new(response_service, client) +} + +fn incoming( + client: &ApiClient, +) -> impl Stream, BoxError>> + Send + '_ { + async_stream::stream! { + loop { + trace!("Waiting for next event (incoming loop)"); + let req = NextEventRequest.into_req().expect("Unable to construct request"); + let res = client.call(req).await; + yield res; + } + } +} + +/// Creates a future that polls the `/next` endpoint. +#[cfg(feature = "concurrency-tokio")] +async fn next_event_future(client: &ApiClient) -> Result, BoxError> { + let req = NextEventRequest.into_req()?; + client.call(req).await +} + +fn max_concurrency_from_env() -> Option { + env::var("AWS_LAMBDA_MAX_CONCURRENCY") + .ok() + .and_then(|v| v.parse::().ok()) + .filter(|&c| c > 0) +} + +fn concurrency_env_value() -> Option { + env::var("AWS_LAMBDA_MAX_CONCURRENCY").ok() +} + +#[cfg(feature = "concurrency-tokio")] +async fn concurrent_worker_loop(mut service: S, config: Arc, client: Arc) -> Result<(), BoxError> +where + S: Service, + S::Future: Send, +{ + let task_id = tokio::task::id(); + let span = info_span!("worker", task_id = %task_id); + loop { + let event = match next_event_future(client.as_ref()).instrument(span.clone()).await { + Ok(event) => event, + Err(e) => { + warn!(task_id = %task_id, error = %e, "Error polling /next, retrying"); + continue; + } + }; + + process_invocation(&mut service, &config, event, false) + .instrument(span.clone()) + .await?; + } +} + +async fn process_invocation( + service: &mut S, + config: &Arc, + event: http::Response, + set_amzn_trace_env: bool, +) -> Result<(), BoxError> +where + S: Service, +{ + let (parts, incoming) = event.into_parts(); + + #[cfg(debug_assertions)] + if parts.status == http::StatusCode::NO_CONTENT { + // Ignore the event if the status code is 204. + // This is a way to keep the runtime alive when + // there are no events pending to be processed. + return Ok(()); + } + + // Build the invocation such that it can be sent to the service right away + // when it is ready + let body = incoming.collect().await?.to_bytes(); + let context = Context::new(invoke_request_id(&parts.headers)?, config.clone(), &parts.headers)?; + let invocation = LambdaInvocation { parts, body, context }; + + if set_amzn_trace_env { + // Setup Amazon's default tracing data + amzn_trace_env(&invocation.context); + } + + // Wait for service to be ready + let ready = service.ready().await?; + + // Once ready, call the service which will respond to the Lambda runtime API + ready.call(invocation).await?; + Ok(()) +} + +fn amzn_trace_env(ctx: &Context) { + match &ctx.xray_trace_id { + Some(trace_id) => env::set_var("_X_AMZN_TRACE_ID", trace_id), + None => env::remove_var("_X_AMZN_TRACE_ID"), + } +} + +/* --------------------------------------------------------------------------------------------- */ +/* TESTS */ +/* --------------------------------------------------------------------------------------------- */ + +#[cfg(test)] +mod endpoint_tests { + use super::{incoming, wrap_handler}; + use crate::{ + requests::{EventCompletionRequest, EventErrorRequest, IntoRequest, NextEventRequest}, + BoxFuture, Config, Diagnostic, Error, Runtime, + }; + use bytes::Bytes; + use http::{HeaderValue, Method, Request, Response, StatusCode}; + use http_body_util::{BodyExt, Full}; + use httpmock::prelude::*; + + use hyper::{body::Incoming, service::service_fn}; + use hyper_util::{ + rt::{tokio::TokioIo, TokioExecutor}, + server::conn::auto::Builder as ServerBuilder, + }; + use lambda_runtime_api_client::PooledClient as Client; + use std::{ + convert::Infallible, + env, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, + time::Duration, + }; + use tokio::{net::TcpListener, sync::Notify}; + use tokio_stream::StreamExt; + + #[tokio::test] + async fn test_next_event() -> Result<(), Error> { + let server = MockServer::start(); + let request_id = "156cb537-e2d4-11e8-9b34-d36013741fb9"; + let deadline = "1542409706888"; + + let mock = server.mock(|when, then| { + when.method(GET).path("/2018-06-01/runtime/invocation/next"); + then.status(200) + .header("content-type", "application/json") + .header("lambda-runtime-aws-request-id", request_id) + .header("lambda-runtime-deadline-ms", deadline) + .body("{}"); + }); + + let base = server.base_url().parse().expect("Invalid mock server Uri"); + let client = Client::builder().with_endpoint(base).build(); + + let req = NextEventRequest.into_req()?; + let rsp = client.call(req).await.expect("Unable to send request"); + + mock.assert_async().await; + assert_eq!(rsp.status(), StatusCode::OK); + assert_eq!( + rsp.headers()["lambda-runtime-aws-request-id"], + &HeaderValue::from_static(request_id) + ); + assert_eq!( + rsp.headers()["lambda-runtime-deadline-ms"], + &HeaderValue::from_static(deadline) + ); + + let body = rsp.into_body().collect().await?.to_bytes(); + assert_eq!("{}", std::str::from_utf8(&body)?); + Ok(()) + } + + #[tokio::test] + async fn test_ok_response() -> Result<(), Error> { + let server = MockServer::start(); + + let mock = server.mock(|when, then| { + when.method(POST) + .path("/2018-06-01/runtime/invocation/156cb537-e2d4-11e8-9b34-d36013741fb9/response") + .body("\"{}\""); + then.status(200).body(""); + }); + + let base = server.base_url().parse().expect("Invalid mock server Uri"); + let client = Client::builder().with_endpoint(base).build(); + + let req = EventCompletionRequest::new( + "156cb537-e2d4-11e8-9b34-d36013741fb9", + Option::Some("invocation_id"), + "{}", + ); + let req = req.into_req()?; + + let rsp = client.call(req).await?; + + mock.assert_async().await; + assert_eq!(rsp.status(), StatusCode::OK); + Ok(()) + } + + #[tokio::test] + async fn test_error_response() -> Result<(), Error> { + let diagnostic = Diagnostic { + error_type: "InvalidEventDataError".into(), + error_message: "Error parsing event data".into(), + }; + let body = serde_json::to_string(&diagnostic)?; + + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(POST) + .path("/2018-06-01/runtime/invocation/156cb537-e2d4-11e8-9b34-d36013741fb9/error") + .header("lambda-runtime-function-error-type", "unhandled") + .body(body); + then.status(200).body(""); + }); + + let base = server.base_url().parse().expect("Invalid mock server Uri"); + let client = Client::builder().with_endpoint(base).build(); + + let req = EventErrorRequest { + request_id: "156cb537-e2d4-11e8-9b34-d36013741fb9", + invocation_id: Option::Some("invocation_id"), + diagnostic, + }; + let req = req.into_req()?; + let rsp = client.call(req).await?; + + mock.assert_async().await; + assert_eq!(rsp.status(), StatusCode::OK); + Ok(()) + } + + #[tokio::test] + async fn successful_end_to_end_run() -> Result<(), Error> { + let server = MockServer::start(); + let request_id = "156cb537-e2d4-11e8-9b34-d36013741fb9"; + let deadline = "1542409706888"; + + let next_request = server.mock(|when, then| { + when.method(GET).path("/2018-06-01/runtime/invocation/next"); + then.status(200) + .header("content-type", "application/json") + .header("lambda-runtime-aws-request-id", request_id) + .header("lambda-runtime-deadline-ms", deadline) + .body("{}"); + }); + let next_response = server.mock(|when, then| { + when.method(POST) + .path(format!("/2018-06-01/runtime/invocation/{request_id}/response")) + .body("{}"); + then.status(200).body(""); + }); + + let base = server.base_url().parse().expect("Invalid mock server Uri"); + let client = Client::builder().with_endpoint(base).build(); + + async fn func(event: crate::LambdaEvent) -> Result { + let (event, _) = event.into_parts(); + Ok(event) + } + let f = crate::service_fn(func); + + // set env vars needed to init Config if they are not already set in the environment + if env::var("AWS_LAMBDA_RUNTIME_API").is_err() { + env::set_var("AWS_LAMBDA_RUNTIME_API", server.base_url()); + } + if env::var("AWS_LAMBDA_FUNCTION_NAME").is_err() { + env::set_var("AWS_LAMBDA_FUNCTION_NAME", "test_fn"); + } + if env::var("AWS_LAMBDA_FUNCTION_MEMORY_SIZE").is_err() { + env::set_var("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "128"); + } + if env::var("AWS_LAMBDA_FUNCTION_VERSION").is_err() { + env::set_var("AWS_LAMBDA_FUNCTION_VERSION", "1"); + } + if env::var("AWS_LAMBDA_LOG_STREAM_NAME").is_err() { + env::set_var("AWS_LAMBDA_LOG_STREAM_NAME", "test_stream"); + } + if env::var("AWS_LAMBDA_LOG_GROUP_NAME").is_err() { + env::set_var("AWS_LAMBDA_LOG_GROUP_NAME", "test_log"); + } + let config = Config::from_env(); + + let client = Arc::new(client); + let runtime = Runtime { + client: client.clone(), + config: Arc::new(config), + service: wrap_handler(f, client), + concurrency_limit: 1, + snapstart_resources: Vec::new(), + }; + let client = &runtime.client; + let incoming = incoming(client).take(1); + Runtime::run_with_incoming(runtime.service, runtime.config, incoming).await?; + + next_request.assert_async().await; + next_response.assert_async().await; + Ok(()) + } + + async fn run_panicking_handler(func: F) -> Result<(), Error> + where + F: FnMut(crate::LambdaEvent) -> BoxFuture<'static, Result> + + Send + + 'static, + { + let server = MockServer::start(); + let request_id = "156cb537-e2d4-11e8-9b34-d36013741fb9"; + let deadline = "1542409706888"; + + let next_request = server.mock(|when, then| { + when.method(GET).path("/2018-06-01/runtime/invocation/next"); + then.status(200) + .header("content-type", "application/json") + .header("lambda-runtime-aws-request-id", request_id) + .header("lambda-runtime-deadline-ms", deadline) + .body("{}"); + }); + + let next_response = server.mock(|when, then| { + when.method(POST) + .path(format!("/2018-06-01/runtime/invocation/{request_id}/error")) + .header("lambda-runtime-function-error-type", "unhandled"); + then.status(200).body(""); + }); + + let base = server.base_url().parse().expect("Invalid mock server Uri"); + let client = Client::builder().with_endpoint(base).build(); + + let f = crate::service_fn(func); + + let config = Arc::new(Config { + function_name: "test_fn".to_string(), + memory: 128, + version: "1".to_string(), + log_stream: "test_stream".to_string(), + log_group: "test_log".to_string(), + }); + + let client = Arc::new(client); + let runtime = Runtime { + client: client.clone(), + config, + service: wrap_handler(f, client), + concurrency_limit: 1, + snapstart_resources: Vec::new(), + }; + let client = &runtime.client; + let incoming = incoming(client).take(1); + Runtime::run_with_incoming(runtime.service, runtime.config, incoming).await?; + + next_request.assert_async().await; + next_response.assert_async().await; + Ok(()) + } + + #[tokio::test] + async fn panic_in_async_run() -> Result<(), Error> { + run_panicking_handler(|_| Box::pin(async { panic!("This is intentionally here") })).await + } + + #[tokio::test] + async fn panic_outside_async_run() -> Result<(), Error> { + run_panicking_handler(|_| { + panic!("This is intentionally here"); + }) + .await + } + + #[cfg(feature = "concurrency-tokio")] + #[tokio::test] + async fn concurrent_worker_crash_does_not_stop_other_workers() -> Result<(), Error> { + let next_calls = Arc::new(AtomicUsize::new(0)); + let response_calls = Arc::new(AtomicUsize::new(0)); + let first_error_served = Arc::new(Notify::new()); + + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let base: http::Uri = format!("http://{addr}").parse().unwrap(); + + let server_handle = { + let next_calls = next_calls.clone(); + let response_calls = response_calls.clone(); + let first_error_served = first_error_served.clone(); + tokio::spawn(async move { + loop { + let (tcp, _) = match listener.accept().await { + Ok(v) => v, + Err(_) => return, + }; + + let next_calls = next_calls.clone(); + let response_calls = response_calls.clone(); + let first_error_served = first_error_served.clone(); + let service = service_fn(move |req: Request| { + let next_calls = next_calls.clone(); + let response_calls = response_calls.clone(); + let first_error_served = first_error_served.clone(); + async move { + let (parts, body) = req.into_parts(); + let method = parts.method; + let path = parts.uri.path().to_string(); + + if method == Method::POST { + // Drain request body to support keep-alive. + let _ = body.collect().await; + } + + if method == Method::GET && path == "/2018-06-01/runtime/invocation/next" { + let call_index = next_calls.fetch_add(1, Ordering::SeqCst); + match call_index { + // First worker errors (missing request id header). + 0 => { + first_error_served.notify_one(); + let res = Response::builder() + .status(StatusCode::OK) + .header("lambda-runtime-deadline-ms", "1542409706888") + .body(Full::new(Bytes::from_static(b"{}"))) + .unwrap(); + return Ok::<_, Infallible>(res); + } + // Second worker should keep running and process an invocation, even if another worker errors. + 1 => { + first_error_served.notified().await; + let res = Response::builder() + .status(StatusCode::OK) + .header("content-type", "application/json") + .header("lambda-runtime-aws-request-id", "good-request") + .header("lambda-runtime-deadline-ms", "1542409706888") + .body(Full::new(Bytes::from_static(b"{}"))) + .unwrap(); + return Ok::<_, Infallible>(res); + } + // Finally, error the remaining worker so the runtime can terminate and the test can assert behavior. + 2 => { + let res = Response::builder() + .status(StatusCode::OK) + .header("lambda-runtime-deadline-ms", "1542409706888") + .body(Full::new(Bytes::from_static(b"{}"))) + .unwrap(); + return Ok::<_, Infallible>(res); + } + _ => { + let res = Response::builder() + .status(StatusCode::NO_CONTENT) + .body(Full::new(Bytes::new())) + .unwrap(); + return Ok::<_, Infallible>(res); + } + } + } + + if method == Method::POST && path.ends_with("/response") { + response_calls.fetch_add(1, Ordering::SeqCst); + let res = Response::builder() + .status(StatusCode::OK) + .body(Full::new(Bytes::new())) + .unwrap(); + return Ok::<_, Infallible>(res); + } + + let res = Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Full::new(Bytes::new())) + .unwrap(); + Ok::<_, Infallible>(res) + } + }); + + let io = TokioIo::new(tcp); + tokio::spawn(async move { + if let Err(err) = ServerBuilder::new(TokioExecutor::new()) + .serve_connection(io, service) + .await + { + eprintln!("Error serving connection: {err:?}"); + } + }); + } + }) + }; + + async fn func(event: crate::LambdaEvent) -> Result { + Ok(event.payload) + } + + let handler = crate::service_fn(func); + let client = Arc::new(Client::builder().with_endpoint(base).build()); + let runtime = Runtime { + client: client.clone(), + config: Arc::new(Config { + function_name: "test_fn".to_string(), + memory: 128, + version: "1".to_string(), + log_stream: "test_stream".to_string(), + log_group: "test_log".to_string(), + }), + service: wrap_handler(handler, client), + concurrency_limit: 2, + snapstart_resources: Vec::new(), + }; + + let res = tokio::time::timeout(Duration::from_secs(2), runtime.run_concurrent()).await; + assert!(res.is_ok(), "run_concurrent timed out"); + assert!( + res.unwrap().is_err(), + "expected runtime to terminate once all workers crashed" + ); + + assert_eq!( + response_calls.load(Ordering::SeqCst), + 1, + "expected remaining worker to keep running after a worker crash" + ); + + server_handle.abort(); + Ok(()) + } + + #[cfg(feature = "concurrency-tokio")] + // Must be current-thread (the default) so the thread-local tracing + // subscriber set via `set_default` propagates to spawned tasks. + #[tokio::test] + async fn test_concurrent_structured_logging_isolation() -> Result<(), Error> { + use std::collections::HashSet; + use tracing::info; + use tracing_capture::{CaptureLayer, SharedStorage}; + use tracing_subscriber::layer::SubscriberExt; + + let storage = SharedStorage::default(); + let subscriber = tracing_subscriber::registry().with(CaptureLayer::new(&storage)); + let _guard = tracing::subscriber::set_default(subscriber); + + let request_count = Arc::new(AtomicUsize::new(0)); + let done = Arc::new(tokio::sync::Notify::new()); + let listener = TcpListener::bind("127.0.0.1:0").await?; + let addr = listener.local_addr()?; + let base: http::Uri = format!("http://{addr}").parse()?; + + let server_handle = { + let request_count = request_count.clone(); + let done = done.clone(); + tokio::spawn(async move { + loop { + let (tcp, _) = match listener.accept().await { + Ok(v) => v, + Err(_) => return, + }; + + let request_count = request_count.clone(); + let done = done.clone(); + let service = service_fn(move |req: Request| { + let request_count = request_count.clone(); + let done = done.clone(); + async move { + let (parts, body) = req.into_parts(); + if parts.method == Method::POST { + let _ = body.collect().await; + } + + if parts.method == Method::GET && parts.uri.path() == "/2018-06-01/runtime/invocation/next" + { + let count = request_count.fetch_add(1, Ordering::SeqCst); + if count < 300 { + let request_id = format!("test-request-{}", count + 1); + let res = Response::builder() + .status(StatusCode::OK) + .header("lambda-runtime-aws-request-id", &request_id) + .header("lambda-runtime-deadline-ms", "9999999999999") + .body(Full::new(Bytes::from_static(b"{}"))) + .unwrap(); + return Ok::<_, Infallible>(res); + } else { + done.notify_one(); + let res = Response::builder() + .status(StatusCode::NO_CONTENT) + .body(Full::new(Bytes::new())) + .unwrap(); + return Ok::<_, Infallible>(res); + } + } + + if parts.method == Method::POST && parts.uri.path().contains("/response") { + let res = Response::builder() + .status(StatusCode::OK) + .body(Full::new(Bytes::new())) + .unwrap(); + return Ok::<_, Infallible>(res); + } + + let res = Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Full::new(Bytes::new())) + .unwrap(); + Ok::<_, Infallible>(res) + } + }); + + let io = TokioIo::new(tcp); + tokio::spawn(async move { + let _ = ServerBuilder::new(TokioExecutor::new()) + .serve_connection(io, service) + .await; + }); + } + }) + }; + + async fn test_handler(event: crate::LambdaEvent) -> Result<(), Error> { + let request_id = &event.context.request_id; + info!(observed_request_id = request_id); + tokio::time::sleep(Duration::from_millis(100)).await; + Ok(()) + } + + let handler = crate::service_fn(test_handler); + let client = Arc::new(Client::builder().with_endpoint(base).build()); + + // Add tracing layer to capture span fields + use crate::layers::trace::TracingLayer; + use tower::ServiceBuilder; + let service = ServiceBuilder::new() + .layer(TracingLayer::new()) + .service(wrap_handler(handler, client.clone())); + + let runtime = Runtime { + client: client.clone(), + config: Arc::new(Config { + function_name: "test_fn".to_string(), + memory: 128, + version: "1".to_string(), + log_stream: "test_stream".to_string(), + log_group: "test_log".to_string(), + }), + service, + concurrency_limit: 3, + snapstart_resources: Vec::new(), + }; + + let runtime_handle = tokio::spawn(async move { runtime.run_concurrent().await }); + + done.notified().await; + // Give handlers time to complete after server signals done + tokio::time::sleep(Duration::from_millis(500)).await; + + runtime_handle.abort(); + server_handle.abort(); + + let storage = storage.lock(); + let events: Vec<_> = storage + .all_events() + .filter(|e| e.value("observed_request_id").is_some()) + .collect(); + + assert!( + events.len() >= 300, + "Should have at least 300 log entries, got {}", + events.len() + ); + + let mut seen_ids = HashSet::new(); + for event in &events { + let observed_id = event["observed_request_id"].as_str().unwrap(); + + // Find the parent "Lambda runtime invoke" span and get its requestId + let span_request_id = event + .ancestors() + .find(|s| s.metadata().name() == "Lambda runtime invoke") + .and_then(|s| s.value("requestId")) + .and_then(|v| v.as_str()) + .expect("Event should have a Lambda runtime invoke ancestor with requestId"); + + assert!( + observed_id.starts_with("test-request-"), + "Request ID should match pattern: {}", + observed_id + ); + assert!( + seen_ids.insert(observed_id.to_string()), + "Request ID should be unique: {}", + observed_id + ); + + // Verify span request ID matches logged request ID + assert_eq!( + observed_id, span_request_id, + "Span request ID should match logged request ID: span={}, logged={}", + span_request_id, observed_id + ); + } + + Ok(()) + } + + /// Records the order in which `before_snapshot`/`after_restore` fire across + /// resources, so tests can assert LIFO/FIFO ordering. + struct OrderRecorder { + label: &'static str, + log: Arc>>, + } + + impl crate::SnapStartResource for OrderRecorder { + fn before_snapshot(&self) -> BoxFuture<'_, Result<(), Error>> { + let log = self.log.clone(); + let label = self.label; + Box::pin(async move { + log.lock().unwrap().push(format!("before:{label}")); + Ok(()) + }) + } + + fn after_restore(&self) -> BoxFuture<'_, Result<(), Error>> { + let log = self.log.clone(); + let label = self.label; + Box::pin(async move { + log.lock().unwrap().push(format!("after:{label}")); + Ok(()) + }) + } + } + + /// Which lifecycle phase a [`FailingResource`] should fail in. + #[derive(Clone, Copy)] + enum Phase { + BeforeSnapshot, + AfterRestore, + } + + /// A resource that returns an error from the selected phase (and is a no-op + /// in the other), used to exercise the error-reporting paths. + struct FailingResource { + phase: Phase, + } + + impl crate::SnapStartResource for FailingResource { + fn before_snapshot(&self) -> BoxFuture<'_, Result<(), Error>> { + let fail = matches!(self.phase, Phase::BeforeSnapshot); + Box::pin(async move { + if fail { + Err("before_snapshot failed".into()) + } else { + Ok(()) + } + }) + } + + fn after_restore(&self) -> BoxFuture<'_, Result<(), Error>> { + let fail = matches!(self.phase, Phase::AfterRestore); + Box::pin(async move { + if fail { + Err("after_restore failed".into()) + } else { + Ok(()) + } + }) + } + } + + #[tokio::test] + async fn test_snapstart_restore_lifecycle_calls_restore_next() -> Result<(), Error> { + let server = MockServer::start(); + + let restore_mock = server.mock(|when, then| { + when.method(GET).path("/2018-06-01/runtime/restore/next"); + then.status(200).body(""); + }); + + let base = server.base_url().parse().expect("Invalid mock server Uri"); + let client = Arc::new(Client::builder().with_endpoint(base).build()); + + let log = Arc::new(std::sync::Mutex::new(Vec::new())); + let resources: Vec> = vec![Arc::new(OrderRecorder { + label: "pool", + log: log.clone(), + })]; + + // Exercise the restore lifecycle directly (no env mutation, no races with + // other tests that call run()/run_concurrent()). + super::run_restore_lifecycle(&client, &resources).await?; + + restore_mock.assert_async().await; + let recorded = log.lock().unwrap().clone(); + assert_eq!(recorded, vec!["before:pool", "after:pool"]); + Ok(()) + } + + #[tokio::test] + async fn test_snapstart_resource_ordering_is_lifo_then_fifo() -> Result<(), Error> { + let server = MockServer::start(); + server.mock(|when, then| { + when.method(GET).path("/2018-06-01/runtime/restore/next"); + then.status(200).body(""); + }); + + let base = server.base_url().parse().expect("Invalid mock server Uri"); + let client = Arc::new(Client::builder().with_endpoint(base).build()); + + let log = Arc::new(std::sync::Mutex::new(Vec::new())); + // Registration order: credentials → pool → cache (foundations first). + let resources: Vec> = vec![ + Arc::new(OrderRecorder { + label: "credentials", + log: log.clone(), + }), + Arc::new(OrderRecorder { + label: "pool", + log: log.clone(), + }), + Arc::new(OrderRecorder { + label: "cache", + log: log.clone(), + }), + ]; + + super::run_restore_lifecycle(&client, &resources).await?; + + let recorded = log.lock().unwrap().clone(); + assert_eq!( + recorded, + vec![ + // before_snapshot: LIFO (reverse registration) + "before:cache", + "before:pool", + "before:credentials", + // after_restore: FIFO (registration order) + "after:credentials", + "after:pool", + "after:cache", + ] + ); + Ok(()) + } + + #[tokio::test] + async fn test_snapstart_restore_next_non_success_is_fatal() -> Result<(), Error> { + let server = MockServer::start(); + server.mock(|when, then| { + when.method(GET).path("/2018-06-01/runtime/restore/next"); + then.status(500).body(""); + }); + let init_err_mock = server.mock(|when, then| { + when.method(POST).path("/2018-06-01/runtime/init/error"); + then.status(202).body(""); + }); + + let base = server.base_url().parse().expect("Invalid mock server Uri"); + let client = Arc::new(Client::builder().with_endpoint(base).build()); + + let log = Arc::new(std::sync::Mutex::new(Vec::new())); + let resources: Vec> = vec![Arc::new(OrderRecorder { + label: "pool", + log: log.clone(), + })]; + + let result = super::run_restore_lifecycle(&client, &resources).await; + + // A non-2xx /restore/next must surface as an error rather than silently + // continuing into the invocation loop with an un-restored VM, and it must + // be reported to /init/error. + assert!(result.is_err(), "expected non-success /restore/next to be fatal"); + init_err_mock.assert_async().await; + // before_snapshot ran; after_restore must NOT run when the restore call fails. + let recorded = log.lock().unwrap().clone(); + assert_eq!(recorded, vec!["before:pool"]); + Ok(()) + } + + #[tokio::test] + async fn test_snapstart_lifecycle_is_noop_when_not_snapstart() -> Result<(), Error> { + // Only meaningful when the SnapStart env var is absent. Never mutate the + // global env (would race with parallel tests); skip if it's set. + if env::var("AWS_LAMBDA_INITIALIZATION_TYPE").is_ok() { + return Ok(()); + } + + let server = MockServer::start(); + let restore_mock = server.mock(|when, then| { + when.method(GET).path("/2018-06-01/runtime/restore/next"); + then.status(200).body(""); + }); + + let base = server.base_url().parse().expect("Invalid mock server Uri"); + let client = Arc::new(Client::builder().with_endpoint(base).build()); + + let log = Arc::new(std::sync::Mutex::new(Vec::new())); + let resources: Vec> = vec![Arc::new(OrderRecorder { + label: "pool", + log: log.clone(), + })]; + + // The env-gated entry point must do nothing when SnapStart is not enabled. + super::snapstart_lifecycle(&client, &resources).await?; + + assert_eq!( + restore_mock.calls(), + 0, + "/restore/next must not be called when not snap-start" + ); + assert!( + log.lock().unwrap().is_empty(), + "no hooks should fire when not snap-start" + ); + Ok(()) + } + + #[tokio::test] + async fn test_before_snapshot_failure_reports_init_error() -> Result<(), Error> { + let server = MockServer::start(); + let init_err_mock = server.mock(|when, then| { + when.method(POST).path("/2018-06-01/runtime/init/error"); + then.status(202).body(""); + }); + let restore_mock = server.mock(|when, then| { + when.method(GET).path("/2018-06-01/runtime/restore/next"); + then.status(200).body(""); + }); + + let base = server.base_url().parse().expect("Invalid mock server Uri"); + let client = Arc::new(Client::builder().with_endpoint(base).build()); + + let resources: Vec> = vec![Arc::new(FailingResource { + phase: Phase::BeforeSnapshot, + })]; + + let result = super::run_restore_lifecycle(&client, &resources).await; + + assert!(result.is_err(), "before_snapshot failure must propagate"); + init_err_mock.assert_async().await; + // /restore/next must NOT be reached when before_snapshot fails. + assert_eq!(restore_mock.calls(), 0); + Ok(()) + } + + #[tokio::test] + async fn test_after_restore_failure_reports_restore_error() -> Result<(), Error> { + let server = MockServer::start(); + let restore_mock = server.mock(|when, then| { + when.method(GET).path("/2018-06-01/runtime/restore/next"); + then.status(200).body(""); + }); + let restore_err_mock = server.mock(|when, then| { + when.method(POST).path("/2018-06-01/runtime/restore/error"); + then.status(202).body(""); + }); + + let base = server.base_url().parse().expect("Invalid mock server Uri"); + let client = Arc::new(Client::builder().with_endpoint(base).build()); + + let resources: Vec> = vec![Arc::new(FailingResource { + phase: Phase::AfterRestore, + })]; + + let result = super::run_restore_lifecycle(&client, &resources).await; + + // Now that the runtime no longer calls process::exit, the after_restore + // failure path returns an error and is unit-testable. + assert!(result.is_err(), "after_restore failure must propagate"); + restore_mock.assert_async().await; + restore_err_mock.assert_async().await; + Ok(()) + } +} diff --git a/lambda-runtime/src/simulated.rs b/lambda-runtime/src/simulated.rs deleted file mode 100644 index 4fcc31066..000000000 --- a/lambda-runtime/src/simulated.rs +++ /dev/null @@ -1,100 +0,0 @@ -use http::Uri; -use hyper::client::connect::Connection; -use std::{ - collections::HashMap, - future::Future, - io::Result as IoResult, - pin::Pin, - sync::{Arc, Mutex}, - task::{Context, Poll}, -}; -use tokio::io::{AsyncRead, AsyncWrite, DuplexStream, ReadBuf}; - -use crate::Error; - -#[derive(Clone)] -pub struct Connector { - inner: Arc>>, -} - -pub struct DuplexStreamWrapper(DuplexStream); - -impl DuplexStreamWrapper { - pub(crate) fn new(stream: DuplexStream) -> DuplexStreamWrapper { - DuplexStreamWrapper(stream) - } -} - -impl Connector { - pub fn new() -> Self { - #[allow(clippy::mutable_key_type)] - let map = HashMap::new(); - Connector { - inner: Arc::new(Mutex::new(map)), - } - } - - pub fn insert(&self, uri: Uri, stream: DuplexStreamWrapper) -> Result<(), Error> { - match self.inner.lock() { - Ok(mut map) => { - map.insert(uri, stream); - Ok(()) - } - Err(_) => Err("mutex was poisoned".into()), - } - } - - pub fn with(uri: Uri, stream: DuplexStreamWrapper) -> Result { - let connector = Connector::new(); - match connector.insert(uri, stream) { - Ok(_) => Ok(connector), - Err(e) => Err(e), - } - } -} - -impl hyper::service::Service for Connector { - type Response = DuplexStreamWrapper; - type Error = crate::Error; - #[allow(clippy::type_complexity)] - type Future = Pin> + Send>>; - - fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, uri: Uri) -> Self::Future { - let res = match self.inner.lock() { - Ok(mut map) if map.contains_key(&uri) => Ok(map.remove(&uri).unwrap()), - Ok(_) => Err(format!("Uri {} is not in map", uri).into()), - Err(_) => Err("mutex was poisoned".into()), - }; - Box::pin(async move { res }) - } -} - -impl Connection for DuplexStreamWrapper { - fn connected(&self) -> hyper::client::connect::Connected { - hyper::client::connect::Connected::new() - } -} - -impl AsyncRead for DuplexStreamWrapper { - fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll> { - Pin::new(&mut self.0).poll_read(cx, buf) - } -} - -impl AsyncWrite for DuplexStreamWrapper { - fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { - Pin::new(&mut self.0).poll_write(cx, buf) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - Pin::new(&mut self.0).poll_flush(cx) - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - Pin::new(&mut self.0).poll_shutdown(cx) - } -} diff --git a/lambda-runtime/src/snapstart.rs b/lambda-runtime/src/snapstart.rs new file mode 100644 index 000000000..c9d81b11d --- /dev/null +++ b/lambda-runtime/src/snapstart.rs @@ -0,0 +1,104 @@ +//! SnapStart lifecycle support. +//! +//! AWS Lambda SnapStart reduces cold start latency by taking a Firecracker +//! microVM snapshot of the initialized execution environment and restoring from +//! it on subsequent invocations. Resources initialized before the snapshot may +//! become invalid after restore: TCP connections are dropped, credentials may +//! expire, and unique values generated during init would be shared across every +//! restored instance. +//! +//! Implement [`SnapStartResource`] on types that hold such resources and +//! register them with +//! [`Runtime::register_snapstart_resource`](crate::Runtime::register_snapstart_resource). +//! The runtime invokes the registered hooks around the snapshot/restore boundary +//! when SnapStart is enabled (`AWS_LAMBDA_INITIALIZATION_TYPE == "snap-start"`) +//! and does nothing otherwise. +//! +//! # Ordering +//! +//! Resources are orchestrated with stack (LIFO/FIFO) ordering, mirroring how +//! Go's `defer`, middleware unwinding, and Rust destructors work: +//! +//! - [`before_snapshot`](SnapStartResource::before_snapshot) runs in **reverse** +//! registration order (LIFO) — dependents tear down before their foundations. +//! - [`after_restore`](SnapStartResource::after_restore) runs in **registration** +//! order (FIFO) — foundations rebuild before the dependents that need them. +//! +//! The rule for users is one line: **register foundations first** (e.g. +//! credentials before the pool that uses them, the pool before the cache that +//! uses it). Teardown and rebuild then both happen in the correct relative order +//! automatically: +//! +//! ```text +//! register: credentials → pool → cache +//! before_snapshot: cache → pool → credentials (reverse) +//! after_restore: credentials → pool → cache (forward) +//! ``` +//! +//! # Example +//! +//! ```no_run +//! use lambda_runtime::{BoxFuture, Error, SnapStartResource}; +//! +//! struct DbPool { +//! // e.g. a connection pool handle +//! } +//! +//! impl SnapStartResource for DbPool { +//! fn before_snapshot(&self) -> BoxFuture<'_, Result<(), Error>> { +//! Box::pin(async move { +//! // release connections before the snapshot +//! Ok(()) +//! }) +//! } +//! +//! fn after_restore(&self) -> BoxFuture<'_, Result<(), Error>> { +//! Box::pin(async move { +//! // re-establish connections after restore +//! Ok(()) +//! }) +//! } +//! } +//! ``` + +use crate::{BoxFuture, Error}; + +/// A resource that needs custom logic around the SnapStart snapshot/restore +/// boundary. +/// +/// Implement this trait on types holding snapshot-sensitive state (connection +/// pools, credentials, cached DNS, etc.) and register them with +/// [`Runtime::register_snapstart_resource`](crate::Runtime::register_snapstart_resource). +/// +/// Both methods default to a no-op, so implementors only override the hook they +/// need. See the [module docs](crate::snapstart) for ordering semantics. +/// +/// Each method returns a [`BoxFuture`] (rather than being an `async fn`) so the +/// trait stays object-safe and can be stored as `dyn SnapStartResource` without +/// pulling in the `async-trait` crate. Implementations wrap their body in +/// `Box::pin(async move { .. })`. +pub trait SnapStartResource: Send + Sync { + /// Called before the VM snapshot is taken. + /// + /// Use this to release resources that will not survive the snapshot, such as + /// open connections or buffered data. + /// + /// Runs in reverse registration order (LIFO) across all registered + /// resources. + fn before_snapshot(&self) -> BoxFuture<'_, Result<(), Error>> { + Box::pin(async { Ok(()) }) + } + + /// Called after the VM is restored from a snapshot. + /// + /// Use this to re-establish resources released in + /// [`before_snapshot`](Self::before_snapshot), refresh credentials, or + /// regenerate values that must be unique per execution environment. + /// + /// Runs in registration order (FIFO) across all registered resources. If + /// this returns an error, the runtime reports it to Lambda via + /// `/restore/error` and the runtime exits. + fn after_restore(&self) -> BoxFuture<'_, Result<(), Error>> { + Box::pin(async { Ok(()) }) + } +} diff --git a/lambda-runtime/src/streaming.rs b/lambda-runtime/src/streaming.rs new file mode 100644 index 000000000..4f0c8083d --- /dev/null +++ b/lambda-runtime/src/streaming.rs @@ -0,0 +1,35 @@ +pub use lambda_runtime_api_client::body::{sender::Sender, Body}; + +pub use crate::types::StreamResponse as Response; + +/// Create a new `Body` stream with associated Sender half. +/// +/// Examples +/// +/// ``` +/// use lambda_runtime::{ +/// streaming::{channel, Body, Response}, +/// Error, LambdaEvent, +/// }; +/// use std::{thread, time::Duration}; +/// +/// async fn func(_event: LambdaEvent) -> Result, Error> { +/// let messages = vec!["Hello", "world", "from", "Lambda!"]; +/// +/// let (mut tx, rx) = channel(); +/// +/// tokio::spawn(async move { +/// for message in messages.iter() { +/// tx.send_data((message.to_string() + "\n").into()).await.unwrap(); +/// thread::sleep(Duration::from_millis(500)); +/// } +/// }); +/// +/// Ok(Response::from(rx)) +/// } +/// ``` +#[allow(unused)] +#[inline] +pub fn channel() -> (Sender, Body) { + Body::channel() +} diff --git a/lambda-runtime/src/types.rs b/lambda-runtime/src/types.rs index 17eb312b0..edd932402 100644 --- a/lambda-runtime/src/types.rs +++ b/lambda-runtime/src/types.rs @@ -1,42 +1,22 @@ -use crate::{Config, Error}; -use http::{HeaderMap, HeaderValue}; +use crate::{ + constants::{ + LAMBDA_RUNTIME_CLIENT_CONTEXT, LAMBDA_RUNTIME_COGNITO_IDENTITY, LAMBDA_RUNTIME_DEADLINE_MS, + LAMBDA_RUNTIME_INVOKED_FUNCTION_ARN, LAMBDA_RUNTIME_REQUEST_ID, LAMBDA_RUNTIME_TENANT_ID, + LAMBDA_RUNTIME_TRACE_ID, + }, + Error, RefConfig, +}; +use base64::prelude::*; +use bytes::Bytes; +use http::{header::ToStrError, HeaderMap, HeaderValue, StatusCode}; +use lambda_runtime_api_client::body::Body; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, convert::TryFrom}; - -#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct Diagnostic<'a> { - pub(crate) error_type: &'a str, - pub(crate) error_message: &'a str, -} - -/// The request ID, which identifies the request that triggered the function invocation. This header -/// tracks the invocation within the Lambda control plane. The request ID is used to specify completion -/// of a given invocation. -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct RequestId(pub String); - -/// The date that the function times out in Unix time milliseconds. For example, `1542409706888`. -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct InvocationDeadline(pub u64); - -/// The ARN of the Lambda function, version, or alias that is specified in the invocation. -/// For instance, `arn:aws:lambda:us-east-2:123456789012:function:custom-runtime`. -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct FunctionArn(pub String); - -/// The AWS X-Ray Tracing header. For more information, -/// please see [AWS' documentation](https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-tracingheader). -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct XRayTraceId(pub String); - -/// For invocations from the AWS Mobile SDK contains data about client application and device. -#[derive(Debug, Clone, Eq, PartialEq)] -struct MobileClientContext(String); - -/// For invocations from the AWS Mobile SDK, data about the Amazon Cognito identity provider. -#[derive(Debug, Clone, Eq, PartialEq)] -struct MobileClientIdentity(String); +use std::{ + collections::HashMap, + fmt::Debug, + time::{Duration, SystemTime}, +}; +use tokio_stream::Stream; /// Client context sent by the AWS Mobile SDK. #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] @@ -57,31 +37,39 @@ pub struct ClientContext { #[serde(rename_all = "camelCase")] pub struct ClientApplication { /// The mobile app installation id + #[serde(alias = "installation_id")] pub installation_id: String, /// The app title for the mobile app as registered with AWS' mobile services. + #[serde(alias = "app_title")] pub app_title: String, /// The version name of the application as registered with AWS' mobile services. + #[serde(alias = "app_version_name")] pub app_version_name: String, /// The app version code. + #[serde(alias = "app_version_code")] pub app_version_code: String, /// The package name for the mobile application invoking the function + #[serde(alias = "app_package_name")] pub app_package_name: String, } /// Cognito identity information sent with the event #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] pub struct CognitoIdentity { /// The unique identity id for the Cognito credentials invoking the function. + #[serde(alias = "cognitoIdentityId", alias = "identity_id")] pub identity_id: String, /// The identity pool id the caller is "registered" with. + #[serde(alias = "cognitoIdentityPoolId", alias = "identity_pool_id")] pub identity_pool_id: String, } /// The Lambda function execution context. The values in this struct /// are populated using the [Lambda environment variables](https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html) -/// and the headers returned by the poll request to the Runtime APIs. +/// and [the headers returned by the poll request to the Runtime APIs](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html#runtimes-api-next). #[non_exhaustive] -#[derive(Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize)] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct Context { /// The AWS request ID generated by the Lambda service. pub request_id: String, @@ -98,55 +86,95 @@ pub struct Context { /// unless the invocation request to the Lambda APIs was made using AWS /// credentials issues by Amazon Cognito Identity Pools. pub identity: Option, + /// The tenant ID for the current invocation. + pub tenant_id: Option, /// Lambda function configuration from the local environment variables. /// Includes information such as the function name, memory allocation, /// version, and log streams. - pub env_config: Config, + pub env_config: RefConfig, } -impl TryFrom for Context { - type Error = Error; - fn try_from(headers: HeaderMap) -> Result { - let client_context: Option = if let Some(value) = headers.get("lambda-runtime-client-context") { - serde_json::from_str(value.to_str()?)? +impl Default for Context { + fn default() -> Context { + Context { + request_id: "".to_owned(), + deadline: 0, + invoked_function_arn: "".to_owned(), + xray_trace_id: None, + client_context: None, + identity: None, + tenant_id: None, + env_config: std::sync::Arc::new(crate::Config::default()), + } + } +} + +impl Context { + /// Create a new [Context] struct based on the function configuration + /// and the incoming request data. + pub fn new(request_id: &str, env_config: RefConfig, headers: &HeaderMap) -> Result { + let client_context: Option = if let Some(value) = headers.get(LAMBDA_RUNTIME_CLIENT_CONTEXT) { + let raw = value.to_str()?; + if raw.is_empty() { + None + } else { + Some(serde_json::from_str(raw)?) + } } else { None }; - let identity: Option = if let Some(value) = headers.get("lambda-runtime-cognito-identity") { - serde_json::from_str(value.to_str()?)? + let identity: Option = if let Some(value) = headers.get(LAMBDA_RUNTIME_COGNITO_IDENTITY) { + let raw = value.to_str()?; + if raw.is_empty() { + None + } else { + Some(serde_json::from_str(raw)?) + } } else { None }; let ctx = Context { - request_id: headers - .get("lambda-runtime-aws-request-id") - .expect("missing lambda-runtime-aws-request-id header") - .to_str()? - .to_owned(), + request_id: request_id.to_owned(), deadline: headers - .get("lambda-runtime-deadline-ms") + .get(LAMBDA_RUNTIME_DEADLINE_MS) .expect("missing lambda-runtime-deadline-ms header") .to_str()? .parse::()?, invoked_function_arn: headers - .get("lambda-runtime-invoked-function-arn") + .get(LAMBDA_RUNTIME_INVOKED_FUNCTION_ARN) .unwrap_or(&HeaderValue::from_static( "No header lambda-runtime-invoked-function-arn found.", )) .to_str()? .to_owned(), xray_trace_id: headers - .get("lambda-runtime-trace-id") + .get(LAMBDA_RUNTIME_TRACE_ID) .map(|v| String::from_utf8_lossy(v.as_bytes()).to_string()), client_context, identity, - ..Default::default() + tenant_id: headers + .get(LAMBDA_RUNTIME_TENANT_ID) + .map(|v| String::from_utf8_lossy(v.as_bytes()).to_string()), + env_config, }; Ok(ctx) } + + /// The execution deadline for the current invocation. + pub fn deadline(&self) -> SystemTime { + SystemTime::UNIX_EPOCH + Duration::from_millis(self.deadline) + } +} + +/// Extract the invocation request id from the incoming request. +pub(crate) fn invoke_request_id(headers: &HeaderMap) -> Result<&str, ToStrError> { + headers + .get(LAMBDA_RUNTIME_REQUEST_ID) + .expect("missing lambda-runtime-aws-request-id header") + .to_str() } /// Incoming Lambda request containing the event payload and context. @@ -170,28 +198,115 @@ impl LambdaEvent { } } +/// Metadata prelude for a stream response. +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct MetadataPrelude { + #[serde(with = "http_serde::status_code")] + /// The HTTP status code. + pub status_code: StatusCode, + #[serde(with = "http_serde::header_map")] + /// The HTTP headers. + pub headers: HeaderMap, + /// The HTTP cookies. + pub cookies: Vec, +} + +pub trait ToStreamErrorTrailer { + /// Convert the hyper error into a stream error trailer. + fn to_tailer(&self) -> String; +} + +impl ToStreamErrorTrailer for Error { + fn to_tailer(&self) -> String { + format!( + "Lambda-Runtime-Function-Error-Type: Runtime.StreamError\r\nLambda-Runtime-Function-Error-Body: {}\r\n", + BASE64_STANDARD.encode(self.to_string()) + ) + } +} + +/// A streaming response that contains the metadata prelude and the stream of bytes that will be +/// sent to the client. +#[derive(Debug)] +pub struct StreamResponse { + /// The metadata prelude. + pub metadata_prelude: MetadataPrelude, + /// The stream of bytes that will be sent to the client. + pub stream: S, +} + +/// An enum representing the response of a function that can return either a buffered +/// response of type `B` or a streaming response of type `S`. +pub enum FunctionResponse { + /// A buffered response containing the entire payload of the response. This is useful + /// for responses that can be processed quickly and have a relatively small payload size(<= 6MB). + BufferedResponse(B), + /// A streaming response that delivers the payload incrementally. This is useful for + /// large payloads(> 6MB) or responses that take a long time to generate. The client can start + /// processing the response as soon as the first chunk is available, without waiting + /// for the entire payload to be generated. + StreamingResponse(StreamResponse), +} + +/// a trait that can be implemented for any type that can be converted into a FunctionResponse. +/// This allows us to use the `into` method to convert a type into a FunctionResponse. +pub trait IntoFunctionResponse { + /// Convert the type into a FunctionResponse. + fn into_response(self) -> FunctionResponse; +} + +impl IntoFunctionResponse for FunctionResponse { + fn into_response(self) -> FunctionResponse { + self + } +} + +impl IntoFunctionResponse for B +where + B: Serialize, +{ + fn into_response(self) -> FunctionResponse { + FunctionResponse::BufferedResponse(self) + } +} + +impl IntoFunctionResponse<(), S> for StreamResponse +where + S: Stream> + Unpin + Send + 'static, + D: Into + Send, + E: Into + Send + Debug, +{ + fn into_response(self) -> FunctionResponse<(), S> { + FunctionResponse::StreamingResponse(self) + } +} + +impl From for StreamResponse +where + S: Stream> + Unpin + Send + 'static, + D: Into + Send, + E: Into + Send + Debug, +{ + fn from(value: S) -> Self { + StreamResponse { + metadata_prelude: Default::default(), + stream: value, + } + } +} + #[cfg(test)] mod test { - use super::*; - #[test] - fn round_trip_lambda_error() { - use serde_json::{json, Value}; - let expected = json!({ - "errorType": "InvalidEventDataError", - "errorMessage": "Error parsing event data.", - }); - - let actual = Diagnostic { - error_type: "InvalidEventDataError", - error_message: "Error parsing event data.", - }; - let actual: Value = serde_json::to_value(actual).expect("failed to serialize diagnostic"); - assert_eq!(expected, actual); - } + use super::*; + use crate::Config; + use std::sync::Arc; #[test] fn context_with_expected_values_and_types_resolves() { + let config = Arc::new(Config::default()); + let mut headers = HeaderMap::new(); headers.insert("lambda-runtime-aws-request-id", HeaderValue::from_static("my-id")); headers.insert("lambda-runtime-deadline-ms", HeaderValue::from_static("123")); @@ -200,16 +315,18 @@ mod test { HeaderValue::from_static("arn::myarn"), ); headers.insert("lambda-runtime-trace-id", HeaderValue::from_static("arn::myarn")); - let tried = Context::try_from(headers); + let tried = Context::new("id", config, &headers); assert!(tried.is_ok()); } #[test] fn context_with_certain_missing_headers_still_resolves() { + let config = Arc::new(Config::default()); + let mut headers = HeaderMap::new(); headers.insert("lambda-runtime-aws-request-id", HeaderValue::from_static("my-id")); headers.insert("lambda-runtime-deadline-ms", HeaderValue::from_static("123")); - let tried = Context::try_from(headers); + let tried = Context::new("id", config, &headers); assert!(tried.is_ok()); } @@ -238,7 +355,9 @@ mod test { "lambda-runtime-client-context", HeaderValue::from_str(&client_context_str).unwrap(), ); - let tried = Context::try_from(headers); + + let config = Arc::new(Config::default()); + let tried = Context::new("id", config, &headers); assert!(tried.is_ok()); let tried = tried.unwrap(); assert!(tried.client_context.is_some()); @@ -247,17 +366,20 @@ mod test { #[test] fn context_with_empty_client_context_resolves() { + let config = Arc::new(Config::default()); let mut headers = HeaderMap::new(); headers.insert("lambda-runtime-aws-request-id", HeaderValue::from_static("my-id")); headers.insert("lambda-runtime-deadline-ms", HeaderValue::from_static("123")); headers.insert("lambda-runtime-client-context", HeaderValue::from_static("{}")); - let tried = Context::try_from(headers); + let tried = Context::new("id", config, &headers); assert!(tried.is_ok()); assert!(tried.unwrap().client_context.is_some()); } #[test] fn context_with_identity_resolves() { + let config = Arc::new(Config::default()); + let cognito_identity = CognitoIdentity { identity_id: String::new(), identity_pool_id: String::new(), @@ -270,7 +392,7 @@ mod test { "lambda-runtime-cognito-identity", HeaderValue::from_str(&cognito_identity_str).unwrap(), ); - let tried = Context::try_from(headers); + let tried = Context::new("id", config, &headers); assert!(tried.is_ok()); let tried = tried.unwrap(); assert!(tried.identity.is_some()); @@ -279,6 +401,8 @@ mod test { #[test] fn context_with_bad_deadline_type_is_err() { + let config = Arc::new(Config::default()); + let mut headers = HeaderMap::new(); headers.insert("lambda-runtime-aws-request-id", HeaderValue::from_static("my-id")); headers.insert( @@ -290,12 +414,14 @@ mod test { HeaderValue::from_static("arn::myarn"), ); headers.insert("lambda-runtime-trace-id", HeaderValue::from_static("arn::myarn")); - let tried = Context::try_from(headers); + let tried = Context::new("id", config, &headers); assert!(tried.is_err()); } #[test] fn context_with_bad_client_context_is_err() { + let config = Arc::new(Config::default()); + let mut headers = HeaderMap::new(); headers.insert("lambda-runtime-aws-request-id", HeaderValue::from_static("my-id")); headers.insert("lambda-runtime-deadline-ms", HeaderValue::from_static("123")); @@ -303,22 +429,26 @@ mod test { "lambda-runtime-client-context", HeaderValue::from_static("BAD-Type,not JSON"), ); - let tried = Context::try_from(headers); + let tried = Context::new("id", config, &headers); assert!(tried.is_err()); } #[test] fn context_with_empty_identity_is_err() { + let config = Arc::new(Config::default()); + let mut headers = HeaderMap::new(); headers.insert("lambda-runtime-aws-request-id", HeaderValue::from_static("my-id")); headers.insert("lambda-runtime-deadline-ms", HeaderValue::from_static("123")); headers.insert("lambda-runtime-cognito-identity", HeaderValue::from_static("{}")); - let tried = Context::try_from(headers); + let tried = Context::new("id", config, &headers); assert!(tried.is_err()); } #[test] fn context_with_bad_identity_is_err() { + let config = Arc::new(Config::default()); + let mut headers = HeaderMap::new(); headers.insert("lambda-runtime-aws-request-id", HeaderValue::from_static("my-id")); headers.insert("lambda-runtime-deadline-ms", HeaderValue::from_static("123")); @@ -326,28 +456,42 @@ mod test { "lambda-runtime-cognito-identity", HeaderValue::from_static("BAD-Type,not JSON"), ); - let tried = Context::try_from(headers); + let tried = Context::new("id", config, &headers); assert!(tried.is_err()); } #[test] #[should_panic] - #[allow(unused_must_use)] - fn context_with_missing_request_id_should_panic() { + fn context_with_missing_deadline_should_panic() { + let config = Arc::new(Config::default()); + + let mut headers = HeaderMap::new(); + headers.insert("lambda-runtime-aws-request-id", HeaderValue::from_static("my-id")); + headers.insert( + "lambda-runtime-invoked-function-arn", + HeaderValue::from_static("arn::myarn"), + ); + headers.insert("lambda-runtime-trace-id", HeaderValue::from_static("arn::myarn")); + let _ = Context::new("id", config, &headers); + } + + #[test] + fn invoke_request_id_should_not_panic() { let mut headers = HeaderMap::new(); headers.insert("lambda-runtime-aws-request-id", HeaderValue::from_static("my-id")); + headers.insert("lambda-runtime-deadline-ms", HeaderValue::from_static("123")); headers.insert( "lambda-runtime-invoked-function-arn", HeaderValue::from_static("arn::myarn"), ); headers.insert("lambda-runtime-trace-id", HeaderValue::from_static("arn::myarn")); - Context::try_from(headers); + + let _ = invoke_request_id(&headers); } #[test] #[should_panic] - #[allow(unused_must_use)] - fn context_with_missing_deadline_should_panic() { + fn invoke_request_id_should_panic() { let mut headers = HeaderMap::new(); headers.insert("lambda-runtime-deadline-ms", HeaderValue::from_static("123")); headers.insert( @@ -355,16 +499,48 @@ mod test { HeaderValue::from_static("arn::myarn"), ); headers.insert("lambda-runtime-trace-id", HeaderValue::from_static("arn::myarn")); - Context::try_from(headers); + + let _ = invoke_request_id(&headers); } -} -impl Context { - /// Add environment details to the context by setting `env_config`. - pub fn with_config(self, config: &Config) -> Self { - Self { - env_config: config.clone(), - ..self - } + #[test] + fn serde_metadata_prelude() { + let metadata_prelude = MetadataPrelude { + status_code: StatusCode::OK, + headers: { + let mut headers = HeaderMap::new(); + headers.insert("key", "val".parse().unwrap()); + headers + }, + cookies: vec!["cookie".to_string()], + }; + + let serialized = serde_json::to_string(&metadata_prelude).unwrap(); + let deserialized: MetadataPrelude = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(metadata_prelude, deserialized); + } + + #[test] + fn context_with_tenant_id_resolves() { + let config = Arc::new(Config::default()); + let mut headers = HeaderMap::new(); + headers.insert("lambda-runtime-aws-request-id", HeaderValue::from_static("my-id")); + headers.insert("lambda-runtime-deadline-ms", HeaderValue::from_static("123")); + headers.insert("lambda-runtime-aws-tenant-id", HeaderValue::from_static("tenant-123")); + + let context = Context::new("id", config, &headers).unwrap(); + assert_eq!(context.tenant_id, Some("tenant-123".to_string())); + } + + #[test] + fn context_without_tenant_id_resolves() { + let config = Arc::new(Config::default()); + let mut headers = HeaderMap::new(); + headers.insert("lambda-runtime-aws-request-id", HeaderValue::from_static("my-id")); + headers.insert("lambda-runtime-deadline-ms", HeaderValue::from_static("123")); + + let context = Context::new("id", config, &headers).unwrap(); + assert_eq!(context.tenant_id, None); } } diff --git a/lambda-runtime/tests/tenant_id_integration.rs b/lambda-runtime/tests/tenant_id_integration.rs new file mode 100644 index 000000000..39d65ccf4 --- /dev/null +++ b/lambda-runtime/tests/tenant_id_integration.rs @@ -0,0 +1,93 @@ +use http::HeaderMap; +use lambda_runtime::Context; +use std::sync::Arc; + +#[test] +fn test_context_tenant_id_extraction() { + let config = Arc::new(lambda_runtime::Config::default()); + + // Test with tenant ID + let mut headers = HeaderMap::new(); + headers.insert("lambda-runtime-aws-request-id", "test-id".parse().unwrap()); + headers.insert("lambda-runtime-deadline-ms", "123456789".parse().unwrap()); + headers.insert("lambda-runtime-aws-tenant-id", "my-tenant-123".parse().unwrap()); + + let context = Context::new("test-id", config.clone(), &headers).unwrap(); + assert_eq!(context.tenant_id, Some("my-tenant-123".to_string())); + + // Test without tenant ID + let mut headers = HeaderMap::new(); + headers.insert("lambda-runtime-aws-request-id", "test-id".parse().unwrap()); + headers.insert("lambda-runtime-deadline-ms", "123456789".parse().unwrap()); + + let context = Context::new("test-id", config, &headers).unwrap(); + assert_eq!(context.tenant_id, None); +} + +#[test] +fn test_context_tenant_id_with_special_characters() { + let config = Arc::new(lambda_runtime::Config::default()); + + // Test with tenant ID containing special characters + let mut headers = HeaderMap::new(); + headers.insert("lambda-runtime-aws-request-id", "test-id".parse().unwrap()); + headers.insert("lambda-runtime-deadline-ms", "123456789".parse().unwrap()); + headers.insert( + "lambda-runtime-aws-tenant-id", + "tenant-with-dashes_and_underscores.123".parse().unwrap(), + ); + + let context = Context::new("test-id", config, &headers).unwrap(); + assert_eq!( + context.tenant_id, + Some("tenant-with-dashes_and_underscores.123".to_string()) + ); +} + +#[test] +fn test_context_tenant_id_empty_value() { + let config = Arc::new(lambda_runtime::Config::default()); + + // Test with empty tenant ID + let mut headers = HeaderMap::new(); + headers.insert("lambda-runtime-aws-request-id", "test-id".parse().unwrap()); + headers.insert("lambda-runtime-deadline-ms", "123456789".parse().unwrap()); + headers.insert("lambda-runtime-aws-tenant-id", "".parse().unwrap()); + + let context = Context::new("test-id", config, &headers).unwrap(); + assert_eq!(context.tenant_id, Some("".to_string())); +} + +#[test] +fn test_trace_layer_request_span_creation() { + use lambda_runtime::layers::trace::request_span; + + // Test with both trace ID and tenant ID + let mut context = Context::default(); + context.request_id = "test-request".to_string(); + context.xray_trace_id = Some("trace-123".to_string()); + context.tenant_id = Some("tenant-456".to_string()); + + let _span = request_span(&context); + // Just verify the span can be created without panicking + + // Test with only trace ID + let mut context = Context::default(); + context.request_id = "test-request".to_string(); + context.xray_trace_id = Some("trace-123".to_string()); + + let _span = request_span(&context); + + // Test with only tenant ID + let mut context = Context::default(); + context.request_id = "test-request".to_string(); + context.tenant_id = Some("tenant-only".to_string()); + + let _span = request_span(&context); + + // Test with only request ID + let mut context = Context::default(); + context.request_id = "test-request".to_string(); + + let _span = request_span(&context); +} diff --git a/release-plz.toml b/release-plz.toml new file mode 100644 index 000000000..67e95dca5 --- /dev/null +++ b/release-plz.toml @@ -0,0 +1,17 @@ +[workspace] +semver_check = true +git_release_enable = false + +[[package]] +name = "lambda_runtime" +git_release_enable = true +git_release_name = "v{{ version }}" +changelog_include = ["lambda_http", "lambda_runtime_api_client", "lambda-extension", "aws_lambda_events"] + +[[package]] +name = "lambda-integration-tests" +publish = false +release = false + +[changelog] +protect_breaking_commits = true diff --git a/scripts/build-examples.sh b/scripts/build-examples.sh new file mode 100755 index 000000000..2e577c3a0 --- /dev/null +++ b/scripts/build-examples.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -e + +OUTPUT_DIR="${OUTPUT_DIR:-/tmp/var-task}" +HANDLERS_TO_BUILD="${HANDLERS_TO_BUILD:-}" + +mkdir -p "$OUTPUT_DIR" + +echo "Building handlers: ${HANDLERS_TO_BUILD}" + + +for handler in ${HANDLERS_TO_BUILD}; do + dir="examples/$handler" + if [ ! -f "$dir/Cargo.toml" ]; then + echo "✗ $handler not found" + continue + fi + + echo "Building $handler..." + if ! (cd "$dir" && cargo build --release); then + continue + fi + + if [ ! -f "$dir/target/release/$handler" ]; then + echo "✗ $handler artifact not found" + continue + fi + + cp "$dir/target/release/$handler" "$OUTPUT_DIR/" + echo "✓ $handler" +done + +echo "" +ls -lh "$OUTPUT_DIR/" 2>/dev/null || echo "No binaries built" +exit 0 diff --git a/scripts/custom-lambda-entrypoint.sh b/scripts/custom-lambda-entrypoint.sh new file mode 100644 index 000000000..fb02b26c5 --- /dev/null +++ b/scripts/custom-lambda-entrypoint.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# custom entrypoint script to allow selection of multiple rust binaries for dockerized tests +set -e + +HANDLER=${1:-basic-lambda} + +if [ -f "/var/task/$HANDLER" ]; then + ln -sf "/var/task/$HANDLER" "${LAMBDA_RUNTIME_DIR}/bootstrap" + exec /usr/local/bin/aws-lambda-rie "${LAMBDA_RUNTIME_DIR}/bootstrap" +else + echo "Error: Handler '$HANDLER' not found in /var/task" + echo "Available handlers:" + ls -la /var/task + exit 1 +fi diff --git a/scripts/download-rie.sh b/scripts/download-rie.sh new file mode 100644 index 000000000..0d22509aa --- /dev/null +++ b/scripts/download-rie.sh @@ -0,0 +1,46 @@ +#!/bin/sh + +set -eu + +if [ "$#" -ne 5 ]; then + echo "Usage: $0 TARGETARCH RIE_VERSION RIE_SHA256_AMD64 RIE_SHA256_ARM64 RIE_PATH" >&2 + exit 1 +fi + +TARGETARCH=$1 +RIE_VERSION=$2 +RIE_SHA256_AMD64=$3 +RIE_SHA256_ARM64=$4 +RIE_PATH=$5 + +case "${TARGETARCH}" in + amd64) + RIE_ASSET=aws-lambda-rie + RIE_SHA256=${RIE_SHA256_AMD64} + ;; + arm64) + RIE_ASSET=aws-lambda-rie-arm64 + RIE_SHA256=${RIE_SHA256_ARM64} + ;; + *) + echo "Unsupported target architecture: ${TARGETARCH}" >&2 + exit 1 + ;; +esac + +: "${RIE_PATH:?RIE_PATH must be set}" +RIE_TMP=$(mktemp) +trap 'rm -f "${RIE_TMP}"' EXIT + +curl \ + --fail \ + --location \ + --silent \ + --show-error \ + --retry 3 \ + --retry-all-errors \ + --output "${RIE_TMP}" \ + "https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/download/v${RIE_VERSION}/${RIE_ASSET}" + +echo "${RIE_SHA256} ${RIE_TMP}" | sha256sum --check --status +install -m 0755 "${RIE_TMP}" "${RIE_PATH}" diff --git a/scripts/pre-commit b/scripts/pre-commit new file mode 100755 index 000000000..ae40a9f1a --- /dev/null +++ b/scripts/pre-commit @@ -0,0 +1,25 @@ +#!/bin/sh +# Pre-commit hook to run cargo fmt +# +# To install this hook, run: +# cp scripts/pre-commit .git/hooks/pre-commit +# chmod +x .git/hooks/pre-commit + +echo "Running cargo fmt..." + +# Run cargo fmt on the entire workspace +cargo fmt --all -- --check + +# Check if cargo fmt found any formatting issues +if [ $? -ne 0 ]; then + echo "" + echo "❌ Code formatting issues detected!" + echo "Running 'cargo fmt --all' to fix formatting..." + cargo fmt --all + echo "" + echo "✅ Formatting applied. Please review the changes and commit again." + exit 1 +fi + +echo "✅ Code formatting is correct!" +exit 0 diff --git a/scripts/test-rie.sh b/scripts/test-rie.sh new file mode 100755 index 000000000..9de6feddd --- /dev/null +++ b/scripts/test-rie.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -euo pipefail + +EXAMPLE=${1:-basic-lambda} +# Optional: set RIE_MAX_CONCURRENCY to enable LMI mode (emulates AWS_LAMBDA_MAX_CONCURRENCY) +RIE_MAX_CONCURRENCY=${RIE_MAX_CONCURRENCY:-} + +echo "Building Docker image with RIE for example: $EXAMPLE..." +docker build -f Dockerfile.rie --build-arg EXAMPLE=$EXAMPLE -t rust-lambda-rie-test . + +echo "Starting RIE container on port 9000..." +if [ -n "$RIE_MAX_CONCURRENCY" ]; then + echo "Enabling LMI mode with AWS_LAMBDA_MAX_CONCURRENCY=$RIE_MAX_CONCURRENCY" + docker run -p 9000:8080 -e AWS_LAMBDA_MAX_CONCURRENCY="$RIE_MAX_CONCURRENCY" rust-lambda-rie-test & +else + docker run -p 9000:8080 rust-lambda-rie-test & +fi +CONTAINER_PID=$! + +echo "Container started. Test with:" +if [ "$EXAMPLE" = "basic-lambda" ] || [ "$EXAMPLE" = "basic-lambda-concurrent" ] || [ "$EXAMPLE" = "invocation-id-concurrent" ]; then + echo "curl -XPOST 'http://localhost:9000/2015-03-31/functions/function/invocations' -d '{\"command\": \"test from RIE\"}' -H 'Content-Type: application/json'" +else + echo "For example '$EXAMPLE', check examples/$EXAMPLE/src/main.rs for the expected payload format." +fi +echo "" +echo "Press Ctrl+C to stop the container." + +wait $CONTAINER_PID \ No newline at end of file diff --git a/test/dockerized/scenarios/concurrent_scenarios.py b/test/dockerized/scenarios/concurrent_scenarios.py new file mode 100644 index 000000000..8370289b3 --- /dev/null +++ b/test/dockerized/scenarios/concurrent_scenarios.py @@ -0,0 +1,97 @@ +"""Multi-concurrency test scenarios.""" + +import os +from containerized_test_runner.models import ConcurrentTest, Request + +HANDLER = "basic-lambda-concurrent" +INVOCATION_ID_HANDLER = "invocation-id-concurrent" +IMAGE = os.environ.get("TEST_IMAGE", "local/test-base") +SAME_REQUEST_ID = "shared-request-id" +DEFAULT_CONCURRENCY = 10 +TIMEOUT = 5 + + +def _make_env(handler: str = HANDLER, concurrency: int = DEFAULT_CONCURRENCY) -> dict: + return { + "_HANDLER": handler, + "AWS_LAMBDA_MAX_CONCURRENCY": str(concurrency), + "AWS_LAMBDA_LOG_FORMAT": "JSON", + } + + +def _invocation_id_env( + handler: str = INVOCATION_ID_HANDLER, + concurrency: int = DEFAULT_CONCURRENCY, + timeout: int = TIMEOUT, +) -> dict: + return _make_env(handler, concurrency) | { + "AWS_LAMBDA_FUNCTION_TIMEOUT": str(timeout), + } + + +def get_concurrent_scenarios(): + scenarios = [] + + # Happy path: DEFAULT_CONCURRENCY unique commands all succeed concurrently + batch = [ + Request( + payload={"command": f"task-{i}"}, + assertions=[{"transform": "{msg: .msg}", "response": {"msg": f"Command task-{i} executed."}}], + ) + for i in range(DEFAULT_CONCURRENCY) + ] + scenarios.append(ConcurrentTest( + name="concurrent_happy_path", + handler=HANDLER, + environment_variables=_make_env(), + request_batches=[batch], + image=IMAGE, + )) + + # Error isolation: N-1 failing requests + 1 valid — the valid one must still succeed + mixed_batch = [ + Request( + payload={"command": "fail"}, + assertions=[{"errorType": "HandlerError"}], + ) + for _ in range(DEFAULT_CONCURRENCY - 1) + ] + [ + Request( + payload={"command": "survivor"}, + assertions=[{"transform": "{msg: .msg}", "response": {"msg": "Command survivor executed."}}], + ) + ] + scenarios.append(ConcurrentTest( + name="concurrent_error_isolation", + handler=HANDLER, + environment_variables=_make_env(), + request_batches=[mixed_batch], + image=IMAGE, + )) + + return scenarios + + +def get_invocation_id_scenarios(): + batches = [ + [Request.create( + payload={"command": "invoke-A", "sleep": TIMEOUT + 2}, + assertions=[{"transform": ".errorType", "error": "Sandbox.Timedout"}], + headers={"X-Amzn-RequestId": SAME_REQUEST_ID}, + )], + [Request.create( + payload={"command": "invoke-B", "sleep": TIMEOUT - 1}, + assertions=[{"response": {"from": "invoke-B"}}], + headers={"X-Amzn-RequestId": SAME_REQUEST_ID}, + )], + ] + + + return [ConcurrentTest( + name="invocation_id", + handler=INVOCATION_ID_HANDLER, + environment_variables=_invocation_id_env(handler=INVOCATION_ID_HANDLER, timeout=TIMEOUT), + request_batches=batches, + image=IMAGE, + )] + diff --git a/test/dockerized/suites/core.json b/test/dockerized/suites/core.json new file mode 100644 index 000000000..d5d56ce7d --- /dev/null +++ b/test/dockerized/suites/core.json @@ -0,0 +1,90 @@ +{ + "tests": [ + { + "name": "test_echo", + "handler": "basic-lambda", + "request": { + "command": "test" + }, + "assertions": [ + { + "response": { + "msg": "Command test executed." + }, + "transform": "{msg: .msg}" + } + ] + }, + { + "name": "test_basic_sqs", + "handler": "basic-sqs", + "request": { + "Records": [ + { + "messageId": "msg-001", + "receiptHandle": "receipt-001", + "body": "{\"id\":\"user-123\",\"text\":\"Hello from SQS\"}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1234567890", + "SenderId": "AIDAI123456789", + "ApproximateFirstReceiveTimestamp": "1234567890" + }, + "messageAttributes": {}, + "md5OfBody": "abc123", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:test-queue", + "awsRegion": "us-east-1" + } + ] + }, + "assertions": [ + { + "response": null + } + ] + }, + { + "name": "test_http_basic_lambda", + "handler": "http-basic-lambda", + "request": { + "version": "2.0", + "routeKey": "$default", + "rawPath": "/", + "rawQueryString": "", + "headers": { + "accept": "text/html", + "user-agent": "test-client" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "domainName": "example.com", + "domainPrefix": "api", + "http": { + "method": "GET", + "path": "/", + "protocol": "HTTP/1.1", + "sourceIp": "127.0.0.1", + "userAgent": "test-client" + }, + "requestId": "req-001", + "routeKey": "$default", + "stage": "$default", + "time": "01/Jan/2024:00:00:00 +0000", + "timeEpoch": 1704067200000 + }, + "isBase64Encoded": false + }, + "assertions": [ + { + "response": { + "statusCode": 200, + "body": "Hello AWS Lambda HTTP request" + }, + "transform": "{statusCode: .statusCode, body: .body}" + } + ] + } + ] +}